From befc1334fdb781ddbe39d7a52ab6f1de4e5abfc3 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 11 Dec 2025 22:41:24 +0000 Subject: [PATCH 01/34] feat: create project structure for Backend, Admin, Operaciones, Campo - backend/: .NET 8 Web API with Clean Architecture - frontend-admin/: Angular 18+ for Control Tower (Admin) - frontend-operaciones/: React PWA for Warehouse Ops (Tablet) - frontend-campo/: React PWA for Field Drivers (Mobile) Each folder includes README with stack info and planned features. --- backend/README.md | 22 ++++++++++++++++++++++ frontend-admin/README.md | 16 ++++++++++++++++ frontend-campo/README.md | 14 ++++++++++++++ frontend-operaciones/README.md | 13 +++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 backend/README.md create mode 100644 frontend-admin/README.md create mode 100644 frontend-campo/README.md create mode 100644 frontend-operaciones/README.md diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..8c302c9 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,22 @@ +# Backend - Parhelion API + +**Stack:** C# / .NET 8 Web API +**Patrón:** Clean Architecture +**Base de Datos:** PostgreSQL + Entity Framework Core + +## Estructura Planificada + +``` +backend/ +├── src/ +│ ├── Parhelion.Domain/ # Entidades, Enums, Exceptions +│ ├── Parhelion.Application/ # DTOs, Interfaces, Validators +│ ├── Parhelion.Infrastructure/ # EF Core, Repositorios, Services +│ └── Parhelion.API/ # Controllers, JWT, Swagger +└── tests/ + └── Parhelion.Tests/ +``` + +## Documentación API + +Swagger disponible en: `/swagger` diff --git a/frontend-admin/README.md b/frontend-admin/README.md new file mode 100644 index 0000000..66f297c --- /dev/null +++ b/frontend-admin/README.md @@ -0,0 +1,16 @@ +# Frontend Admin - Control Tower + +**Stack:** Angular 18+ con Angular Material +**Usuario:** Administrador / Gerente de Tráfico +**Dispositivo:** PC / Laptop + +## Funcionalidades + +- Dashboard con KPIs operativos +- Gestión de Flotilla (Camiones tipificados) +- Gestión de Choferes +- Red Logística y Rutas Hub & Spoke +- Creación y seguimiento de Envíos +- Generación de QR para transferencia de custodia +- Documentación B2B (Carta Porte, Manifiestos) +- Modo Demo para reclutadores diff --git a/frontend-campo/README.md b/frontend-campo/README.md new file mode 100644 index 0000000..b416859 --- /dev/null +++ b/frontend-campo/README.md @@ -0,0 +1,14 @@ +# Frontend Campo - App Chofer + +**Stack:** React (PWA - Progressive Web App) +**Usuario:** Chofer / Operador de Campo +**Dispositivo:** Celular (uso con una mano) + +## Funcionalidades + +- Hoja de Ruta con paradas asignadas +- Confirmación de llegada a Hubs +- Escaneo de QR para toma de custodia +- Confirmación de entregas +- Captura de firma digital (POD) +- Reporte de excepciones/incidencias diff --git a/frontend-operaciones/README.md b/frontend-operaciones/README.md new file mode 100644 index 0000000..a454b63 --- /dev/null +++ b/frontend-operaciones/README.md @@ -0,0 +1,13 @@ +# Frontend Operaciones - App Almacenista + +**Stack:** React (PWA - Progressive Web App) +**Usuario:** Almacenista / Operador de Bodega +**Dispositivo:** Tablet + +## Funcionalidades + +- Gestión de carga de camiones +- Checklist de estiba (Manifiesto) +- Validación de peso y volumen +- Escaneo de QR para transferencia de custodia +- Vista de envíos pendientes por cargar From 448d19ea7e5f10de079fba5a05d511dc9170266b Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 11 Dec 2025 22:42:42 +0000 Subject: [PATCH 02/34] docs: add Git Flow branching strategy and GitHub Actions placeholder - BRANCHING.md: Document branch strategy (main, develop, feature/*) - .github/workflows/README.md: Planned CI/CD pipelines for all projects - Feature branches created for each project area --- .github/workflows/README.md | 46 +++++++++++++++++++++ BRANCHING.md | 79 +++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 .github/workflows/README.md create mode 100644 BRANCHING.md diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..a677a9d --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,46 @@ +# GitHub Actions - CI/CD Pipeline + +## Workflows Planificados + +### 🔧 Backend (.NET 8) + +| Archivo | Trigger | Acciones | +| :------------------- | :---------------------------- | :---------------------------------- | +| `backend-ci.yml` | Push a `develop`, PR a `main` | Build, Test, Lint | +| `backend-deploy.yml` | Push a `main` | Build Docker, Deploy a DigitalOcean | + +### 🎨 Frontend Admin (Angular) + +| Archivo | Trigger | Acciones | +| :----------------- | :---------------------------- | :--------------------------------- | +| `admin-ci.yml` | Push a `develop`, PR a `main` | Build, Lint, Test | +| `admin-deploy.yml` | Push a `main` | Build, Deploy a DigitalOcean/Nginx | + +### 📱 Frontend Operaciones (React PWA) + +| Archivo | Trigger | Acciones | +| :----------------------- | :---------------------------- | :---------------- | +| `operaciones-ci.yml` | Push a `develop`, PR a `main` | Build, Lint | +| `operaciones-deploy.yml` | Push a `main` | Build PWA, Deploy | + +### 📲 Frontend Campo (React PWA) + +| Archivo | Trigger | Acciones | +| :----------------- | :---------------------------- | :---------------- | +| `campo-ci.yml` | Push a `develop`, PR a `main` | Build, Lint | +| `campo-deploy.yml` | Push a `main` | Build PWA, Deploy | + +--- + +## Dominios Objetivo + +| Servicio | URL | +| :------------------- | :----------------------- | +| API Backend | `api.macrostasis.lat` | +| Frontend Admin | `admin.macrostasis.lat` | +| Frontend Operaciones | `ops.macrostasis.lat` | +| Frontend Campo | `driver.macrostasis.lat` | + +--- + +**Nota:** Los workflows se implementarán cuando los proyectos estén configurados. diff --git a/BRANCHING.md b/BRANCHING.md new file mode 100644 index 0000000..1d228f1 --- /dev/null +++ b/BRANCHING.md @@ -0,0 +1,79 @@ +# Estrategia de Ramas - Git Flow + +## Ramas Principales + +| Rama | Propósito | Protección | +| :-------- | :---------------------- | :---------------------- | +| `main` | Producción estable | ✅ Requiere PR + Review | +| `develop` | Integración de features | ✅ Requiere PR | + +## Ramas de Trabajo + +| Prefijo | Uso | Ejemplo | +| :---------- | :---------------------------- | :---------------------------- | +| `feature/*` | Nuevas funcionalidades | `feature/backend-setup` | +| `bugfix/*` | Correcciones en develop | `bugfix/fix-login-validation` | +| `hotfix/*` | Correcciones urgentes en prod | `hotfix/fix-critical-crash` | +| `release/*` | Preparación de release | `release/v1.0.0` | + +## Feature Branches Actuales + +``` +feature/backend-setup → Configuración inicial .NET 8 + Clean Architecture +feature/frontend-admin → Proyecto Angular 18 + Material +feature/frontend-operaciones → React PWA para Almacenistas (Tablet) +feature/frontend-campo → React PWA para Choferes (Mobile) +``` + +## Flujo de Trabajo + +```mermaid +gitGraph + commit id: "Initial" + branch develop + checkout develop + commit id: "Setup" + branch feature/backend-setup + checkout feature/backend-setup + commit id: "Add .NET project" + commit id: "Add entities" + checkout develop + merge feature/backend-setup + branch feature/frontend-admin + checkout feature/frontend-admin + commit id: "Add Angular project" + checkout develop + merge feature/frontend-admin + checkout main + merge develop tag: "v1.0.0" +``` + +## Reglas + +1. **Nunca** hacer push directo a `main` +2. **Siempre** crear PR de `feature/*` → `develop` +3. **Solo** hacer merge a `main` desde `develop` via Release +4. Los **commits** deben seguir Conventional Commits: + - `feat:` Nueva funcionalidad + - `fix:` Corrección de bug + - `docs:` Documentación + - `refactor:` Refactorización sin cambio funcional + - `test:` Agregar/modificar tests + - `chore:` Tareas de mantenimiento + +## Comandos Útiles + +```bash +# Crear nueva feature +git checkout develop +git pull origin develop +git checkout -b feature/nombre-feature + +# Terminar feature (crear PR en GitHub) +git push -u origin feature/nombre-feature +# → Crear Pull Request en GitHub: feature/* → develop + +# Sincronizar develop local +git checkout develop +git pull origin develop +``` From c21be750a3a74b7e044469e67fe84b111e14fd48 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 11 Dec 2025 23:29:31 +0000 Subject: [PATCH 03/34] feat: add project scaffolding for all 4 applications Backend: - .NET 8 solution with Clean Architecture structure - Parhelion.Domain, Parhelion.Application, Parhelion.Infrastructure, Parhelion.API Frontend Admin (Angular 18): - Angular project with routing and SCSS Frontend Operaciones (React + Vite): - React TypeScript PWA for warehouse operations Frontend Campo (React + Vite): - React TypeScript PWA for field drivers Also added root .gitignore for all projects --- .gitignore | 68 + backend/Parhelion.sln | 48 + .../src/Parhelion.API/Parhelion.API.csproj | 14 + backend/src/Parhelion.API/Parhelion.API.http | 6 + backend/src/Parhelion.API/Program.cs | 44 + .../Properties/launchSettings.json | 41 + backend/src/Parhelion.API/appsettings.json | 9 + backend/src/Parhelion.Application/Class1.cs | 6 + .../Parhelion.Application.csproj | 9 + backend/src/Parhelion.Domain/Class1.cs | 6 + .../Parhelion.Domain/Parhelion.Domain.csproj | 9 + .../src/Parhelion.Infrastructure/Class1.cs | 6 + .../Parhelion.Infrastructure.csproj | 9 + frontend-admin/.editorconfig | 17 + frontend-admin/.gitignore | 42 + frontend-admin/.vscode/extensions.json | 4 + frontend-admin/.vscode/launch.json | 20 + frontend-admin/.vscode/tasks.json | 42 + frontend-admin/README.md | 43 +- frontend-admin/angular.json | 124 + frontend-admin/package-lock.json | 14384 ++++++++++++++++ frontend-admin/package.json | 38 + frontend-admin/public/favicon.ico | Bin 0 -> 15086 bytes frontend-admin/src/app/app.component.html | 336 + frontend-admin/src/app/app.component.scss | 0 frontend-admin/src/app/app.component.ts | 13 + frontend-admin/src/app/app.config.ts | 8 + frontend-admin/src/app/app.routes.ts | 3 + frontend-admin/src/index.html | 13 + frontend-admin/src/main.ts | 6 + frontend-admin/src/styles.scss | 1 + frontend-admin/tsconfig.app.json | 15 + frontend-admin/tsconfig.json | 33 + frontend-admin/tsconfig.spec.json | 15 + frontend-campo/.gitignore | 24 + frontend-campo/README.md | 81 +- frontend-campo/eslint.config.js | 23 + frontend-campo/index.html | 13 + frontend-campo/package-lock.json | 3212 ++++ frontend-campo/package.json | 30 + frontend-campo/public/vite.svg | 1 + frontend-campo/src/App.css | 42 + frontend-campo/src/App.tsx | 35 + frontend-campo/src/assets/react.svg | 1 + frontend-campo/src/index.css | 68 + frontend-campo/src/main.tsx | 10 + frontend-campo/tsconfig.app.json | 28 + frontend-campo/tsconfig.json | 7 + frontend-campo/tsconfig.node.json | 26 + frontend-campo/vite.config.ts | 7 + frontend-operaciones/.gitignore | 24 + frontend-operaciones/README.md | 80 +- frontend-operaciones/eslint.config.js | 23 + frontend-operaciones/index.html | 13 + frontend-operaciones/package-lock.json | 3212 ++++ frontend-operaciones/package.json | 30 + frontend-operaciones/public/vite.svg | 1 + frontend-operaciones/src/App.css | 42 + frontend-operaciones/src/App.tsx | 35 + frontend-operaciones/src/assets/react.svg | 1 + frontend-operaciones/src/index.css | 68 + frontend-operaciones/src/main.tsx | 10 + frontend-operaciones/tsconfig.app.json | 28 + frontend-operaciones/tsconfig.json | 7 + frontend-operaciones/tsconfig.node.json | 26 + frontend-operaciones/vite.config.ts | 7 + 66 files changed, 22610 insertions(+), 37 deletions(-) create mode 100644 .gitignore create mode 100644 backend/Parhelion.sln create mode 100644 backend/src/Parhelion.API/Parhelion.API.csproj create mode 100644 backend/src/Parhelion.API/Parhelion.API.http create mode 100644 backend/src/Parhelion.API/Program.cs create mode 100644 backend/src/Parhelion.API/Properties/launchSettings.json create mode 100644 backend/src/Parhelion.API/appsettings.json create mode 100644 backend/src/Parhelion.Application/Class1.cs create mode 100644 backend/src/Parhelion.Application/Parhelion.Application.csproj create mode 100644 backend/src/Parhelion.Domain/Class1.cs create mode 100644 backend/src/Parhelion.Domain/Parhelion.Domain.csproj create mode 100644 backend/src/Parhelion.Infrastructure/Class1.cs create mode 100644 backend/src/Parhelion.Infrastructure/Parhelion.Infrastructure.csproj create mode 100644 frontend-admin/.editorconfig create mode 100644 frontend-admin/.gitignore create mode 100644 frontend-admin/.vscode/extensions.json create mode 100644 frontend-admin/.vscode/launch.json create mode 100644 frontend-admin/.vscode/tasks.json create mode 100644 frontend-admin/angular.json create mode 100644 frontend-admin/package-lock.json create mode 100644 frontend-admin/package.json create mode 100644 frontend-admin/public/favicon.ico create mode 100644 frontend-admin/src/app/app.component.html create mode 100644 frontend-admin/src/app/app.component.scss create mode 100644 frontend-admin/src/app/app.component.ts create mode 100644 frontend-admin/src/app/app.config.ts create mode 100644 frontend-admin/src/app/app.routes.ts create mode 100644 frontend-admin/src/index.html create mode 100644 frontend-admin/src/main.ts create mode 100644 frontend-admin/src/styles.scss create mode 100644 frontend-admin/tsconfig.app.json create mode 100644 frontend-admin/tsconfig.json create mode 100644 frontend-admin/tsconfig.spec.json create mode 100644 frontend-campo/.gitignore create mode 100644 frontend-campo/eslint.config.js create mode 100644 frontend-campo/index.html create mode 100644 frontend-campo/package-lock.json create mode 100644 frontend-campo/package.json create mode 100644 frontend-campo/public/vite.svg create mode 100644 frontend-campo/src/App.css create mode 100644 frontend-campo/src/App.tsx create mode 100644 frontend-campo/src/assets/react.svg create mode 100644 frontend-campo/src/index.css create mode 100644 frontend-campo/src/main.tsx create mode 100644 frontend-campo/tsconfig.app.json create mode 100644 frontend-campo/tsconfig.json create mode 100644 frontend-campo/tsconfig.node.json create mode 100644 frontend-campo/vite.config.ts create mode 100644 frontend-operaciones/.gitignore create mode 100644 frontend-operaciones/eslint.config.js create mode 100644 frontend-operaciones/index.html create mode 100644 frontend-operaciones/package-lock.json create mode 100644 frontend-operaciones/package.json create mode 100644 frontend-operaciones/public/vite.svg create mode 100644 frontend-operaciones/src/App.css create mode 100644 frontend-operaciones/src/App.tsx create mode 100644 frontend-operaciones/src/assets/react.svg create mode 100644 frontend-operaciones/src/index.css create mode 100644 frontend-operaciones/src/main.tsx create mode 100644 frontend-operaciones/tsconfig.app.json create mode 100644 frontend-operaciones/tsconfig.json create mode 100644 frontend-operaciones/tsconfig.node.json create mode 100644 frontend-operaciones/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ab753f --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# =========================== +# PARHELION-LOGISTICS GITIGNORE +# =========================== + +# ===== ENVIRONMENT & SECRETS ===== +.env +.env.* +!.env.example +*.local +appsettings.Development.json +appsettings.Local.json +secrets.json + +# ===== NODE (Angular/React) ===== +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +dist/ +build/ +.cache/ +.parcel-cache/ +.next/ +.nuxt/ +.vite/ + +# ===== .NET ===== +bin/ +obj/ +*.user +*.suo +*.userosscache +*.sln.docstates +.vs/ +*.csproj.user +publish/ +out/ + +# ===== IDE ===== +.idea/ +*.swp +*.swo +*~ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# ===== OS ===== +.DS_Store +Thumbs.db +*.log + +# ===== Docker ===== +docker-compose.override.yml + +# ===== Coverage & Testing ===== +coverage/ +*.lcov +.nyc_output/ +TestResults/ + +# ===== Misc ===== +*.tmp +*.temp +*.bak diff --git a/backend/Parhelion.sln b/backend/Parhelion.sln new file mode 100644 index 0000000..ec3ca27 --- /dev/null +++ b/backend/Parhelion.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0DB876F5-F853-4A2B-B99D-B70B16F3B8DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parhelion.Domain", "src\Parhelion.Domain\Parhelion.Domain.csproj", "{9CE90642-26E9-41D1-A0FC-E221B0926E21}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parhelion.Application", "src\Parhelion.Application\Parhelion.Application.csproj", "{145355DE-4C48-467D-8E8E-300BADDA0427}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parhelion.Infrastructure", "src\Parhelion.Infrastructure\Parhelion.Infrastructure.csproj", "{6DE6AD38-2121-4375-BA34-37389D4E9675}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parhelion.API", "src\Parhelion.API\Parhelion.API.csproj", "{7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9CE90642-26E9-41D1-A0FC-E221B0926E21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CE90642-26E9-41D1-A0FC-E221B0926E21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CE90642-26E9-41D1-A0FC-E221B0926E21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CE90642-26E9-41D1-A0FC-E221B0926E21}.Release|Any CPU.Build.0 = Release|Any CPU + {145355DE-4C48-467D-8E8E-300BADDA0427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {145355DE-4C48-467D-8E8E-300BADDA0427}.Debug|Any CPU.Build.0 = Debug|Any CPU + {145355DE-4C48-467D-8E8E-300BADDA0427}.Release|Any CPU.ActiveCfg = Release|Any CPU + {145355DE-4C48-467D-8E8E-300BADDA0427}.Release|Any CPU.Build.0 = Release|Any CPU + {6DE6AD38-2121-4375-BA34-37389D4E9675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DE6AD38-2121-4375-BA34-37389D4E9675}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DE6AD38-2121-4375-BA34-37389D4E9675}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DE6AD38-2121-4375-BA34-37389D4E9675}.Release|Any CPU.Build.0 = Release|Any CPU + {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9CE90642-26E9-41D1-A0FC-E221B0926E21} = {0DB876F5-F853-4A2B-B99D-B70B16F3B8DD} + {145355DE-4C48-467D-8E8E-300BADDA0427} = {0DB876F5-F853-4A2B-B99D-B70B16F3B8DD} + {6DE6AD38-2121-4375-BA34-37389D4E9675} = {0DB876F5-F853-4A2B-B99D-B70B16F3B8DD} + {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02} = {0DB876F5-F853-4A2B-B99D-B70B16F3B8DD} + EndGlobalSection +EndGlobal diff --git a/backend/src/Parhelion.API/Parhelion.API.csproj b/backend/src/Parhelion.API/Parhelion.API.csproj new file mode 100644 index 0000000..0902e17 --- /dev/null +++ b/backend/src/Parhelion.API/Parhelion.API.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/backend/src/Parhelion.API/Parhelion.API.http b/backend/src/Parhelion.API/Parhelion.API.http new file mode 100644 index 0000000..86c5bc2 --- /dev/null +++ b/backend/src/Parhelion.API/Parhelion.API.http @@ -0,0 +1,6 @@ +@Parhelion.API_HostAddress = http://localhost:5222 + +GET {{Parhelion.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs new file mode 100644 index 0000000..00ff539 --- /dev/null +++ b/backend/src/Parhelion.API/Program.cs @@ -0,0 +1,44 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast") +.WithOpenApi(); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/backend/src/Parhelion.API/Properties/launchSettings.json b/backend/src/Parhelion.API/Properties/launchSettings.json new file mode 100644 index 0000000..793313c --- /dev/null +++ b/backend/src/Parhelion.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:2828", + "sslPort": 44301 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5222", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7004;http://localhost:5222", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/src/Parhelion.API/appsettings.json b/backend/src/Parhelion.API/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/backend/src/Parhelion.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/backend/src/Parhelion.Application/Class1.cs b/backend/src/Parhelion.Application/Class1.cs new file mode 100644 index 0000000..4780c1c --- /dev/null +++ b/backend/src/Parhelion.Application/Class1.cs @@ -0,0 +1,6 @@ +namespace Parhelion.Application; + +public class Class1 +{ + +} diff --git a/backend/src/Parhelion.Application/Parhelion.Application.csproj b/backend/src/Parhelion.Application/Parhelion.Application.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/backend/src/Parhelion.Application/Parhelion.Application.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/backend/src/Parhelion.Domain/Class1.cs b/backend/src/Parhelion.Domain/Class1.cs new file mode 100644 index 0000000..3da44d9 --- /dev/null +++ b/backend/src/Parhelion.Domain/Class1.cs @@ -0,0 +1,6 @@ +namespace Parhelion.Domain; + +public class Class1 +{ + +} diff --git a/backend/src/Parhelion.Domain/Parhelion.Domain.csproj b/backend/src/Parhelion.Domain/Parhelion.Domain.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/backend/src/Parhelion.Domain/Parhelion.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/backend/src/Parhelion.Infrastructure/Class1.cs b/backend/src/Parhelion.Infrastructure/Class1.cs new file mode 100644 index 0000000..4a92927 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Class1.cs @@ -0,0 +1,6 @@ +namespace Parhelion.Infrastructure; + +public class Class1 +{ + +} diff --git a/backend/src/Parhelion.Infrastructure/Parhelion.Infrastructure.csproj b/backend/src/Parhelion.Infrastructure/Parhelion.Infrastructure.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Parhelion.Infrastructure.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/frontend-admin/.editorconfig b/frontend-admin/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/frontend-admin/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/frontend-admin/.gitignore b/frontend-admin/.gitignore new file mode 100644 index 0000000..cc7b141 --- /dev/null +++ b/frontend-admin/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/frontend-admin/.vscode/extensions.json b/frontend-admin/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/frontend-admin/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/frontend-admin/.vscode/launch.json b/frontend-admin/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/frontend-admin/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/frontend-admin/.vscode/tasks.json b/frontend-admin/.vscode/tasks.json new file mode 100644 index 0000000..a298b5b --- /dev/null +++ b/frontend-admin/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/frontend-admin/README.md b/frontend-admin/README.md index 66f297c..ddedb6d 100644 --- a/frontend-admin/README.md +++ b/frontend-admin/README.md @@ -1,16 +1,27 @@ -# Frontend Admin - Control Tower - -**Stack:** Angular 18+ con Angular Material -**Usuario:** Administrador / Gerente de Tráfico -**Dispositivo:** PC / Laptop - -## Funcionalidades - -- Dashboard con KPIs operativos -- Gestión de Flotilla (Camiones tipificados) -- Gestión de Choferes -- Red Logística y Rutas Hub & Spoke -- Creación y seguimiento de Envíos -- Generación de QR para transferencia de custodia -- Documentación B2B (Carta Porte, Manifiestos) -- Modo Demo para reclutadores +# ParhelionAdmin + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.21. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/frontend-admin/angular.json b/frontend-admin/angular.json new file mode 100644 index 0000000..69ef8f9 --- /dev/null +++ b/frontend-admin/angular.json @@ -0,0 +1,124 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "parhelion-admin": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss", + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/parhelion-admin", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kB", + "maximumError": "4kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "parhelion-admin:build:production" + }, + "development": { + "buildTarget": "parhelion-admin:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/frontend-admin/package-lock.json b/frontend-admin/package-lock.json new file mode 100644 index 0000000..20bb4a3 --- /dev/null +++ b/frontend-admin/package-lock.json @@ -0,0 +1,14384 @@ +{ + "name": "parhelion-admin", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "parhelion-admin", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^18.2.0", + "@angular/common": "^18.2.0", + "@angular/compiler": "^18.2.0", + "@angular/core": "^18.2.0", + "@angular/forms": "^18.2.0", + "@angular/platform-browser": "^18.2.0", + "@angular/platform-browser-dynamic": "^18.2.0", + "@angular/router": "^18.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.10" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.2.21", + "@angular/cli": "^18.2.21", + "@angular/compiler-cli": "^18.2.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.5.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1802.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.21.tgz", + "integrity": "sha512-+Ll+xtpKwZ3iLWN/YypvnCZV/F0MVbP+/7ZpMR+Xv/uB0OmribhBVj9WGaCd9I/bGgoYBw8wBV/NFNCKkf0k3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.21.tgz", + "integrity": "sha512-0pJfURFpEUV2USgZ2TL3nNAaJmF9bICx9OVddBoC+F9FeOpVKxkcVIb+c8Km5zHFo1iyVtPZ6Rb25vFk9Zm/ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.21", + "@angular-devkit/build-webpack": "0.1802.21", + "@angular-devkit/core": "18.2.21", + "@angular/build": "18.2.21", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", + "@discoveryjs/json-ext": "0.6.1", + "@ngtools/webpack": "18.2.21", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.1.3", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "critters": "0.0.24", + "css-loader": "7.1.2", + "esbuild-wasm": "0.23.0", + "fast-glob": "3.3.2", + "http-proxy-middleware": "3.0.5", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "magic-string": "0.30.11", + "mini-css-extract-plugin": "2.9.0", + "mrmime": "2.0.0", + "open": "10.1.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "postcss": "8.4.41", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.77.6", + "sass-loader": "16.0.0", + "semver": "7.6.3", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.31.6", + "tree-kill": "1.2.2", + "tslib": "2.6.3", + "watchpack": "2.4.1", + "webpack": "5.94.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.2.2", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.23.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^18.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1802.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.21.tgz", + "integrity": "sha512-2jSVRhA3N4Elg8OLcBktgi+CMSjlAm/bBQJE6TQYbdQWnniuT7JAWUHA/iPf7MYlQE5qj4rnAni1CI/c1Bk4HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1802.21", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.21.tgz", + "integrity": "sha512-Lno6GNbJME85wpc/uqn+wamBxvfZJZFYSH8+oAkkyjU/hk8r5+X8DuyqsKAa0m8t46zSTUsonHsQhVe5vgrZeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.21.tgz", + "integrity": "sha512-yuC2vN4VL48JhnsaOa9J/o0Jl+cxOklRNQp5J2/ypMuRROaVCrZAPiX+ChSHh++kHYMpj8+ggNrrUwRNfMKACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.11", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/animations": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.14.tgz", + "integrity": "sha512-Kp/MWShoYYO+R3lrrZbZgszbbLGVXHB+39mdJZwnIuZMDkeL3JsIBlSOzyJRTnpS1vITc+9jgHvP/6uKbMrW1Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14" + } + }, + "node_modules/@angular/build": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.21.tgz", + "integrity": "sha512-uvq3qP4cByJrUkV1ri0v3x6LxOFt4fDKiQdNwbQAqdxtfRs3ssEIoCGns4t89sTWXv6VZWBNDcDIKK9/Fa9mmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.21", + "@babel/core": "7.25.2", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.24.7", + "@inquirer/confirm": "3.1.22", + "@vitejs/plugin-basic-ssl": "1.1.0", + "browserslist": "^4.23.0", + "critters": "0.0.24", + "esbuild": "0.23.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "listr2": "8.2.4", + "lmdb": "3.0.13", + "magic-string": "0.30.11", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "rollup": "4.22.4", + "sass": "1.77.6", + "semver": "7.6.3", + "vite": "~5.4.17", + "watchpack": "2.4.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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/@angular/build/node_modules/@babel/core/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/@angular/build/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular/build/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/@angular/cli": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.21.tgz", + "integrity": "sha512-efweY4p8awRTbHs+HKdg6s44hl7Y0gdVlXYi3HeY8Z5JDC0abbka0K6sA/MrV9AXvn/5ovxYbxiL3AsOApjTpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1802.21", + "@angular-devkit/core": "18.2.21", + "@angular-devkit/schematics": "18.2.21", + "@inquirer/prompts": "5.3.8", + "@listr2/prompt-adapter-inquirer": "2.0.15", + "@schematics/angular": "18.2.21", + "@yarnpkg/lockfile": "1.1.0", + "ini": "4.1.3", + "jsonc-parser": "3.3.1", + "listr2": "8.2.4", + "npm-package-arg": "11.0.3", + "npm-pick-manifest": "9.1.0", + "pacote": "18.0.6", + "resolve": "1.22.8", + "semver": "7.6.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.14.tgz", + "integrity": "sha512-ZPRswzaVRiqcfZoowuAM22Hr2/z10ajWOUoFDoQ9tWqz/fH/773kJv2F9VvePIekgNPCzaizqv9gF6tGNqaAwg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.14.tgz", + "integrity": "sha512-Mpq3v/mztQzGAQAAFV+wAI1hlXxZ0m8eDBgaN2kD3Ue+r4S6bLm1Vlryw0iyUnt05PcFIdxPT6xkcphq5pl6lw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.14.tgz", + "integrity": "sha512-BmmjyrFSBSYkm0tBSqpu4cwnJX/b/XvhM36mj2k8jah3tNS5zLDDx5w6tyHmaPJa/1D95MlXx2h6u7K9D+Mhew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.25.2", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "18.2.14", + "typescript": ">=5.4 <5.6" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/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/@angular/core": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.14.tgz", + "integrity": "sha512-BIPrCs93ZZTY9ym7yfoTgAQ5rs706yoYeAdrgc8kh/bDbM9DawxKlgeKBx2FLt09Y0YQ1bFhKVp0cV4gDEaMxQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.10" + } + }, + "node_modules/@angular/forms": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.14.tgz", + "integrity": "sha512-fZVwXctmBJa5VdopJae/T9MYKPXNd04+6j4k/6X819y+9fiyWLJt2QicSc5Rc+YD9mmhXag3xaljlrnotf9VGA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.14.tgz", + "integrity": "sha512-W+JTxI25su3RiZVZT3Yrw6KNUCmOIy7OZIZ+612skPgYK2f2qil7VclnW1oCwG896h50cMJU/lnAfxZxefQgyQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "18.2.14", + "@angular/common": "18.2.14", + "@angular/core": "18.2.14" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.14.tgz", + "integrity": "sha512-QOv+o89u8HLN0LG8faTIVHKBxfkOBHVDB0UuXy19+HJofWZGGvho+vGjV0/IAkhZnMC4Sxdoy/mOHP2ytALX3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/compiler": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14" + } + }, + "node_modules/@angular/router": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.14.tgz", + "integrity": "sha512-v/gweh8MBjjDfh1QssuyjISa+6SVVIvIZox7MaMs81RkaoVHwS9grDtPud1pTKHzms2KxSVpvwwyvkRJQplueg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "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.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "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/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/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/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "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-compilation-targets/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/@babel/helper-create-class-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/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/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/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/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "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-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.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-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "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/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "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-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "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-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "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-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "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-block-scoping": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "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-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "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-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "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-exponentiation-operator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "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-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "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-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "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-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "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-logical-assignment-operators": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "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-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "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-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "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-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "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-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "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-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "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-optional-chaining": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "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-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "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-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "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-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "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-runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/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/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "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-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "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-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "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-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "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-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "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-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "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/preset-env/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/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.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/traverse/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/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/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", + "integrity": "sha512-boghen8F0Q8D+0/Q1/1r6DUEieUJ8w2a1gIknExMSHBsJFOr2+0KUfHiVYBvucPwl3+RU5PFBK833FjFCh3BhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", + "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", + "integrity": "sha512-MZrGem/Ujjd4cPTLYDfCZK2iKKeiO/8OX13S6jqxldLs0Prf2aGqVlJ77nMBqMv7fzqgXEgjrNHLXcKR8l9lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 6" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.0.13.tgz", + "integrity": "sha512-uiKPB0Fv6WEEOZjruu9a6wnW/8jrjzlZbxXscMB8kuCJ1k6kHpcBnuvaAWcqhbI7rqX5GKziwWEdD+wi2gNLfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.0.13.tgz", + "integrity": "sha512-bEVIIfK5mSQoG1R19qA+fJOvCB+0wVGGnXHT3smchBVahYBdlPn2OsZZKzlHWfb1E+PhLBmYfqB5zQXFP7hJig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.0.13.tgz", + "integrity": "sha512-Yml1KlMzOnXj/tnW7yX8U78iAzTk39aILYvCPbqeewAq1kSzl+w59k/fiVkTBfvDi/oW/5YRxL+Fq+Y1Fr1r2Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.0.13.tgz", + "integrity": "sha512-afbVrsMgZ9dUTNUchFpj5VkmJRxvht/u335jUJ7o23YTbNbnpmXif3VKQGCtnjSh+CZaqm6N3CPG8KO3zwyZ1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.0.13.tgz", + "integrity": "sha512-vOtxu0xC0SLdQ2WRXg8Qgd8T32ak4SPqk5zjItRszrJk2BdeXqfGxBJbP7o4aOvSPSmSSv46Lr1EP4HXU8v7Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.0.13.tgz", + "integrity": "sha512-UCrMJQY/gJnOl3XgbWRZZUvGGBuKy6i0YNSptgMzHBjs+QYDYR1Mt/RLTOPy4fzzves65O1EDmlL//OzEqoLlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ngtools/webpack": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.21.tgz", + "integrity": "sha512-mfLT7lXbyJRlsazuPyuF5AGsMcgzRJRwsDlgxFbiy1DBlaF1chRFsXrKYj1gQ/WXQWNcEd11aedU0Rt+iCNDVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "typescript": ">=5.4 <5.6", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/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/@npmcli/package-json/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/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/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.21.tgz", + "integrity": "sha512-5Ai+NEflQZi67y4NsQ3o04iEp7zT0/BUFVCrJ3CueU3uYQGs8jrN1Lk6tvQ9c5HzGcTDrMXuTrCswyR9o6ecpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "@angular-devkit/schematics": "18.2.21", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", + "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/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/@tufjs/models/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/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz", + "integrity": "sha512-MYCcDkruFc92LeYZux5BC0dmqo2jk+M5UIZ4/oFnAPCXN9mCcQhLyj7F3/Za7rocVyt5YRr1MmqJqFlvQ9LVcg==", + "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/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", + "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "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.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/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/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.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/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "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/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "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/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/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/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "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/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/critters": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", + "integrity": "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==", + "deprecated": "Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "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/cross-spawn/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/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "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/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "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==", + "dev": true, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "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/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.23.0.tgz", + "integrity": "sha512-6jP8UmWy6R6TUUV8bMuC3ZyZ6lZKI56x0tkxyCIqWwRRJ/DgeQKneh/Oid5EoGoPFLrGNkz47ZEtWAYuiY/u9g==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "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-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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/esrecurse/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/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "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-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/globby/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "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/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/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/ignore-walk/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/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", + "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/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/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.0.13.tgz", + "integrity": "sha512-UGe+BbaSUQtAMZobTb4nHvFMrmvuAQKSeaqAX2meTEQjfsbpl5sxdHD8T72OnwD4GU9uwNhYXIVe4QGs8N9Zyw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "msgpackr": "^1.10.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.0.13", + "@lmdb/lmdb-darwin-x64": "3.0.13", + "@lmdb/lmdb-linux-arm": "3.0.13", + "@lmdb/lmdb-linux-arm64": "3.0.13", + "@lmdb/lmdb-linux-x64": "3.0.13", + "@lmdb/lmdb-win32-x64": "3.0.13" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "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/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.51.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", + "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/nice-napi/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/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/node-gyp/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/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/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "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/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ordered-binary": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz", + "integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", + "integrity": "sha512-z30AwWGtQE+Apr+2WBZensP2lIvwoaMcOPkQlIEmSGMJNUvaYACylPYrQM6wSdUNJlnDVMSpLv7xTMJqlVshOA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "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.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "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/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", + "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/sass/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sass/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/sax": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "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/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "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/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "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/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/uri-js/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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/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/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/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/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/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/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", + "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==", + "license": "MIT" + } + } +} diff --git a/frontend-admin/package.json b/frontend-admin/package.json new file mode 100644 index 0000000..81cfbda --- /dev/null +++ b/frontend-admin/package.json @@ -0,0 +1,38 @@ +{ + "name": "parhelion-admin", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^18.2.0", + "@angular/common": "^18.2.0", + "@angular/compiler": "^18.2.0", + "@angular/core": "^18.2.0", + "@angular/forms": "^18.2.0", + "@angular/platform-browser": "^18.2.0", + "@angular/platform-browser-dynamic": "^18.2.0", + "@angular/router": "^18.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.10" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.2.21", + "@angular/cli": "^18.2.21", + "@angular/compiler-cli": "^18.2.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.5.2" + } +} diff --git a/frontend-admin/public/favicon.ico b/frontend-admin/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/frontend-admin/src/app/app.component.html b/frontend-admin/src/app/app.component.html new file mode 100644 index 0000000..36093e1 --- /dev/null +++ b/frontend-admin/src/app/app.component.html @@ -0,0 +1,336 @@ + + + + + + + + + + + +
+
+
+ +

Hello, {{ title }}

+

Congratulations! Your app is running. 🎉

+
+ +
+
+ @for (item of [ + { title: 'Explore the Docs', link: 'https://angular.dev' }, + { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, + { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, + { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, + { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, + ]; track item.title) { + + {{ item.title }} + + + + + } +
+ +
+
+
+ + + + + + + + + + + diff --git a/frontend-admin/src/app/app.component.scss b/frontend-admin/src/app/app.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend-admin/src/app/app.component.ts b/frontend-admin/src/app/app.component.ts new file mode 100644 index 0000000..d1d30a0 --- /dev/null +++ b/frontend-admin/src/app/app.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + templateUrl: './app.component.html', + styleUrl: './app.component.scss' +}) +export class AppComponent { + title = 'parhelion-admin'; +} diff --git a/frontend-admin/src/app/app.config.ts b/frontend-admin/src/app/app.config.ts new file mode 100644 index 0000000..a1e7d6f --- /dev/null +++ b/frontend-admin/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] +}; diff --git a/frontend-admin/src/app/app.routes.ts b/frontend-admin/src/app/app.routes.ts new file mode 100644 index 0000000..dc39edb --- /dev/null +++ b/frontend-admin/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = []; diff --git a/frontend-admin/src/index.html b/frontend-admin/src/index.html new file mode 100644 index 0000000..6b44599 --- /dev/null +++ b/frontend-admin/src/index.html @@ -0,0 +1,13 @@ + + + + + ParhelionAdmin + + + + + + + + diff --git a/frontend-admin/src/main.ts b/frontend-admin/src/main.ts new file mode 100644 index 0000000..35b00f3 --- /dev/null +++ b/frontend-admin/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/frontend-admin/src/styles.scss b/frontend-admin/src/styles.scss new file mode 100644 index 0000000..90d4ee0 --- /dev/null +++ b/frontend-admin/src/styles.scss @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/frontend-admin/tsconfig.app.json b/frontend-admin/tsconfig.app.json new file mode 100644 index 0000000..3775b37 --- /dev/null +++ b/frontend-admin/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/frontend-admin/tsconfig.json b/frontend-admin/tsconfig.json new file mode 100644 index 0000000..a8bb65b --- /dev/null +++ b/frontend-admin/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend-admin/tsconfig.spec.json b/frontend-admin/tsconfig.spec.json new file mode 100644 index 0000000..5fb748d --- /dev/null +++ b/frontend-admin/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend-campo/.gitignore b/frontend-campo/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend-campo/.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-campo/README.md b/frontend-campo/README.md index b416859..d2e7761 100644 --- a/frontend-campo/README.md +++ b/frontend-campo/README.md @@ -1,14 +1,73 @@ -# Frontend Campo - App Chofer +# React + TypeScript + Vite -**Stack:** React (PWA - Progressive Web App) -**Usuario:** Chofer / Operador de Campo -**Dispositivo:** Celular (uso con una mano) +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. -## Funcionalidades +Currently, two official plugins are available: -- Hoja de Ruta con paradas asignadas -- Confirmación de llegada a Hubs -- Escaneo de QR para toma de custodia -- Confirmación de entregas -- Captura de firma digital (POD) -- Reporte de excepciones/incidencias +- [@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-campo/eslint.config.js b/frontend-campo/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend-campo/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-campo/index.html b/frontend-campo/index.html new file mode 100644 index 0000000..e1a369c --- /dev/null +++ b/frontend-campo/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend-campo + + +
+ + + diff --git a/frontend-campo/package-lock.json b/frontend-campo/package-lock.json new file mode 100644 index 0000000..d9b32cf --- /dev/null +++ b/frontend-campo/package-lock.json @@ -0,0 +1,3212 @@ +{ + "name": "frontend-campo", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend-campo", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "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/@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.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz", + "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==", + "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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "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/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/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/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/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-campo/package.json b/frontend-campo/package.json new file mode 100644 index 0000000..040ef87 --- /dev/null +++ b/frontend-campo/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend-campo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/frontend-campo/public/vite.svg b/frontend-campo/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend-campo/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-campo/src/App.css b/frontend-campo/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend-campo/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-campo/src/App.tsx b/frontend-campo/src/App.tsx new file mode 100644 index 0000000..3d7ded3 --- /dev/null +++ b/frontend-campo/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/frontend-campo/src/assets/react.svg b/frontend-campo/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend-campo/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-campo/src/index.css b/frontend-campo/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/frontend-campo/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend-campo/src/main.tsx b/frontend-campo/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend-campo/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-campo/tsconfig.app.json b/frontend-campo/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/frontend-campo/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-campo/tsconfig.json b/frontend-campo/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend-campo/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend-campo/tsconfig.node.json b/frontend-campo/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend-campo/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-campo/vite.config.ts b/frontend-campo/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/frontend-campo/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/frontend-operaciones/.gitignore b/frontend-operaciones/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend-operaciones/.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-operaciones/README.md b/frontend-operaciones/README.md index a454b63..d2e7761 100644 --- a/frontend-operaciones/README.md +++ b/frontend-operaciones/README.md @@ -1,13 +1,73 @@ -# Frontend Operaciones - App Almacenista +# React + TypeScript + Vite -**Stack:** React (PWA - Progressive Web App) -**Usuario:** Almacenista / Operador de Bodega -**Dispositivo:** Tablet +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. -## Funcionalidades +Currently, two official plugins are available: -- Gestión de carga de camiones -- Checklist de estiba (Manifiesto) -- Validación de peso y volumen -- Escaneo de QR para transferencia de custodia -- Vista de envíos pendientes por cargar +- [@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-operaciones/eslint.config.js b/frontend-operaciones/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend-operaciones/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-operaciones/index.html b/frontend-operaciones/index.html new file mode 100644 index 0000000..91adf69 --- /dev/null +++ b/frontend-operaciones/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend-operaciones + + +
+ + + diff --git a/frontend-operaciones/package-lock.json b/frontend-operaciones/package-lock.json new file mode 100644 index 0000000..5ec1963 --- /dev/null +++ b/frontend-operaciones/package-lock.json @@ -0,0 +1,3212 @@ +{ + "name": "frontend-operaciones", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend-operaciones", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "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/@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.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz", + "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==", + "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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "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/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/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/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/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-operaciones/package.json b/frontend-operaciones/package.json new file mode 100644 index 0000000..6e6135c --- /dev/null +++ b/frontend-operaciones/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend-operaciones", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/frontend-operaciones/public/vite.svg b/frontend-operaciones/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend-operaciones/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-operaciones/src/App.css b/frontend-operaciones/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend-operaciones/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-operaciones/src/App.tsx b/frontend-operaciones/src/App.tsx new file mode 100644 index 0000000..3d7ded3 --- /dev/null +++ b/frontend-operaciones/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/frontend-operaciones/src/assets/react.svg b/frontend-operaciones/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend-operaciones/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-operaciones/src/index.css b/frontend-operaciones/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/frontend-operaciones/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend-operaciones/src/main.tsx b/frontend-operaciones/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend-operaciones/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-operaciones/tsconfig.app.json b/frontend-operaciones/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/frontend-operaciones/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-operaciones/tsconfig.json b/frontend-operaciones/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend-operaciones/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend-operaciones/tsconfig.node.json b/frontend-operaciones/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend-operaciones/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-operaciones/vite.config.ts b/frontend-operaciones/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/frontend-operaciones/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From 5b04cbd46cc6f56dfe0f78561a9032415002b9a0 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 11 Dec 2025 23:47:32 +0000 Subject: [PATCH 04/34] =?UTF-8?q?feat:=20agregar=20configuraci=C3=B3n=20Do?= =?UTF-8?q?cker=20y=20Cloudflare=20Tunnel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfiles para backend (.NET 8), admin (Angular), operaciones y campo (React) - nginx.conf para cada frontend con routing SPA - docker-compose.yml con red parhelion-network - Servicios: postgres, api, admin, operaciones, campo, cloudflared - Variables de entorno via .env (no incluido en repo) --- backend/Dockerfile | 44 +++++++++++++ docker-compose.yml | 107 ++++++++++++++++++++++++++++++++ frontend-admin/Dockerfile | 35 +++++++++++ frontend-admin/nginx.conf | 30 +++++++++ frontend-campo/Dockerfile | 35 +++++++++++ frontend-campo/nginx.conf | 30 +++++++++ frontend-operaciones/Dockerfile | 35 +++++++++++ frontend-operaciones/nginx.conf | 30 +++++++++ 8 files changed, 346 insertions(+) create mode 100644 backend/Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend-admin/Dockerfile create mode 100644 frontend-admin/nginx.conf create mode 100644 frontend-campo/Dockerfile create mode 100644 frontend-campo/nginx.conf create mode 100644 frontend-operaciones/Dockerfile create mode 100644 frontend-operaciones/nginx.conf diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3d31afe --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,44 @@ +# =================================== +# PARHELION API - Dockerfile +# Multi-stage build para producción +# =================================== + +# --- STAGE 1: Build --- +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +# Copiar archivos de proyecto primero (cache de dependencias) +COPY src/Parhelion.Domain/*.csproj ./Parhelion.Domain/ +COPY src/Parhelion.Application/*.csproj ./Parhelion.Application/ +COPY src/Parhelion.Infrastructure/*.csproj ./Parhelion.Infrastructure/ +COPY src/Parhelion.API/*.csproj ./Parhelion.API/ + +# Restaurar dependencias +RUN dotnet restore Parhelion.API/Parhelion.API.csproj + +# Copiar todo el código fuente +COPY src/ ./ + +# Build de la aplicación +RUN dotnet publish Parhelion.API/Parhelion.API.csproj -c Release -o /app/publish --no-restore + +# --- STAGE 2: Runtime --- +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app + +# Crear usuario no-root para seguridad +RUN adduser --disabled-password --gecos '' appuser +USER appuser + +# Copiar artefactos del build +COPY --from=build /app/publish . + +# Puerto por defecto +EXPOSE 5000 + +# Variables de entorno +ENV ASPNETCORE_URLS=http://+:5000 +ENV ASPNETCORE_ENVIRONMENT=Production + +# Comando de inicio +ENTRYPOINT ["dotnet", "Parhelion.API.dll"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8117fab --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,107 @@ +# =================================== +# PARHELION LOGISTICS - Docker Compose +# Red unificada: parhelion-network +# =================================== + +services: + # ===== BASE DE DATOS ===== + postgres: + image: postgres:16-alpine + container_name: parhelion-db + restart: unless-stopped + environment: + POSTGRES_DB: parhelion_db + POSTGRES_USER: parhelion_user + POSTGRES_PASSWORD: ${DB_PASSWORD:-parhelion_dev_123} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - parhelion-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U parhelion_user -d parhelion_db"] + interval: 10s + timeout: 5s + retries: 5 + + # ===== BACKEND API ===== + api: + build: + context: ./backend + dockerfile: Dockerfile + container_name: parhelion-api + restart: unless-stopped + ports: + - "5000:5000" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ConnectionStrings__DefaultConnection=Host=postgres;Database=parhelion_db;Username=parhelion_user;Password=${DB_PASSWORD:-parhelion_dev_123} + depends_on: + postgres: + condition: service_healthy + networks: + - parhelion-network + + # ===== FRONTEND ADMIN (Angular) ===== + admin: + build: + context: ./frontend-admin + dockerfile: Dockerfile + container_name: parhelion-admin + restart: unless-stopped + ports: + - "4200:80" + networks: + - parhelion-network + + # ===== FRONTEND OPERACIONES (React PWA) ===== + operaciones: + build: + context: ./frontend-operaciones + dockerfile: Dockerfile + container_name: parhelion-operaciones + restart: unless-stopped + ports: + - "5173:80" + networks: + - parhelion-network + + # ===== FRONTEND CAMPO (React PWA) ===== + campo: + build: + context: ./frontend-campo + dockerfile: Dockerfile + container_name: parhelion-campo + restart: unless-stopped + ports: + - "5174:80" + networks: + - parhelion-network + + # ===== CLOUDFLARE TUNNEL ===== + cloudflared: + image: cloudflare/cloudflared:latest + container_name: parhelion-tunnel + restart: unless-stopped + command: tunnel --no-autoupdate run + environment: + - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} + depends_on: + - api + - admin + - operaciones + - campo + networks: + - parhelion-network + +# ===== VOLUMES ===== +volumes: + postgres_data: + name: parhelion-postgres-data + +# ===== NETWORK ===== +networks: + parhelion-network: + name: parhelion-network + driver: bridge diff --git a/frontend-admin/Dockerfile b/frontend-admin/Dockerfile new file mode 100644 index 0000000..3743f5b --- /dev/null +++ b/frontend-admin/Dockerfile @@ -0,0 +1,35 @@ +# =================================== +# PARHELION ADMIN (Angular) - Dockerfile +# Multi-stage build para producción +# =================================== + +# --- STAGE 1: Build --- +FROM node:20-alpine AS build +WORKDIR /app + +# Copiar archivos de dependencias primero (cache) +COPY package*.json ./ + +# Instalar dependencias +RUN npm ci --silent + +# Copiar código fuente +COPY . . + +# Build de producción +RUN npm run build + +# --- STAGE 2: Nginx --- +FROM nginx:alpine AS runtime + +# Copiar configuración de Nginx +COPY nginx.conf /etc/nginx/nginx.conf + +# Copiar artefactos del build +COPY --from=build /app/dist/parhelion-admin/browser /usr/share/nginx/html + +# Puerto +EXPOSE 80 + +# Comando de inicio +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend-admin/nginx.conf b/frontend-admin/nginx.conf new file mode 100644 index 0000000..d700400 --- /dev/null +++ b/frontend-admin/nginx.conf @@ -0,0 +1,30 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + # SPA routing - redirect all requests to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } +} diff --git a/frontend-campo/Dockerfile b/frontend-campo/Dockerfile new file mode 100644 index 0000000..019cfcf --- /dev/null +++ b/frontend-campo/Dockerfile @@ -0,0 +1,35 @@ +# =================================== +# PARHELION CAMPO (React PWA) - Dockerfile +# Multi-stage build para producción +# =================================== + +# --- STAGE 1: Build --- +FROM node:20-alpine AS build +WORKDIR /app + +# Copiar archivos de dependencias primero (cache) +COPY package*.json ./ + +# Instalar dependencias +RUN npm ci --silent + +# Copiar código fuente +COPY . . + +# Build de producción +RUN npm run build + +# --- STAGE 2: Nginx --- +FROM nginx:alpine AS runtime + +# Copiar configuración de Nginx +COPY nginx.conf /etc/nginx/nginx.conf + +# Copiar artefactos del build (Vite genera en /dist) +COPY --from=build /app/dist /usr/share/nginx/html + +# Puerto +EXPOSE 80 + +# Comando de inicio +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend-campo/nginx.conf b/frontend-campo/nginx.conf new file mode 100644 index 0000000..d700400 --- /dev/null +++ b/frontend-campo/nginx.conf @@ -0,0 +1,30 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + # SPA routing - redirect all requests to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } +} diff --git a/frontend-operaciones/Dockerfile b/frontend-operaciones/Dockerfile new file mode 100644 index 0000000..e3c9fc9 --- /dev/null +++ b/frontend-operaciones/Dockerfile @@ -0,0 +1,35 @@ +# =================================== +# PARHELION OPERACIONES (React PWA) - Dockerfile +# Multi-stage build para producción +# =================================== + +# --- STAGE 1: Build --- +FROM node:20-alpine AS build +WORKDIR /app + +# Copiar archivos de dependencias primero (cache) +COPY package*.json ./ + +# Instalar dependencias +RUN npm ci --silent + +# Copiar código fuente +COPY . . + +# Build de producción +RUN npm run build + +# --- STAGE 2: Nginx --- +FROM nginx:alpine AS runtime + +# Copiar configuración de Nginx +COPY nginx.conf /etc/nginx/nginx.conf + +# Copiar artefactos del build (Vite genera en /dist) +COPY --from=build /app/dist /usr/share/nginx/html + +# Puerto +EXPOSE 80 + +# Comando de inicio +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend-operaciones/nginx.conf b/frontend-operaciones/nginx.conf new file mode 100644 index 0000000..d700400 --- /dev/null +++ b/frontend-operaciones/nginx.conf @@ -0,0 +1,30 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + # SPA routing - redirect all requests to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } +} From de46db22924089ebdbb44393dabb15140bb221fd Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 11 Dec 2025 23:53:47 +0000 Subject: [PATCH 05/34] feat: agregar healthchecks y pipeline CI de GitHub Actions - Healthchecks en docker-compose para todos los servicios - Cloudflared espera que todos los servicios esten healthy - Pipeline CI: build y test de backend, admin, operaciones, campo - Validacion de docker-compose en CI --- .github/workflows/ci.yml | 145 +++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 33 +++++++-- 2 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ab4c702 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,145 @@ +# =================================== +# PARHELION CI - Build & Test Pipeline +# Se ejecuta en cada push/PR a develop y main +# =================================== + +name: CI Pipeline + +on: + push: + branches: [develop, main] + pull_request: + branches: [develop, main] + +jobs: + # ===== BACKEND (.NET 8) ===== + backend: + name: Backend Build & Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./backend + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test --no-build --verbosity normal || true + + # ===== FRONTEND ADMIN (Angular) ===== + frontend-admin: + name: Admin Build & Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend-admin + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: "./frontend-admin/package-lock.json" + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint || true + + - name: Build + run: npm run build + + # ===== FRONTEND OPERACIONES (React) ===== + frontend-operaciones: + name: Operaciones Build & Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend-operaciones + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: "./frontend-operaciones/package-lock.json" + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint || true + + - name: Build + run: npm run build + + # ===== FRONTEND CAMPO (React) ===== + frontend-campo: + name: Campo Build & Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend-campo + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: "./frontend-campo/package-lock.json" + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint || true + + - name: Build + run: npm run build + + # ===== DOCKER BUILD TEST ===== + docker: + name: Docker Compose Validation + runs-on: ubuntu-latest + needs: [backend, frontend-admin, frontend-operaciones, frontend-campo] + + steps: + - uses: actions/checkout@v4 + + - name: Validate docker-compose + run: docker compose config + + - name: Build all images + run: docker compose build + + # ===== RESUMEN FINAL ===== + summary: + name: ✅ All Checks Passed + runs-on: ubuntu-latest + needs: [docker] + if: success() + + steps: + - name: Success + run: echo "🎉 Todos los builds y tests pasaron correctamente!" diff --git a/docker-compose.yml b/docker-compose.yml index 8117fab..c901201 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,12 @@ services: condition: service_healthy networks: - parhelion-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s # ===== FRONTEND ADMIN (Angular) ===== admin: @@ -54,6 +60,11 @@ services: - "4200:80" networks: - parhelion-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 # ===== FRONTEND OPERACIONES (React PWA) ===== operaciones: @@ -66,6 +77,11 @@ services: - "5173:80" networks: - parhelion-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 # ===== FRONTEND CAMPO (React PWA) ===== campo: @@ -78,6 +94,11 @@ services: - "5174:80" networks: - parhelion-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 # ===== CLOUDFLARE TUNNEL ===== cloudflared: @@ -88,10 +109,14 @@ services: environment: - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} depends_on: - - api - - admin - - operaciones - - campo + api: + condition: service_healthy + admin: + condition: service_healthy + operaciones: + condition: service_healthy + campo: + condition: service_healthy networks: - parhelion-network From 68af45753ead0f8115d3b779fc792a8e1e33dbc2 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 11 Dec 2025 23:57:42 +0000 Subject: [PATCH 06/34] docs: agregar CHANGELOG con historial del proyecto --- CHANGELOG.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f76db01 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,69 @@ +# Changelog + +Historial de cambios del proyecto Parhelion Logistics. + +--- + +## [Unreleased] - En desarrollo + +### Pendiente + +- Implementar entidades del Domain (Tenant, User, Shipment, etc.) +- Configurar Entity Framework Core y migraciones +- Crear endpoints de la API +- Diseñar interfaces de los frontends +- Configurar Swagger en el backend + +--- + +## [0.2.0] - 2025-12-11 + +### Agregado + +- **Docker**: Configuración completa de docker-compose con 6 servicios + - PostgreSQL 16 con healthcheck + - Backend API (.NET 8) + - Frontend Admin (Angular 18) + - Frontend Operaciones (React + Vite) + - Frontend Campo (React + Vite) + - Cloudflare Tunnel para exposición pública +- **Healthchecks**: Todos los servicios tienen verificación de salud +- **CI/CD**: Pipeline de GitHub Actions para build y test automático +- **Red Docker**: Todos los servicios en `parhelion-network` + +### Configurado + +- Variables de entorno via `.env` (no versionado) +- Cloudflared espera a que todos los servicios estén healthy + +--- + +## [0.1.0] - 2025-12-11 + +### Agregado + +- **Estructura del proyecto**: 4 carpetas principales + - `backend/`: .NET 8 Web API con Clean Architecture + - `frontend-admin/`: Angular 18 con routing + - `frontend-operaciones/`: React + Vite + TypeScript + - `frontend-campo/`: React + Vite + TypeScript +- **Documentación**: + - `database-schema.md`: Esquema completo de BD + - `requirments.md`: Requerimientos funcionales + - `BRANCHING.md`: Estrategia de ramas Git Flow +- **Git Flow**: Ramas `main` y `develop` configuradas + +### Notas + +- Las 4 feature branches vacías fueron eliminadas +- Solo se crean branches cuando hay trabajo real + +--- + +## Próximos Pasos + +1. Implementar Domain Layer (entidades) +2. Configurar Infrastructure Layer (EF Core) +3. Crear API endpoints básicos +4. Diseñar UI del Admin +5. Probar Docker en local From 4f105cce3d32a457e0d92a455111e6e32cb0befd Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Fri, 12 Dec 2025 16:10:22 +0000 Subject: [PATCH 07/34] feat: agregar Tailwind CSS a frontends React - Instalado tailwindcss y @tailwindcss/vite en operaciones y campo - Configurado plugin en vite.config.ts de ambos proyectos - Agregado import de Tailwind en index.css - Actualizado README con stack correcto (Vite + Tailwind) --- .vscode/settings.json | 4 + README.md | 19 +- frontend-campo/package-lock.json | 607 +++++++++++++++++++++++++ frontend-campo/package.json | 2 + frontend-campo/src/index.css | 2 + frontend-campo/vite.config.ts | 3 +- frontend-operaciones/package-lock.json | 607 +++++++++++++++++++++++++ frontend-operaciones/package.json | 2 + frontend-operaciones/src/index.css | 2 + frontend-operaciones/vite.config.ts | 3 +- 10 files changed, 1240 insertions(+), 11 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..36ae7c9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "git.ignoreLimitWarning": true, + "remote.autoForwardPortsFallback": 0 +} \ No newline at end of file diff --git a/README.md b/README.md index 1059038..9b7b01f 100644 --- a/README.md +++ b/README.md @@ -69,15 +69,16 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in ## Stack Tecnológico -| Capa | Tecnología | Usuario | -| :----------------------- | :------------------------------------ | :------------- | -| **Backend** | C# / .NET 8 Web API | - | -| **Base de Datos** | PostgreSQL 16 | - | -| **ORM** | Entity Framework Core (Code First) | - | -| **Frontend (Admin)** | Angular 18+ (Material Design) | Admin | -| **Frontend (Operación)** | React (PWA) | Chofer/Almacén | -| **Infraestructura** | Docker Compose, Nginx (Reverse Proxy) | - | -| **Hosting** | Digital Ocean Droplet (Linux) | - | +| Capa | Tecnología | Usuario | +| :----------------------- | :------------------------------------ | :---------- | +| **Backend** | C# / .NET 8 Web API | - | +| **Base de Datos** | PostgreSQL 16 | - | +| **ORM** | Entity Framework Core (Code First) | - | +| **Frontend (Admin)** | Angular 18+ (Material Design) | Admin | +| **Frontend (Operacion)** | React + Vite + Tailwind CSS (PWA) | Almacenista | +| **Frontend (Campo)** | React + Vite + Tailwind CSS (PWA) | Chofer | +| **Infraestructura** | Docker Compose, Nginx (Reverse Proxy) | - | +| **Hosting** | Digital Ocean Droplet (Linux) | - | --- diff --git a/frontend-campo/package-lock.json b/frontend-campo/package-lock.json index d9b32cf..ce2bfe4 100644 --- a/frontend-campo/package-lock.json +++ b/frontend-campo/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -21,6 +22,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "tailwindcss": "^4.1.18", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" @@ -1324,6 +1326,278 @@ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "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.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "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.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@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.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1957,6 +2231,16 @@ "dev": true, "license": "MIT" }, + "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/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -1964,6 +2248,20 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -2354,6 +2652,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -2448,6 +2753,16 @@ "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", @@ -2539,6 +2854,267 @@ "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", @@ -2572,6 +3148,16 @@ "yallist": "^3.0.2" } }, + "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/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2935,6 +3521,27 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "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", diff --git a/frontend-campo/package.json b/frontend-campo/package.json index 040ef87..b102728 100644 --- a/frontend-campo/package.json +++ b/frontend-campo/package.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -23,6 +24,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "tailwindcss": "^4.1.18", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" diff --git a/frontend-campo/src/index.css b/frontend-campo/src/index.css index 08a3ac9..444f889 100644 --- a/frontend-campo/src/index.css +++ b/frontend-campo/src/index.css @@ -1,3 +1,5 @@ +@import "tailwindcss"; + :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; diff --git a/frontend-campo/vite.config.ts b/frontend-campo/vite.config.ts index 8b0f57b..c4069b7 100644 --- a/frontend-campo/vite.config.ts +++ b/frontend-campo/vite.config.ts @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], }) diff --git a/frontend-operaciones/package-lock.json b/frontend-operaciones/package-lock.json index 5ec1963..45e1098 100644 --- a/frontend-operaciones/package-lock.json +++ b/frontend-operaciones/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -21,6 +22,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "tailwindcss": "^4.1.18", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" @@ -1324,6 +1326,278 @@ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "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.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "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.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@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.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1957,6 +2231,16 @@ "dev": true, "license": "MIT" }, + "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/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -1964,6 +2248,20 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -2354,6 +2652,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -2448,6 +2753,16 @@ "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", @@ -2539,6 +2854,267 @@ "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", @@ -2572,6 +3148,16 @@ "yallist": "^3.0.2" } }, + "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/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2935,6 +3521,27 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "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", diff --git a/frontend-operaciones/package.json b/frontend-operaciones/package.json index 6e6135c..7eeaffb 100644 --- a/frontend-operaciones/package.json +++ b/frontend-operaciones/package.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -23,6 +24,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "tailwindcss": "^4.1.18", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" diff --git a/frontend-operaciones/src/index.css b/frontend-operaciones/src/index.css index 08a3ac9..444f889 100644 --- a/frontend-operaciones/src/index.css +++ b/frontend-operaciones/src/index.css @@ -1,3 +1,5 @@ +@import "tailwindcss"; + :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; diff --git a/frontend-operaciones/vite.config.ts b/frontend-operaciones/vite.config.ts index 8b0f57b..c4069b7 100644 --- a/frontend-operaciones/vite.config.ts +++ b/frontend-operaciones/vite.config.ts @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], }) From a3549eea605f927c47362deb3a142adbba89689c Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Fri, 12 Dec 2025 20:30:21 +0000 Subject: [PATCH 08/34] =?UTF-8?q?feat:=20implementar=20sistema=20de=20dise?= =?UTF-8?q?=C3=B1o=20Neo-Brutalism=20y=20configuraci=C3=B3n=20dev=20remota?= =?UTF-8?q?=20v0.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + .vscode/settings.json | 3 +- CHANGELOG.md | 29 +- README.md | 25 ++ backend/src/Parhelion.API/Program.cs | 29 +- .../Properties/launchSettings.json | 4 +- frontend-admin/src/app/app.component.html | 336 ------------------ frontend-admin/src/app/app.component.ts | 156 +++++++- frontend-admin/src/styles.scss | 150 +++++++- frontend-campo/src/App.tsx | 73 ++-- .../src/components/AnimatedGrid.tsx | 30 ++ frontend-campo/src/index.css | 196 +++++++--- frontend-campo/vite.config.ts | 4 + frontend-operaciones/src/App.tsx | 73 ++-- .../src/components/AnimatedGrid.tsx | 30 ++ frontend-operaciones/src/index.css | 196 +++++++--- frontend-operaciones/vite.config.ts | 4 + 17 files changed, 827 insertions(+), 514 deletions(-) delete mode 100644 frontend-admin/src/app/app.component.html create mode 100644 frontend-campo/src/components/AnimatedGrid.tsx create mode 100644 frontend-operaciones/src/components/AnimatedGrid.tsx diff --git a/.gitignore b/.gitignore index 7ab753f..a094688 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,6 @@ TestResults/ *.tmp *.temp *.bak + +# ===== Local Development Tools ===== +control-panel/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 36ae7c9..09e184f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "git.ignoreLimitWarning": true, - "remote.autoForwardPortsFallback": 0 + "remote.autoForwardPortsFallback": 0, + "css.validate": false } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f76db01..599971f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,33 @@ Historial de cambios del proyecto Parhelion Logistics. - Implementar entidades del Domain (Tenant, User, Shipment, etc.) - Configurar Entity Framework Core y migraciones - Crear endpoints de la API -- Diseñar interfaces de los frontends -- Configurar Swagger en el backend +- PWA Service Workers para modo offline + +--- + +## [0.3.0] - 2025-12-12 + +### Agregado + +- **Sistema de Diseño Neo-Brutalism**: Estilo visual moderno con bordes sólidos y sombras + - Paleta "Industrial Solar": Oxide (#C85A17), Sand (#E8E6E1), Black (#000000) + - Tipografía: New Rocker (logo), Merriweather (títulos), Inter (body) + - Componentes: Buttons, Cards, Inputs con estilo brutalist +- **Grid Animado**: Fondo con grid cuadriculado naranja y movimiento aleatorio + - Dirección random en cada carga de página + - 8 direcciones posibles (cardinales + diagonales) +- **Remote Development**: Frontends configurados para acceso via Tailscale + - Vite servers escuchando en `0.0.0.0` + - Backend API accesible remotamente + +### Configurado + +- **Puertos dedicados** via `.env`: + - Backend: 5100 + - Admin: 4100 + - Operaciones: 5101 + - Campo: 5102 +- **Endpoint `/health`** en backend API para verificación de estado --- diff --git a/README.md b/README.md index 9b7b01f..5811ebc 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,31 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in --- +## Design System + +El proyecto utiliza un estilo visual **Neo-Brutalism** con la paleta de colores "Industrial Solar": + +| Token | Color | Uso | +| :------ | :-------- | :------------------------------ | +| `oxide` | `#C85A17` | Acciones, acentos, hover states | +| `sand` | `#E8E6E1` | Fondos secundarios | +| `black` | `#000000` | Bordes, texto, sombras | +| `white` | `#FAFAFA` | Fondos principales | + +### Tipografía + +- **Logo:** New Rocker (display font) +- **Títulos:** Merriweather (serif) +- **Body:** Inter (sans-serif) + +### Componentes + +Los frontends incluyen componentes pre-estilizados: `btn`, `btn-primary`, `btn-oxide`, `card`, `input` con sombras brutalist y transiciones sólidas (sin gradientes). + +> UI inspirada en [neobrutalism-components](https://github.com/ekmas/neobrutalism-components) + +--- + ## Arquitectura El proyecto sigue estrictamente **Clean Architecture** para desacoplar la lógica de negocio de la infraestructura: diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index 00ff539..d370456 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -1,10 +1,20 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// CORS para desarrollo +builder.Services.AddCors(options => +{ + options.AddPolicy("DevCors", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -14,8 +24,20 @@ app.UseSwaggerUI(); } -app.UseHttpsRedirection(); +app.UseCors("DevCors"); +// Health check endpoint +app.MapGet("/health", () => new +{ + status = "healthy", + service = "Parhelion API", + timestamp = DateTime.UtcNow, + version = "0.0.2" +}) +.WithName("HealthCheck") +.WithOpenApi(); + +// Weather forecast (sample) var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" @@ -23,7 +45,7 @@ app.MapGet("/weatherforecast", () => { - var forecast = Enumerable.Range(1, 5).Select(index => + var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), @@ -42,3 +64,4 @@ record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } + diff --git a/backend/src/Parhelion.API/Properties/launchSettings.json b/backend/src/Parhelion.API/Properties/launchSettings.json index 793313c..ab9db4a 100644 --- a/backend/src/Parhelion.API/Properties/launchSettings.json +++ b/backend/src/Parhelion.API/Properties/launchSettings.json @@ -12,9 +12,9 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5222", + "applicationUrl": "http://0.0.0.0:5100", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/frontend-admin/src/app/app.component.html b/frontend-admin/src/app/app.component.html deleted file mode 100644 index 36093e1..0000000 --- a/frontend-admin/src/app/app.component.html +++ /dev/null @@ -1,336 +0,0 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - diff --git a/frontend-admin/src/app/app.component.ts b/frontend-admin/src/app/app.component.ts index d1d30a0..8f6ccfb 100644 --- a/frontend-admin/src/app/app.component.ts +++ b/frontend-admin/src/app/app.component.ts @@ -1,13 +1,155 @@ -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet], - templateUrl: './app.component.html', - styleUrl: './app.component.scss' + imports: [], + template: ` +
+ +
+ +
+

Parhelion

+

Logistics

+

Gestión Administrativa

+

En desarrollo

+ + + + + + Ver en GitHub + + +
+ + +
+
+ + +
+ `, + styles: [` + .app { + min-height: 100vh; + background-color: var(--parhelion-sand); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + position: relative; + overflow: hidden; + } + + main { + text-align: center; + max-width: 500px; + } + + h1 { + font-size: 3rem; + margin-bottom: 0.5rem; + } + + .font-logo { + font-family: var(--font-logo); + } + + .logo-subtitle { + font-family: var(--font-logo); + font-size: 2rem; + color: var(--parhelion-oxide); + margin: 0 0 1rem 0; + } + + .subtitle { + font-family: var(--font-heading); + font-size: 1.25rem; + color: #666; + margin: 0 0 1.5rem 0; + } + + .status { + color: #666; + margin-bottom: 2rem; + } + + .btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + } + + .buttons { + display: flex; + gap: 1rem; + justify-content: center; + margin-top: 2rem; + } + + footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 1rem; + text-align: center; + } + + .portfolio { + color: #666; + font-size: 0.875rem; + margin: 0; + } + + .credits { + color: #999; + font-size: 0.75rem; + margin-top: 0.25rem; + } + + .credits a { + color: var(--parhelion-oxide); + text-decoration: none; + } + + .credits a:hover { + text-decoration: underline; + } + `] }) -export class AppComponent { - title = 'parhelion-admin'; +export class AppComponent implements AfterViewInit { + @ViewChild('gridBg') gridBg!: ElementRef; + + ngAfterViewInit() { + const directions = [ + { x: 150, y: 0 }, + { x: -150, y: 0 }, + { x: 0, y: 150 }, + { x: 0, y: -150 }, + { x: 120, y: 120 }, + { x: -120, y: 120 }, + { x: 120, y: -120 }, + { x: -120, y: -120 } + ]; + + const randomDir = directions[Math.floor(Math.random() * directions.length)]; + const randomDuration = 20 + Math.random() * 20; + + const el = this.gridBg.nativeElement; + el.style.setProperty('--grid-x', `${randomDir.x}px`); + el.style.setProperty('--grid-y', `${randomDir.y}px`); + el.style.setProperty('--grid-duration', `${randomDuration}s`); + } } diff --git a/frontend-admin/src/styles.scss b/frontend-admin/src/styles.scss index 90d4ee0..786cfab 100644 --- a/frontend-admin/src/styles.scss +++ b/frontend-admin/src/styles.scss @@ -1 +1,149 @@ -/* You can add global styles to this file, and also import other style files */ +/* ===== PARHELION DESIGN SYSTEM - NEO-BRUTALISM ===== */ + +@import url('https://fonts.googleapis.com/css2?family=New+Rocker&family=Merriweather:wght@700;900&family=Inter:wght@400;500;600;700&display=swap'); + +:root { + /* Color Palette - Industrial Solar */ + --parhelion-oxide: #C85A17; + --parhelion-oxide-dark: #A84810; + --parhelion-white: #FAFAFA; + --parhelion-black: #000000; + --parhelion-sand: #E8E6E1; + --parhelion-gray: #333333; + --parhelion-slate: #2C3E50; + + /* Typography */ + --font-logo: 'New Rocker', cursive; + --font-heading: 'Merriweather', Georgia, serif; + --font-body: 'Inter', system-ui, sans-serif; + + /* Neo-Brutalism */ + --border-width: 2px; + --shadow-offset: 4px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-body); + background-color: var(--parhelion-white); + color: var(--parhelion-black); + -webkit-font-smoothing: antialiased; +} + +/* ===== TYPOGRAPHY ===== */ + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + font-weight: 900; + color: var(--parhelion-black); +} + +/* ===== NEO-BRUTALIST BUTTON ===== */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 24px; + font-family: var(--font-body); + font-weight: 700; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + border: var(--border-width) solid var(--parhelion-black); + cursor: pointer; + transition: transform 0.1s, box-shadow 0.1s, background-color 0s, color 0s; + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--parhelion-black); +} + +.btn:hover { + transform: translate(2px, 2px); + box-shadow: 2px 2px 0 var(--parhelion-black); +} + +.btn:active { + transform: translate(4px, 4px); + box-shadow: 0 0 0 var(--parhelion-black); +} + +.btn-primary { + background-color: var(--parhelion-white); + color: var(--parhelion-black); +} + +.btn-primary:hover { + background-color: var(--parhelion-oxide); + color: var(--parhelion-white); +} + +.btn-oxide { + background-color: var(--parhelion-oxide); + color: var(--parhelion-white); +} + +.btn-oxide:hover { + background-color: var(--parhelion-white); + color: var(--parhelion-black); +} + +/* ===== NEO-BRUTALIST CARD ===== */ + +.card { + background-color: var(--parhelion-white); + border: var(--border-width) solid var(--parhelion-black); + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--parhelion-black); +} + +/* ===== NEO-BRUTALIST INPUT ===== */ + +.input { + width: 100%; + padding: 12px 16px; + font-family: var(--font-body); + font-size: 1rem; + background-color: var(--parhelion-white); + border: var(--border-width) solid var(--parhelion-black); + outline: none; +} + +.input:focus { + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--parhelion-black); +} + +/* ===== ANIMATED GRID BACKGROUND ===== */ +.grid-background { + position: fixed; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + pointer-events: none; + z-index: 0; + opacity: 0.18; + background-image: + linear-gradient(rgba(200, 90, 23, 0.6) 2px, transparent 2px), + linear-gradient(90deg, rgba(200, 90, 23, 0.6) 2px, transparent 2px); + background-size: 40px 40px; + animation: grid-move var(--grid-duration, 30s) linear infinite; + --grid-x: 0px; + --grid-y: 0px; +} + +@keyframes grid-move { + 0% { + transform: translate(0, 0); + } + 100% { + transform: translate(var(--grid-x), var(--grid-y)); + } +} + +.content-layer { + position: relative; + z-index: 1; +} diff --git a/frontend-campo/src/App.tsx b/frontend-campo/src/App.tsx index 3d7ded3..b4eeb73 100644 --- a/frontend-campo/src/App.tsx +++ b/frontend-campo/src/App.tsx @@ -1,34 +1,55 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { AnimatedGrid } from './components/AnimatedGrid' function App() { - const [count, setCount] = useState(0) - return ( - <> -
- - Vite logo - - - React logo + -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR + + {/* Interactive Components Demo */} +

+ + +
+ + + {/* Footer */} +
-

- Click on the Vite and React logos to learn more -

- + +
) } diff --git a/frontend-campo/src/components/AnimatedGrid.tsx b/frontend-campo/src/components/AnimatedGrid.tsx new file mode 100644 index 0000000..3b0cf13 --- /dev/null +++ b/frontend-campo/src/components/AnimatedGrid.tsx @@ -0,0 +1,30 @@ +import { useEffect, useRef } from 'react' + +export function AnimatedGrid() { + const gridRef = useRef(null) + + useEffect(() => { + if (!gridRef.current) return + + // Generate random direction on mount + const directions = [ + { x: 150, y: 0 }, // right + { x: -150, y: 0 }, // left + { x: 0, y: 150 }, // down + { x: 0, y: -150 }, // up + { x: 120, y: 120 }, // diagonal right-down + { x: -120, y: 120 }, // diagonal left-down + { x: 120, y: -120 }, // diagonal right-up + { x: -120, y: -120 } // diagonal left-up + ] + + const randomDir = directions[Math.floor(Math.random() * directions.length)] + const randomDuration = 20 + Math.random() * 20 // 20-40 seconds + + gridRef.current.style.setProperty('--grid-x', `${randomDir.x}px`) + gridRef.current.style.setProperty('--grid-y', `${randomDir.y}px`) + gridRef.current.style.setProperty('--grid-duration', `${randomDuration}s`) + }, []) + + return
+} diff --git a/frontend-campo/src/index.css b/frontend-campo/src/index.css index 444f889..a176a75 100644 --- a/frontend-campo/src/index.css +++ b/frontend-campo/src/index.css @@ -1,70 +1,156 @@ -@import "tailwindcss"; - -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +/* Google Fonts - Must be first */ +@import url('https://fonts.googleapis.com/css2?family=New+Rocker&family=Merriweather:wght@700;900&family=Inter:wght@400;500;600;700&display=swap'); - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; +@import "tailwindcss"; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +/* ===== PARHELION THEME - TAILWIND V4 ===== */ +@theme { + /* Color Palette - Industrial Solar */ + --color-oxide: #C85A17; + --color-oxide-dark: #A84810; + --color-sand: #E8E6E1; + --color-slate-dark: #2C3E50; + + /* Typography */ + --font-logo: 'New Rocker', cursive; + --font-heading: 'Merriweather', Georgia, serif; + --font-body: 'Inter', system-ui, sans-serif; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +/* ===== BASE STYLES ===== */ +@layer base { + body { + @apply font-body antialiased; + } + + h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + @apply font-black; + } } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +/* ===== NEO-BRUTALIST COMPONENTS ===== */ +@layer components { + /* Button Base */ + .btn { + @apply inline-flex items-center justify-center px-6 py-3; + @apply font-bold text-sm uppercase tracking-wide; + @apply border-2 border-black cursor-pointer; + @apply transition-transform duration-100; + box-shadow: 4px 4px 0 theme(colors.black); + } + + .btn:hover { + @apply translate-x-0.5 translate-y-0.5; + box-shadow: 2px 2px 0 theme(colors.black); + } + + .btn:active { + @apply translate-x-1 translate-y-1; + box-shadow: 0 0 0 theme(colors.black); + } + + /* Button Variants */ + .btn-primary { + @apply bg-white text-black; + } + + .btn-primary:hover { + @apply bg-oxide text-white; + } + + .btn-oxide { + @apply bg-oxide text-white; + } + + .btn-oxide:hover { + @apply bg-white text-black; + } + + .btn-black { + @apply bg-black text-white; + } + + .btn-black:hover { + @apply bg-oxide text-white; + } + + /* Card */ + .card { + @apply bg-white border-2 border-black; + box-shadow: 4px 4px 0 theme(colors.black); + } + + .card-sand { + @apply bg-sand; + } + + .card-oxide { + @apply bg-oxide text-white; + } + + /* Input */ + .input { + @apply w-full px-4 py-3 text-base; + @apply bg-white border-2 border-black outline-none; + font-family: var(--font-body); + } + + .input:focus { + box-shadow: 4px 4px 0 theme(colors.black); + } + + /* Shadow Utility */ + .shadow-brutal { + box-shadow: 4px 4px 0 theme(colors.black); + } } -h1 { - font-size: 3.2em; - line-height: 1.1; +/* ===== UTILITIES ===== */ +@layer utilities { + .font-logo { + font-family: var(--font-logo); + } + + .font-heading { + font-family: var(--font-heading); + } + + .font-body { + font-family: var(--font-body); + } } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; +/* ===== ANIMATED GRID BACKGROUND ===== */ +.grid-background { + position: fixed; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + pointer-events: none; + z-index: 0; + opacity: 0.18; + background-image: + linear-gradient(rgba(200, 90, 23, 0.6) 2px, transparent 2px), + linear-gradient(90deg, rgba(200, 90, 23, 0.6) 2px, transparent 2px); + background-size: 40px 40px; + animation: grid-move var(--grid-duration, 30s) linear infinite; + --grid-x: 0px; + --grid-y: 0px; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; +@keyframes grid-move { + 0% { + transform: translate(0, 0); } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; + 100% { + transform: translate(var(--grid-x), var(--grid-y)); } } + +/* Ensure content is above grid */ +.content-layer { + position: relative; + z-index: 1; +} diff --git a/frontend-campo/vite.config.ts b/frontend-campo/vite.config.ts index c4069b7..59b4364 100644 --- a/frontend-campo/vite.config.ts +++ b/frontend-campo/vite.config.ts @@ -5,4 +5,8 @@ import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + server: { + host: '0.0.0.0', + port: 5102 + } }) diff --git a/frontend-operaciones/src/App.tsx b/frontend-operaciones/src/App.tsx index 3d7ded3..e9b1398 100644 --- a/frontend-operaciones/src/App.tsx +++ b/frontend-operaciones/src/App.tsx @@ -1,34 +1,55 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { AnimatedGrid } from './components/AnimatedGrid' function App() { - const [count, setCount] = useState(0) - return ( - <> -
- - Vite logo - - - React logo + -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR + + {/* Interactive Components Demo */} +

+ + +
+ + + {/* Footer */} +
-

- Click on the Vite and React logos to learn more -

- + +
) } diff --git a/frontend-operaciones/src/components/AnimatedGrid.tsx b/frontend-operaciones/src/components/AnimatedGrid.tsx new file mode 100644 index 0000000..3b0cf13 --- /dev/null +++ b/frontend-operaciones/src/components/AnimatedGrid.tsx @@ -0,0 +1,30 @@ +import { useEffect, useRef } from 'react' + +export function AnimatedGrid() { + const gridRef = useRef(null) + + useEffect(() => { + if (!gridRef.current) return + + // Generate random direction on mount + const directions = [ + { x: 150, y: 0 }, // right + { x: -150, y: 0 }, // left + { x: 0, y: 150 }, // down + { x: 0, y: -150 }, // up + { x: 120, y: 120 }, // diagonal right-down + { x: -120, y: 120 }, // diagonal left-down + { x: 120, y: -120 }, // diagonal right-up + { x: -120, y: -120 } // diagonal left-up + ] + + const randomDir = directions[Math.floor(Math.random() * directions.length)] + const randomDuration = 20 + Math.random() * 20 // 20-40 seconds + + gridRef.current.style.setProperty('--grid-x', `${randomDir.x}px`) + gridRef.current.style.setProperty('--grid-y', `${randomDir.y}px`) + gridRef.current.style.setProperty('--grid-duration', `${randomDuration}s`) + }, []) + + return
+} diff --git a/frontend-operaciones/src/index.css b/frontend-operaciones/src/index.css index 444f889..a176a75 100644 --- a/frontend-operaciones/src/index.css +++ b/frontend-operaciones/src/index.css @@ -1,70 +1,156 @@ -@import "tailwindcss"; - -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +/* Google Fonts - Must be first */ +@import url('https://fonts.googleapis.com/css2?family=New+Rocker&family=Merriweather:wght@700;900&family=Inter:wght@400;500;600;700&display=swap'); - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; +@import "tailwindcss"; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +/* ===== PARHELION THEME - TAILWIND V4 ===== */ +@theme { + /* Color Palette - Industrial Solar */ + --color-oxide: #C85A17; + --color-oxide-dark: #A84810; + --color-sand: #E8E6E1; + --color-slate-dark: #2C3E50; + + /* Typography */ + --font-logo: 'New Rocker', cursive; + --font-heading: 'Merriweather', Georgia, serif; + --font-body: 'Inter', system-ui, sans-serif; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +/* ===== BASE STYLES ===== */ +@layer base { + body { + @apply font-body antialiased; + } + + h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + @apply font-black; + } } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +/* ===== NEO-BRUTALIST COMPONENTS ===== */ +@layer components { + /* Button Base */ + .btn { + @apply inline-flex items-center justify-center px-6 py-3; + @apply font-bold text-sm uppercase tracking-wide; + @apply border-2 border-black cursor-pointer; + @apply transition-transform duration-100; + box-shadow: 4px 4px 0 theme(colors.black); + } + + .btn:hover { + @apply translate-x-0.5 translate-y-0.5; + box-shadow: 2px 2px 0 theme(colors.black); + } + + .btn:active { + @apply translate-x-1 translate-y-1; + box-shadow: 0 0 0 theme(colors.black); + } + + /* Button Variants */ + .btn-primary { + @apply bg-white text-black; + } + + .btn-primary:hover { + @apply bg-oxide text-white; + } + + .btn-oxide { + @apply bg-oxide text-white; + } + + .btn-oxide:hover { + @apply bg-white text-black; + } + + .btn-black { + @apply bg-black text-white; + } + + .btn-black:hover { + @apply bg-oxide text-white; + } + + /* Card */ + .card { + @apply bg-white border-2 border-black; + box-shadow: 4px 4px 0 theme(colors.black); + } + + .card-sand { + @apply bg-sand; + } + + .card-oxide { + @apply bg-oxide text-white; + } + + /* Input */ + .input { + @apply w-full px-4 py-3 text-base; + @apply bg-white border-2 border-black outline-none; + font-family: var(--font-body); + } + + .input:focus { + box-shadow: 4px 4px 0 theme(colors.black); + } + + /* Shadow Utility */ + .shadow-brutal { + box-shadow: 4px 4px 0 theme(colors.black); + } } -h1 { - font-size: 3.2em; - line-height: 1.1; +/* ===== UTILITIES ===== */ +@layer utilities { + .font-logo { + font-family: var(--font-logo); + } + + .font-heading { + font-family: var(--font-heading); + } + + .font-body { + font-family: var(--font-body); + } } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; +/* ===== ANIMATED GRID BACKGROUND ===== */ +.grid-background { + position: fixed; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + pointer-events: none; + z-index: 0; + opacity: 0.18; + background-image: + linear-gradient(rgba(200, 90, 23, 0.6) 2px, transparent 2px), + linear-gradient(90deg, rgba(200, 90, 23, 0.6) 2px, transparent 2px); + background-size: 40px 40px; + animation: grid-move var(--grid-duration, 30s) linear infinite; + --grid-x: 0px; + --grid-y: 0px; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; +@keyframes grid-move { + 0% { + transform: translate(0, 0); } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; + 100% { + transform: translate(var(--grid-x), var(--grid-y)); } } + +/* Ensure content is above grid */ +.content-layer { + position: relative; + z-index: 1; +} diff --git a/frontend-operaciones/vite.config.ts b/frontend-operaciones/vite.config.ts index c4069b7..91a1dc9 100644 --- a/frontend-operaciones/vite.config.ts +++ b/frontend-operaciones/vite.config.ts @@ -5,4 +5,8 @@ import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + server: { + host: '0.0.0.0', + port: 5101 + } }) From adccc6253d812e645d2effccb6909db206177e61 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Fri, 12 Dec 2025 20:32:49 +0000 Subject: [PATCH 09/34] fix: regenerar package-lock.json de frontend-admin para CI --- frontend-admin/package-lock.json | 278 +++++++++---------------------- 1 file changed, 75 insertions(+), 203 deletions(-) diff --git a/frontend-admin/package-lock.json b/frontend-admin/package-lock.json index 20bb4a3..ca53f91 100644 --- a/frontend-admin/package-lock.json +++ b/frontend-admin/package-lock.json @@ -607,6 +607,36 @@ "semver": "bin/semver.js" } }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/core": { "version": "18.2.14", "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.14.tgz", @@ -5540,19 +5570,41 @@ "license": "MIT" }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 8.10.0" }, "funding": { "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/chownr": { @@ -8584,31 +8636,6 @@ "source-map-support": "^0.5.5" } }, - "node_modules/karma/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -8628,19 +8655,6 @@ "dev": true, "license": "MIT" }, - "node_modules/karma/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/karma/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -8651,32 +8665,6 @@ "node": ">=8" } }, - "node_modules/karma/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/karma/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/karma/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11062,17 +11050,29 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">= 14.18.0" + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/reflect-metadata": { @@ -11500,70 +11500,6 @@ } } }, - "node_modules/sass/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/sass/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/sass/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/sax": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", @@ -13880,44 +13816,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/webpack-dev-server/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", @@ -13943,32 +13841,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/webpack-dev-server/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/webpack-dev-server/node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", From 1ad8d738c32bb5d80d48bd218128fe36652210f6 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sat, 13 Dec 2025 05:03:27 +0000 Subject: [PATCH 10/34] =?UTF-8?q?feat(auth):=20Sistema=20de=20autenticaci?= =?UTF-8?q?=C3=B3n=20JWT=20v0.4.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Autenticación: - AuthController con endpoints: login, refresh, logout, me - JWT access tokens (2h) + refresh tokens (7 días) - RolePermissions con 60+ permisos inmutables por rol - JwtService y PasswordHasher (BCrypt) Cambios en Base de Datos: - Nueva tabla Clients (remitentes/destinatarios con datos fiscales) - Nueva tabla RefreshTokens (almacenamiento seguro de tokens) - Campos legales en Drivers (RFC, NSS, CURP, licencia, emergencia) - Campos legales en Trucks (VIN, seguro, verificación, mantenimiento) - Trazabilidad en ShipmentCheckpoints (driver, truck, action) - Shipment con SenderId y RecipientClientId FK Documentación: - CHANGELOG.md actualizado con v0.4.2 - database-schema.md con nuevas entidades y campos - README.md versión 0.4.2 --- BRANCHING.md | 79 - CHANGELOG.md | 106 +- README.md | 39 +- .../Controllers/AuthController.cs | 237 +++ .../src/Parhelion.API/Parhelion.API.csproj | 9 + backend/src/Parhelion.API/Program.cs | 118 +- backend/src/Parhelion.API/appsettings.json | 15 +- .../Parhelion.Application/Auth/IJwtService.cs | 41 + .../Auth/IPasswordHasher.cs | 25 + .../Auth/RolePermissions.cs | 166 ++ .../DTOs/Auth/AuthDtos.cs | 59 + .../Parhelion.Application.csproj | 4 + backend/src/Parhelion.Domain/Class1.cs | 6 - .../src/Parhelion.Domain/Common/BaseEntity.cs | 28 + .../src/Parhelion.Domain/Entities/Client.cs | 69 + .../src/Parhelion.Domain/Entities/Driver.cs | 73 + .../src/Parhelion.Domain/Entities/FleetLog.cs | 31 + .../src/Parhelion.Domain/Entities/Location.cs | 42 + .../Parhelion.Domain/Entities/NetworkLink.cs | 23 + .../Parhelion.Domain/Entities/RefreshToken.cs | 40 + backend/src/Parhelion.Domain/Entities/Role.cs | 16 + .../Entities/RouteBlueprint.cs | 21 + .../Parhelion.Domain/Entities/RouteStep.cs | 21 + .../src/Parhelion.Domain/Entities/Shipment.cs | 81 + .../Entities/ShipmentCheckpoint.cs | 42 + .../Entities/ShipmentDocument.cs | 26 + .../Parhelion.Domain/Entities/ShipmentItem.cs | 45 + .../src/Parhelion.Domain/Entities/Tenant.cs | 26 + .../src/Parhelion.Domain/Entities/Truck.cs | 78 + backend/src/Parhelion.Domain/Entities/User.cs | 38 + .../Enums/CheckpointStatus.cs | 31 + .../Parhelion.Domain/Enums/ClientPriority.cs | 21 + .../Parhelion.Domain/Enums/DocumentType.cs | 22 + .../Parhelion.Domain/Enums/DriverStatus.cs | 16 + .../Parhelion.Domain/Enums/FleetLogReason.cs | 16 + .../Parhelion.Domain/Enums/LocationType.cs | 22 + .../Parhelion.Domain/Enums/NetworkLinkType.cs | 16 + .../Parhelion.Domain/Enums/PackagingType.cs | 12 + .../src/Parhelion.Domain/Enums/Permission.cs | 72 + .../Parhelion.Domain/Enums/RouteStepType.cs | 11 + .../Enums/ShipmentPriority.cs | 11 + .../Parhelion.Domain/Enums/ShipmentStatus.cs | 31 + .../src/Parhelion.Domain/Enums/TruckType.cs | 22 + .../Auth/JwtService.cs | 111 ++ .../Auth/PasswordHasher.cs | 47 + .../src/Parhelion.Infrastructure/Class1.cs | 6 - .../Configurations/ClientConfiguration.cs | 75 + .../Configurations/DriverConfiguration.cs | 49 + .../Configurations/FleetLogConfiguration.cs | 42 + .../Configurations/LocationConfiguration.cs | 37 + .../NetworkLinkConfiguration.cs | 33 + .../RefreshTokenConfiguration.cs | 45 + .../Data/Configurations/RoleConfiguration.cs | 20 + .../RouteBlueprintConfiguration.cs | 26 + .../Configurations/RouteStepConfiguration.cs | 27 + .../ShipmentCheckpointConfiguration.cs | 59 + .../Configurations/ShipmentConfiguration.cs | 101 ++ .../ShipmentDocumentConfiguration.cs | 27 + .../ShipmentItemConfiguration.cs | 48 + .../Configurations/TenantConfiguration.cs | 24 + .../Data/Configurations/TruckConfiguration.cs | 39 + .../Data/Configurations/UserConfiguration.cs | 42 + .../20251213001913_InitialCreate.Designer.cs | 1204 +++++++++++++ .../20251213001913_InitialCreate.cs | 771 +++++++++ ...251213030538_AddAuthAndClients.Designer.cs | 1505 +++++++++++++++++ .../20251213030538_AddAuthAndClients.cs | 466 +++++ .../ParhelionDbContextModelSnapshot.cs | 1502 ++++++++++++++++ .../Data/ParhelionDbContext.cs | 163 ++ .../Parhelion.Infrastructure/Data/SeedData.cs | 195 +++ .../Parhelion.Infrastructure.csproj | 11 + database-schema.md | 229 ++- 71 files changed, 8683 insertions(+), 128 deletions(-) delete mode 100644 BRANCHING.md create mode 100644 backend/src/Parhelion.API/Controllers/AuthController.cs create mode 100644 backend/src/Parhelion.Application/Auth/IJwtService.cs create mode 100644 backend/src/Parhelion.Application/Auth/IPasswordHasher.cs create mode 100644 backend/src/Parhelion.Application/Auth/RolePermissions.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Auth/AuthDtos.cs delete mode 100644 backend/src/Parhelion.Domain/Class1.cs create mode 100644 backend/src/Parhelion.Domain/Common/BaseEntity.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Client.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Driver.cs create mode 100644 backend/src/Parhelion.Domain/Entities/FleetLog.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Location.cs create mode 100644 backend/src/Parhelion.Domain/Entities/NetworkLink.cs create mode 100644 backend/src/Parhelion.Domain/Entities/RefreshToken.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Role.cs create mode 100644 backend/src/Parhelion.Domain/Entities/RouteBlueprint.cs create mode 100644 backend/src/Parhelion.Domain/Entities/RouteStep.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Shipment.cs create mode 100644 backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs create mode 100644 backend/src/Parhelion.Domain/Entities/ShipmentDocument.cs create mode 100644 backend/src/Parhelion.Domain/Entities/ShipmentItem.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Tenant.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Truck.cs create mode 100644 backend/src/Parhelion.Domain/Entities/User.cs create mode 100644 backend/src/Parhelion.Domain/Enums/CheckpointStatus.cs create mode 100644 backend/src/Parhelion.Domain/Enums/ClientPriority.cs create mode 100644 backend/src/Parhelion.Domain/Enums/DocumentType.cs create mode 100644 backend/src/Parhelion.Domain/Enums/DriverStatus.cs create mode 100644 backend/src/Parhelion.Domain/Enums/FleetLogReason.cs create mode 100644 backend/src/Parhelion.Domain/Enums/LocationType.cs create mode 100644 backend/src/Parhelion.Domain/Enums/NetworkLinkType.cs create mode 100644 backend/src/Parhelion.Domain/Enums/PackagingType.cs create mode 100644 backend/src/Parhelion.Domain/Enums/Permission.cs create mode 100644 backend/src/Parhelion.Domain/Enums/RouteStepType.cs create mode 100644 backend/src/Parhelion.Domain/Enums/ShipmentPriority.cs create mode 100644 backend/src/Parhelion.Domain/Enums/ShipmentStatus.cs create mode 100644 backend/src/Parhelion.Domain/Enums/TruckType.cs create mode 100644 backend/src/Parhelion.Infrastructure/Auth/JwtService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Auth/PasswordHasher.cs delete mode 100644 backend/src/Parhelion.Infrastructure/Class1.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/ClientConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/DriverConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/FleetLogConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/LocationConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/NetworkLinkConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/RefreshTokenConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/RoleConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/RouteBlueprintConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/RouteStepConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentCheckpointConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentDocumentConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentItemConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/TenantConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/TruckConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/UserConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251213001913_InitialCreate.Designer.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251213001913_InitialCreate.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251213030538_AddAuthAndClients.Designer.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251213030538_AddAuthAndClients.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/SeedData.cs diff --git a/BRANCHING.md b/BRANCHING.md deleted file mode 100644 index 1d228f1..0000000 --- a/BRANCHING.md +++ /dev/null @@ -1,79 +0,0 @@ -# Estrategia de Ramas - Git Flow - -## Ramas Principales - -| Rama | Propósito | Protección | -| :-------- | :---------------------- | :---------------------- | -| `main` | Producción estable | ✅ Requiere PR + Review | -| `develop` | Integración de features | ✅ Requiere PR | - -## Ramas de Trabajo - -| Prefijo | Uso | Ejemplo | -| :---------- | :---------------------------- | :---------------------------- | -| `feature/*` | Nuevas funcionalidades | `feature/backend-setup` | -| `bugfix/*` | Correcciones en develop | `bugfix/fix-login-validation` | -| `hotfix/*` | Correcciones urgentes en prod | `hotfix/fix-critical-crash` | -| `release/*` | Preparación de release | `release/v1.0.0` | - -## Feature Branches Actuales - -``` -feature/backend-setup → Configuración inicial .NET 8 + Clean Architecture -feature/frontend-admin → Proyecto Angular 18 + Material -feature/frontend-operaciones → React PWA para Almacenistas (Tablet) -feature/frontend-campo → React PWA para Choferes (Mobile) -``` - -## Flujo de Trabajo - -```mermaid -gitGraph - commit id: "Initial" - branch develop - checkout develop - commit id: "Setup" - branch feature/backend-setup - checkout feature/backend-setup - commit id: "Add .NET project" - commit id: "Add entities" - checkout develop - merge feature/backend-setup - branch feature/frontend-admin - checkout feature/frontend-admin - commit id: "Add Angular project" - checkout develop - merge feature/frontend-admin - checkout main - merge develop tag: "v1.0.0" -``` - -## Reglas - -1. **Nunca** hacer push directo a `main` -2. **Siempre** crear PR de `feature/*` → `develop` -3. **Solo** hacer merge a `main` desde `develop` via Release -4. Los **commits** deben seguir Conventional Commits: - - `feat:` Nueva funcionalidad - - `fix:` Corrección de bug - - `docs:` Documentación - - `refactor:` Refactorización sin cambio funcional - - `test:` Agregar/modificar tests - - `chore:` Tareas de mantenimiento - -## Comandos Útiles - -```bash -# Crear nueva feature -git checkout develop -git pull origin develop -git checkout -b feature/nombre-feature - -# Terminar feature (crear PR en GitHub) -git push -u origin feature/nombre-feature -# → Crear Pull Request en GitHub: feature/* → develop - -# Sincronizar develop local -git checkout develop -git pull origin develop -``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 599971f..3328692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,99 @@ Historial de cambios del proyecto Parhelion Logistics. ### Pendiente -- Implementar entidades del Domain (Tenant, User, Shipment, etc.) -- Configurar Entity Framework Core y migraciones -- Crear endpoints de la API - PWA Service Workers para modo offline +- Endpoints CRUD para todas las entidades + +--- + +## [0.4.2] - 2025-12-13 + +### Agregado + +- **Sistema de Autenticación JWT**: + + - `AuthController`: `/login`, `/refresh`, `/logout`, `/me` + - Access token (2h) + Refresh token (7 días) + - Revocación de tokens, tracking de IP + +- **Autorización por Roles (Inmutable)**: + + - `RolePermissions.cs` con 60+ permisos en código + - Roles: Admin, Driver, Warehouse, DemoUser + - Permisos NO modificables en runtime + +- **Nueva Tabla `Clients`** (Remitentes/Destinatarios): + + - Datos: CompanyName, ContactName, Email, Phone + - Fiscales: TaxId (RFC), LegalName, BillingAddress + - Prioridad: Normal, Low, High, Urgent + - FK en Shipment: SenderId, RecipientClientId + +- **Nueva Tabla `RefreshTokens`**: + + - Token hasheado, expiración, revocación, IP, UserAgent + +- **Campos Legales en `Drivers`**: + + - RFC, NSS, CURP, LicenseType, LicenseExpiration + - EmergencyContact, EmergencyPhone, HireDate + +- **Campos Legales en `Trucks`**: + + - VIN, EngineNumber, Year, Color, seguro, verificación + +- **Trazabilidad en `ShipmentCheckpoints`**: + - HandledByDriverId, LoadedOntoTruckId, ActionType + +### Migración + +- `AddAuthAndClients` aplicada a PostgreSQL + +--- + +## [0.4.0] - 2025-12-12 + +### Agregado + +- **Domain Layer Completo**: 14 entidades según `database-schema.md` + - Core: Tenant, User, Role + - Flotilla: Driver, Truck, FleetLog + - Red Logística: Location, NetworkLink, RouteBlueprint, RouteStep + - Envíos: Shipment, ShipmentItem, ShipmentCheckpoint, ShipmentDocument +- **11 Enums**: ShipmentStatus, TruckType, LocationType, CheckpointStatus, etc. +- **Infrastructure Layer con EF Core**: + - DbContext con Query Filters globales (multi-tenancy + soft delete) + - 14 configuraciones Fluent API con índices y constraints + - Audit Trail automático (CreatedAt, UpdatedAt, DeletedAt) +- **Migración Inicial**: `InitialCreate` aplicada a PostgreSQL +- **Seed Data**: Roles del sistema (Admin, Driver, Warehouse, DemoUser) +- **Endpoint `/health/db`**: Verificación de estado de base de datos + +### Metodología de Implementación + +| Aspecto | Implementación | +| --------------------- | ----------------------------------------------- | +| **Approach** | Code First con Entity Framework Core 8.0.10 | +| **Database** | PostgreSQL 17 (Docker) | +| **Naming Convention** | PascalCase en C#, preservado en PostgreSQL | +| **Architecture** | Clean Architecture + Domain-Driven Design (DDD) | +| **Multi-Tenancy** | Query Filters globales por TenantId | +| **Soft Delete** | IsDeleted flag en todas las entidades | +| **Audit Trail** | CreatedAt, UpdatedAt, DeletedAt automáticos | + +### Seguridad + +- **Anti SQL Injection**: Queries parameterizadas automáticas de EF Core +- **Tenant Isolation**: Query Filters globales por TenantId +- **Soft Delete**: Todas las entidades soportan borrado lógico +- **Password Strategy**: BCrypt (usuarios) + Argon2id (admins) + +### Configurado + +- **Connection Strings**: Separación develop/production +- **Paquetes NuGet**: + - Npgsql.EntityFrameworkCore.PostgreSQL 8.0.10 + - Microsoft.EntityFrameworkCore.Design 8.0.10 --- @@ -87,8 +176,9 @@ Historial de cambios del proyecto Parhelion Logistics. ## Próximos Pasos -1. Implementar Domain Layer (entidades) -2. Configurar Infrastructure Layer (EF Core) -3. Crear API endpoints básicos -4. Diseñar UI del Admin -5. Probar Docker en local +1. ~~Implementar Domain Layer (entidades)~~ ✅ +2. ~~Configurar Infrastructure Layer (EF Core)~~ ✅ +3. Crear API endpoints básicos (CRUD) +4. Implementar autenticación JWT +5. Diseñar UI del Admin +6. Probar Docker con PostgreSQL diff --git a/README.md b/README.md index 5811ebc..fe18b5d 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,13 @@ ![Angular](https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white) ![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) +![EF Core](https://img.shields.io/badge/EF%20Core-512BD4?style=for-the-badge&logo=dotnet&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white) ![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge) Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. -> **Estado del Proyecto:** Diseño Finalizado (v2.3) - Listo para Implementación +> **Estado:** Development Preview v0.4.2 - Database Layer Implementado ✅ --- @@ -28,8 +29,10 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in ### Core - [x] Documentación de requerimientos y esquema de base de datos -- [ ] **Arquitectura Base:** Configuración de Clean Architecture y estructura de proyecto -- [ ] **Multi-tenancy:** Aislamiento de datos por cliente/empresa +- [x] **Arquitectura Base:** Configuración de Clean Architecture y estructura de proyecto +- [x] **Multi-tenancy:** Query Filters globales por TenantId +- [x] **Domain Layer:** 14 entidades + 11 enumeraciones +- [x] **Infrastructure Layer:** EF Core + PostgreSQL + Migrations ### Gestión de Flotilla @@ -151,6 +154,36 @@ graph TD --- +## Base de Datos + +### Tecnologías + +| Componente | Tecnología | Versión | +| ---------- | ------------------------------------- | ----------- | +| ORM | Entity Framework Core | 8.0.10 | +| Provider | Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.10 | +| Database | PostgreSQL | 17 (Docker) | +| Migrations | Code First | ✅ | + +### Características de Seguridad + +- **Anti SQL Injection:** Queries parameterizadas automáticas de EF Core +- **Multi-Tenancy:** Query Filters globales por TenantId +- **Soft Delete:** Todas las entidades soportan borrado lógico +- **Audit Trail:** CreatedAt, UpdatedAt, DeletedAt automáticos +- **Password Hashing:** BCrypt (usuarios) + Argon2id (admins) + +### Naming Convention + +``` +PascalCase en C# → PascalCase en PostgreSQL +Ejemplo: ShipmentItem.TenantId → "ShipmentItems"."TenantId" +``` + +Para más detalles técnicos, ver [Sección 12 de database-schema.md](./database-schema.md#12-metodología-de-implementación-detalles-técnicos) + +--- + ## Estructura del Proyecto ``` diff --git a/backend/src/Parhelion.API/Controllers/AuthController.cs b/backend/src/Parhelion.API/Controllers/AuthController.cs new file mode 100644 index 0000000..6de30f5 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/AuthController.cs @@ -0,0 +1,237 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.Auth; +using Parhelion.Application.DTOs.Auth; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador de autenticación. +/// Maneja login, refresh de tokens y logout. +/// +[ApiController] +[Route("api/auth")] +public class AuthController : ControllerBase +{ + private readonly ParhelionDbContext _context; + private readonly IJwtService _jwtService; + private readonly IPasswordHasher _passwordHasher; + + public AuthController( + ParhelionDbContext context, + IJwtService jwtService, + IPasswordHasher passwordHasher) + { + _context = context; + _jwtService = jwtService; + _passwordHasher = passwordHasher; + } + + /// + /// Login de usuario con email y password. + /// + [HttpPost("login")] + [AllowAnonymous] + public async Task> Login([FromBody] LoginRequest request) + { + // Buscar usuario por email (ignorar filtro de tenant para login) + var user = await _context.Users + .IgnoreQueryFilters() + .Include(u => u.Role) + .Include(u => u.Tenant) + .FirstOrDefaultAsync(u => u.Email == request.Email && !u.IsDeleted); + + if (user == null) + { + return Unauthorized(new { error = "Email o contraseña incorrectos" }); + } + + if (!user.IsActive) + { + return Unauthorized(new { error = "Usuario inactivo" }); + } + + // Verificar password + if (!_passwordHasher.VerifyPassword(request.Password, user.PasswordHash, user.UsesArgon2)) + { + return Unauthorized(new { error = "Email o contraseña incorrectos" }); + } + + // Actualizar último login + user.LastLogin = DateTime.UtcNow; + + // Generar tokens + var accessToken = _jwtService.GenerateAccessToken(user, user.Role.Name); + var refreshToken = _jwtService.GenerateRefreshToken(); + + // Guardar refresh token hasheado + var refreshTokenEntity = new RefreshToken + { + UserId = user.Id, + TokenHash = HashToken(refreshToken), + ExpiresAt = _jwtService.GetRefreshTokenExpiration(), + CreatedFromIp = GetClientIp(), + UserAgent = Request.Headers.UserAgent.ToString() + }; + + _context.RefreshTokens.Add(refreshTokenEntity); + await _context.SaveChangesAsync(); + + return Ok(new LoginResponse + { + AccessToken = accessToken, + RefreshToken = refreshToken, + ExpiresAt = _jwtService.GetAccessTokenExpiration(), + User = new UserInfo + { + Id = user.Id, + Email = user.Email, + FullName = user.FullName, + Role = user.Role.Name, + TenantId = user.TenantId + } + }); + } + + /// + /// Renovar access token usando refresh token. + /// + [HttpPost("refresh")] + [AllowAnonymous] + public async Task> Refresh([FromBody] RefreshTokenRequest request) + { + var tokenHash = HashToken(request.RefreshToken); + + var refreshToken = await _context.RefreshTokens + .Include(rt => rt.User) + .ThenInclude(u => u.Role) + .FirstOrDefaultAsync(rt => + rt.TokenHash == tokenHash && + !rt.IsRevoked && + rt.ExpiresAt > DateTime.UtcNow); + + if (refreshToken == null) + { + return Unauthorized(new { error = "Refresh token inválido o expirado" }); + } + + var user = refreshToken.User; + if (!user.IsActive || user.IsDeleted) + { + return Unauthorized(new { error = "Usuario inactivo" }); + } + + // Revocar token actual + refreshToken.IsRevoked = true; + refreshToken.RevokedAt = DateTime.UtcNow; + refreshToken.RevokedReason = "Replaced"; + + // Generar nuevos tokens + var newAccessToken = _jwtService.GenerateAccessToken(user, user.Role.Name); + var newRefreshToken = _jwtService.GenerateRefreshToken(); + + // Guardar nuevo refresh token + var newRefreshTokenEntity = new RefreshToken + { + UserId = user.Id, + TokenHash = HashToken(newRefreshToken), + ExpiresAt = _jwtService.GetRefreshTokenExpiration(), + CreatedFromIp = GetClientIp(), + UserAgent = Request.Headers.UserAgent.ToString() + }; + + _context.RefreshTokens.Add(newRefreshTokenEntity); + user.LastLogin = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return Ok(new LoginResponse + { + AccessToken = newAccessToken, + RefreshToken = newRefreshToken, + ExpiresAt = _jwtService.GetAccessTokenExpiration(), + User = new UserInfo + { + Id = user.Id, + Email = user.Email, + FullName = user.FullName, + Role = user.Role.Name, + TenantId = user.TenantId + } + }); + } + + /// + /// Logout - revoca el refresh token. + /// + [HttpPost("logout")] + [Authorize] + public async Task Logout([FromBody] RefreshTokenRequest request) + { + var tokenHash = HashToken(request.RefreshToken); + + var refreshToken = await _context.RefreshTokens + .FirstOrDefaultAsync(rt => rt.TokenHash == tokenHash && !rt.IsRevoked); + + if (refreshToken != null) + { + refreshToken.IsRevoked = true; + refreshToken.RevokedAt = DateTime.UtcNow; + refreshToken.RevokedReason = "Logout"; + await _context.SaveChangesAsync(); + } + + return Ok(new { message = "Sesión cerrada correctamente" }); + } + + /// + /// Obtener información del usuario actual. + /// + [HttpGet("me")] + [Authorize] + public async Task> GetCurrentUser() + { + var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); + if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId)) + { + return Unauthorized(); + } + + var user = await _context.Users + .IgnoreQueryFilters() + .Include(u => u.Role) + .FirstOrDefaultAsync(u => u.Id == userId && !u.IsDeleted); + + if (user == null) + { + return NotFound(); + } + + return Ok(new UserInfo + { + Id = user.Id, + Email = user.Email, + FullName = user.FullName, + Role = user.Role.Name, + TenantId = user.TenantId + }); + } + + // ========== Private Helpers ========== + + private static string HashToken(string token) + { + using var sha256 = SHA256.Create(); + var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token)); + return Convert.ToBase64String(bytes); + } + + private string GetClientIp() + { + return HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + } +} diff --git a/backend/src/Parhelion.API/Parhelion.API.csproj b/backend/src/Parhelion.API/Parhelion.API.csproj index 0902e17..df5f7d2 100644 --- a/backend/src/Parhelion.API/Parhelion.API.csproj +++ b/backend/src/Parhelion.API/Parhelion.API.csproj @@ -8,7 +8,16 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index d370456..b7d0d22 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -1,9 +1,61 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Parhelion.Application.Auth; +using Parhelion.Infrastructure.Auth; +using Parhelion.Infrastructure.Data; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// ========== DATABASE ========== +// Usar connection string de variables de entorno o appsettings +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + +builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString, npgsqlOptions => + { + npgsqlOptions.MigrationsAssembly("Parhelion.Infrastructure"); + npgsqlOptions.EnableRetryOnFailure(3); + })); + +// ========== AUTH SERVICES ========== +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// ========== JWT AUTHENTICATION ========== +var jwtSecretKey = builder.Configuration["Jwt:SecretKey"] + ?? "ParhelionLogisticsDefaultSecretKey2024!"; + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"] ?? "Parhelion", + ValidAudience = builder.Configuration["Jwt:Audience"] ?? "ParhelionClient", + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtSecretKey)), + ClockSkew = TimeSpan.Zero + }; +}); + +builder.Services.AddAuthorization(); + // CORS para desarrollo builder.Services.AddCors(options => { @@ -17,6 +69,21 @@ var app = builder.Build(); +// ========== DATABASE MIGRATION & SEED ========== +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + + // Auto-migrate en desarrollo + if (app.Environment.IsDevelopment()) + { + await db.Database.MigrateAsync(); + } + + // Seed data siempre (es idempotente) + await SeedData.InitializeAsync(db); +} + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -26,42 +93,47 @@ app.UseCors("DevCors"); +// ========== AUTHENTICATION & AUTHORIZATION ========== +app.UseAuthentication(); +app.UseAuthorization(); + +// ========== CONTROLLERS ========== +app.MapControllers(); + // Health check endpoint app.MapGet("/health", () => new { status = "healthy", service = "Parhelion API", timestamp = DateTime.UtcNow, - version = "0.0.2" + version = "0.4.2", + database = "PostgreSQL" }) .WithName("HealthCheck") .WithOpenApi(); -// Weather forecast (sample) -var summaries = new[] +// Database status endpoint +app.MapGet("/health/db", async (ParhelionDbContext db) => { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; + try + { + var canConnect = await db.Database.CanConnectAsync(); + var pendingMigrations = await db.Database.GetPendingMigrationsAsync(); + + return Results.Ok(new + { + status = canConnect ? "connected" : "disconnected", + pendingMigrations = pendingMigrations.Count(), + timestamp = DateTime.UtcNow + }); + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } }) -.WithName("GetWeatherForecast") +.WithName("DatabaseHealthCheck") .WithOpenApi(); app.Run(); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} - diff --git a/backend/src/Parhelion.API/appsettings.json b/backend/src/Parhelion.API/appsettings.json index 10f68b8..e926034 100644 --- a/backend/src/Parhelion.API/appsettings.json +++ b/backend/src/Parhelion.API/appsettings.json @@ -2,8 +2,19 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=parhelion_dev;Username=MetaCodeX;Password=H4NZC0D3X1521" + }, + "Jwt": { + "SecretKey": "ParhelionLogistics2024SecureJwtKeyMinimum32Characters!", + "Issuer": "Parhelion", + "Audience": "ParhelionClient", + "AccessTokenExpirationMinutes": 120, + "RefreshTokenExpirationDays": 7 + } } diff --git a/backend/src/Parhelion.Application/Auth/IJwtService.cs b/backend/src/Parhelion.Application/Auth/IJwtService.cs new file mode 100644 index 0000000..568da9a --- /dev/null +++ b/backend/src/Parhelion.Application/Auth/IJwtService.cs @@ -0,0 +1,41 @@ +using Parhelion.Domain.Entities; +using System.Security.Claims; + +namespace Parhelion.Application.Auth; + +/// +/// Servicio para generación y validación de JWT tokens. +/// +public interface IJwtService +{ + /// + /// Genera un token JWT para un usuario autenticado. + /// + /// Usuario autenticado + /// Nombre del rol del usuario + /// Token JWT como string + string GenerateAccessToken(User user, string roleName); + + /// + /// Genera un refresh token para renovar el access token. + /// + /// Refresh token como string + string GenerateRefreshToken(); + + /// + /// Valida un token JWT y extrae los claims. + /// + /// Token a validar + /// ClaimsPrincipal si es válido, null si no + ClaimsPrincipal? ValidateAccessToken(string token); + + /// + /// Obtiene la fecha de expiración del access token. + /// + DateTime GetAccessTokenExpiration(); + + /// + /// Obtiene la fecha de expiración del refresh token. + /// + DateTime GetRefreshTokenExpiration(); +} diff --git a/backend/src/Parhelion.Application/Auth/IPasswordHasher.cs b/backend/src/Parhelion.Application/Auth/IPasswordHasher.cs new file mode 100644 index 0000000..d15620e --- /dev/null +++ b/backend/src/Parhelion.Application/Auth/IPasswordHasher.cs @@ -0,0 +1,25 @@ +namespace Parhelion.Application.Auth; + +/// +/// Servicio para hashear y verificar passwords. +/// Usa BCrypt para usuarios normales y Argon2id para admin. +/// +public interface IPasswordHasher +{ + /// + /// Genera un hash del password. + /// + /// Password en texto plano + /// True para usar Argon2id (admin), false para BCrypt + /// Hash del password + string HashPassword(string password, bool useArgon2 = false); + + /// + /// Verifica si un password coincide con su hash. + /// + /// Password en texto plano + /// Hash almacenado + /// True si el hash es Argon2id + /// True si coincide + bool VerifyPassword(string password, string passwordHash, bool usesArgon2 = false); +} diff --git a/backend/src/Parhelion.Application/Auth/RolePermissions.cs b/backend/src/Parhelion.Application/Auth/RolePermissions.cs new file mode 100644 index 0000000..7dec01b --- /dev/null +++ b/backend/src/Parhelion.Application/Auth/RolePermissions.cs @@ -0,0 +1,166 @@ +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Auth; + +/// +/// Definición INMUTABLE de permisos por rol. +/// +/// IMPORTANTE: Estos permisos están definidos en código. +/// Ni siquiera el Admin puede modificarlos en runtime. +/// Solo pueden cambiarse modificando el código fuente. +/// +/// Esto garantiza que los permisos son consistentes y seguros +/// independientemente de lo que haya en la base de datos. +/// +public static class RolePermissions +{ + /// + /// Permisos asignados a cada rol del sistema. + /// + private static readonly Dictionary> _permissions = new() + { + // ========== ADMIN ========== + // Acceso total excepto modificar permisos de roles + ["Admin"] = new HashSet + { + // Usuarios + Permission.UsersRead, + Permission.UsersCreate, + Permission.UsersUpdate, + Permission.UsersDelete, + + // Camiones + Permission.TrucksRead, + Permission.TrucksCreate, + Permission.TrucksUpdate, + Permission.TrucksDelete, + + // Choferes + Permission.DriversRead, + Permission.DriversCreate, + Permission.DriversUpdate, + Permission.DriversDelete, + + // Clientes + Permission.ClientsRead, + Permission.ClientsCreate, + Permission.ClientsUpdate, + Permission.ClientsDelete, + + // Envíos + Permission.ShipmentsRead, + Permission.ShipmentsCreate, + Permission.ShipmentsUpdate, + Permission.ShipmentsDelete, + Permission.ShipmentsAssign, + + // Items + Permission.ShipmentItemsRead, + Permission.ShipmentItemsCreate, + Permission.ShipmentItemsUpdate, + + // Checkpoints + Permission.CheckpointsRead, + Permission.CheckpointsCreate, + + // Rutas + Permission.RoutesRead, + Permission.RoutesCreate, + Permission.RoutesUpdate, + Permission.RoutesDelete, + + // Ubicaciones + Permission.LocationsRead, + Permission.LocationsCreate, + Permission.LocationsUpdate, + Permission.LocationsDelete, + + // Documentos + Permission.DocumentsRead, + Permission.DocumentsCreate, + + // Fleet Logs + Permission.FleetLogsRead, + Permission.FleetLogsCreate + }, + + // ========== DRIVER (Chofer) ========== + // Solo ve sus envíos asignados, puede crear checkpoints + ["Driver"] = new HashSet + { + Permission.ShipmentsReadOwn, + Permission.CheckpointsCreate, + Permission.DocumentsReadOwn, + Permission.RoutesRead, + Permission.LocationsRead + }, + + // ========== WAREHOUSE (Almacenista) ========== + // Ve envíos de su ubicación, gestiona items y carga + ["Warehouse"] = new HashSet + { + Permission.ShipmentsReadByLocation, + Permission.ShipmentsRead, + Permission.ShipmentItemsRead, + Permission.ShipmentItemsUpdate, + Permission.CheckpointsCreate, + Permission.TrucksRead, + Permission.DriversRead, + Permission.LocationsRead + }, + + // ========== DEMOUSER (Reclutador/Demo) ========== + // Solo lectura de datos demo + ["DemoUser"] = new HashSet + { + Permission.UsersRead, + Permission.TrucksRead, + Permission.DriversRead, + Permission.ClientsRead, + Permission.ShipmentsRead, + Permission.ShipmentItemsRead, + Permission.CheckpointsRead, + Permission.RoutesRead, + Permission.LocationsRead, + Permission.DocumentsRead, + Permission.FleetLogsRead + } + }; + + /// + /// Verifica si un rol tiene un permiso específico. + /// + /// Nombre del rol (Admin, Driver, Warehouse, DemoUser) + /// Permiso a verificar + /// True si el rol tiene el permiso + public static bool HasPermission(string roleName, Permission permission) + { + return _permissions.TryGetValue(roleName, out var permissions) + && permissions.Contains(permission); + } + + /// + /// Obtiene todos los permisos de un rol. + /// + /// Nombre del rol + /// Conjunto de permisos (vacío si el rol no existe) + public static IReadOnlySet GetPermissions(string roleName) + { + return _permissions.TryGetValue(roleName, out var permissions) + ? permissions + : new HashSet(); + } + + /// + /// Obtiene todos los roles disponibles en el sistema. + /// + /// Lista de nombres de roles + public static IEnumerable GetAllRoles() => _permissions.Keys; + + /// + /// Verifica si un rol existe. + /// + /// Nombre del rol + /// True si el rol existe + public static bool RoleExists(string roleName) => _permissions.ContainsKey(roleName); +} diff --git a/backend/src/Parhelion.Application/DTOs/Auth/AuthDtos.cs b/backend/src/Parhelion.Application/DTOs/Auth/AuthDtos.cs new file mode 100644 index 0000000..5c75ab4 --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Auth/AuthDtos.cs @@ -0,0 +1,59 @@ +using System.ComponentModel.DataAnnotations; + +namespace Parhelion.Application.DTOs.Auth; + +/// +/// Request para login de usuario. +/// +public record LoginRequest +{ + [Required(ErrorMessage = "El email es requerido")] + [EmailAddress(ErrorMessage = "Email inválido")] + public string Email { get; init; } = null!; + + [Required(ErrorMessage = "La contraseña es requerida")] + [MinLength(8, ErrorMessage = "La contraseña debe tener al menos 8 caracteres")] + public string Password { get; init; } = null!; +} + +/// +/// Response de login exitoso. +/// +public record LoginResponse +{ + /// JWT Access Token + public string AccessToken { get; init; } = null!; + + /// Refresh Token para renovar el access token + public string RefreshToken { get; init; } = null!; + + /// Tipo de token (Bearer) + public string TokenType { get; init; } = "Bearer"; + + /// Fecha de expiración del access token + public DateTime ExpiresAt { get; init; } + + /// Información del usuario autenticado + public UserInfo User { get; init; } = null!; +} + +/// +/// Información básica del usuario en response de login. +/// +public record UserInfo +{ + public Guid Id { get; init; } + public string Email { get; init; } = null!; + public string FullName { get; init; } = null!; + public string Role { get; init; } = null!; + public Guid TenantId { get; init; } +} + +/// +/// Request para renovar el access token. +/// +public record RefreshTokenRequest +{ + [Required(ErrorMessage = "El refresh token es requerido")] + public string RefreshToken { get; init; } = null!; +} diff --git a/backend/src/Parhelion.Application/Parhelion.Application.csproj b/backend/src/Parhelion.Application/Parhelion.Application.csproj index fa71b7a..e3095cc 100644 --- a/backend/src/Parhelion.Application/Parhelion.Application.csproj +++ b/backend/src/Parhelion.Application/Parhelion.Application.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/backend/src/Parhelion.Domain/Class1.cs b/backend/src/Parhelion.Domain/Class1.cs deleted file mode 100644 index 3da44d9..0000000 --- a/backend/src/Parhelion.Domain/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Parhelion.Domain; - -public class Class1 -{ - -} diff --git a/backend/src/Parhelion.Domain/Common/BaseEntity.cs b/backend/src/Parhelion.Domain/Common/BaseEntity.cs new file mode 100644 index 0000000..b31f3fe --- /dev/null +++ b/backend/src/Parhelion.Domain/Common/BaseEntity.cs @@ -0,0 +1,28 @@ +namespace Parhelion.Domain.Common; + +/// +/// Entidad base para todas las entidades del sistema. +/// Incluye Soft Delete y Audit Trail automático. +/// +public abstract class BaseEntity +{ + public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + + /// + /// Soft Delete: true indica que la entidad fue eliminada lógicamente. + /// + public bool IsDeleted { get; set; } + public DateTime? DeletedAt { get; set; } +} + +/// +/// Entidad base para entidades que pertenecen a un tenant específico. +/// Hereda de BaseEntity para incluir Soft Delete y Audit Trail. +/// Todas las queries automáticamente filtran por TenantId via Query Filters. +/// +public abstract class TenantEntity : BaseEntity +{ + public Guid TenantId { get; set; } +} diff --git a/backend/src/Parhelion.Domain/Entities/Client.cs b/backend/src/Parhelion.Domain/Entities/Client.cs new file mode 100644 index 0000000..d4ecb1a --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Client.cs @@ -0,0 +1,69 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Cliente (remitente o destinatario de envíos). +/// Representa a las empresas o personas que envían o reciben paquetes. +/// +public class Client : TenantEntity +{ + // ========== DATOS BÁSICOS ========== + + /// Nombre de la empresa + public string CompanyName { get; set; } = null!; + + /// Nombre comercial (opcional) + public string? TradeName { get; set; } + + /// Nombre del contacto principal + public string ContactName { get; set; } = null!; + + /// Email de contacto + public string Email { get; set; } = null!; + + /// Teléfono de contacto + public string Phone { get; set; } = null!; + + // ========== DATOS FISCALES ========== + + /// RFC para facturación (México) + public string? TaxId { get; set; } + + /// Razón Social para facturación + public string? LegalName { get; set; } + + /// Dirección fiscal para facturación + public string? BillingAddress { get; set; } + + // ========== DATOS DE ENVÍO ========== + + /// Dirección de envío/recepción predeterminada + public string ShippingAddress { get; set; } = null!; + + /// Tipos de productos que suele enviar/recibir (ej: "Electrónicos, Frágiles") + public string? PreferredProductTypes { get; set; } + + /// Prioridad del cliente para atención + public ClientPriority Priority { get; set; } = ClientPriority.Normal; + + // ========== ESTADO ========== + + /// Si el cliente está activo + public bool IsActive { get; set; } = true; + + /// Notas internas sobre el cliente + public string? Notes { get; set; } + + // ========== NAVIGATION PROPERTIES ========== + + /// Tenant al que pertenece este cliente + public Tenant Tenant { get; set; } = null!; + + /// Envíos donde este cliente es el remitente + public ICollection ShipmentsAsSender { get; set; } = new List(); + + /// Envíos donde este cliente es el destinatario + public ICollection ShipmentsAsRecipient { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/Driver.cs b/backend/src/Parhelion.Domain/Entities/Driver.cs new file mode 100644 index 0000000..f29ea95 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Driver.cs @@ -0,0 +1,73 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Chofer de la flotilla con asignación híbrida de camiones. +/// - DefaultTruckId: Camión fijo asignado ("su unidad") +/// - CurrentTruckId: Camión que conduce actualmente (puede diferir) +/// +public class Driver : TenantEntity +{ + public Guid UserId { get; set; } + public string FullName { get; set; } = null!; + public string Phone { get; set; } = null!; + + // ========== DATOS LEGALES ========== + + /// RFC del chofer para nómina + public string? Rfc { get; set; } + + /// Número de Seguro Social (IMSS) + public string? Nss { get; set; } + + /// CURP del chofer + public string? Curp { get; set; } + + /// Número de licencia de conducir + public string LicenseNumber { get; set; } = null!; + + /// Tipo de licencia: A, B, C, D, E (Federal) + public string? LicenseType { get; set; } + + /// Fecha de vencimiento de la licencia + public DateTime? LicenseExpiration { get; set; } + + // ========== CONTACTO DE EMERGENCIA ========== + + /// Nombre del contacto de emergencia + public string? EmergencyContact { get; set; } + + /// Teléfono del contacto de emergencia + public string? EmergencyPhone { get; set; } + + // ========== INFORMACIÓN LABORAL ========== + + /// Fecha de contratación + public DateTime? HireDate { get; set; } + + // ========== ASIGNACIÓN DE CAMIONES ========== + + /// + /// Camión fijo asignado al chofer ("su unidad"). + /// + public Guid? DefaultTruckId { get; set; } + + /// + /// Camión que conduce actualmente (puede diferir del fijo). + /// Es una caché del último registro de FleetLog. + /// + public Guid? CurrentTruckId { get; set; } + + public DriverStatus Status { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public User User { get; set; } = null!; + public Truck? DefaultTruck { get; set; } + public Truck? CurrentTruck { get; set; } + public ICollection Shipments { get; set; } = new List(); + public ICollection FleetHistory { get; set; } = new List(); + public ICollection HandledCheckpoints { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/FleetLog.cs b/backend/src/Parhelion.Domain/Entities/FleetLog.cs new file mode 100644 index 0000000..4169cd7 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/FleetLog.cs @@ -0,0 +1,31 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Bitácora de cambios de vehículo. +/// SOURCE OF TRUTH para la asignación Chofer-Camión. +/// Driver.CurrentTruckId es solo una caché del último registro. +/// +public class FleetLog : TenantEntity +{ + public Guid DriverId { get; set; } + + /// + /// Nullable si el chofer no tenía camión asignado antes. + /// + public Guid? OldTruckId { get; set; } + + public Guid NewTruckId { get; set; } + public FleetLogReason Reason { get; set; } + public DateTime Timestamp { get; set; } + public Guid CreatedByUserId { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public Driver Driver { get; set; } = null!; + public Truck? OldTruck { get; set; } + public Truck NewTruck { get; set; } = null!; + public User CreatedBy { get; set; } = null!; +} diff --git a/backend/src/Parhelion.Domain/Entities/Location.cs b/backend/src/Parhelion.Domain/Entities/Location.cs new file mode 100644 index 0000000..bdaacf9 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Location.cs @@ -0,0 +1,42 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Nodo de la red logística: Almacenes, Hubs, Cross-docks, puntos de venta. +/// Código único estilo aeropuerto (MTY, GDL, MM). +/// +public class Location : TenantEntity +{ + public string Code { get; set; } = null!; + public string Name { get; set; } = null!; + public LocationType Type { get; set; } + public string FullAddress { get; set; } = null!; + + /// + /// Flag: Puede recibir mercancía. + /// + public bool CanReceive { get; set; } + + /// + /// Flag: Puede despachar mercancía. + /// + public bool CanDispatch { get; set; } + + /// + /// True si es ubicación propia, false si es externa (cliente/proveedor). + /// + public bool IsInternal { get; set; } + + public bool IsActive { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public ICollection OriginShipments { get; set; } = new List(); + public ICollection DestinationShipments { get; set; } = new List(); + public ICollection Checkpoints { get; set; } = new List(); + public ICollection RouteSteps { get; set; } = new List(); + public ICollection OutgoingLinks { get; set; } = new List(); + public ICollection IncomingLinks { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/NetworkLink.cs b/backend/src/Parhelion.Domain/Entities/NetworkLink.cs new file mode 100644 index 0000000..0efb674 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/NetworkLink.cs @@ -0,0 +1,23 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Enlace de red logística (Lista de Adyacencia). +/// Define conexiones permitidas entre ubicaciones. +/// +public class NetworkLink : TenantEntity +{ + public Guid OriginLocationId { get; set; } + public Guid DestinationLocationId { get; set; } + public NetworkLinkType LinkType { get; set; } + public TimeSpan TransitTime { get; set; } + public bool IsBidirectional { get; set; } + public bool IsActive { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public Location OriginLocation { get; set; } = null!; + public Location DestinationLocation { get; set; } = null!; +} diff --git a/backend/src/Parhelion.Domain/Entities/RefreshToken.cs b/backend/src/Parhelion.Domain/Entities/RefreshToken.cs new file mode 100644 index 0000000..8a2f796 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/RefreshToken.cs @@ -0,0 +1,40 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Refresh token para renovar tokens JWT sin re-autenticación. +/// Los tokens tienen expiración de 7 días y pueden ser revocados. +/// +public class RefreshToken : BaseEntity +{ + /// Usuario al que pertenece este token + public Guid UserId { get; set; } + + /// Token hasheado (nunca almacenar en texto plano) + public string TokenHash { get; set; } = null!; + + /// Fecha de expiración del token + public DateTime ExpiresAt { get; set; } + + /// Si el token ha sido revocado + public bool IsRevoked { get; set; } + + /// Fecha de revocación (si aplica) + public DateTime? RevokedAt { get; set; } + + /// Razón de revocación + public string? RevokedReason { get; set; } + + /// Dirección IP desde donde se creó + public string? CreatedFromIp { get; set; } + + /// User Agent del dispositivo que creó el token + public string? UserAgent { get; set; } + + // Navigation Properties + public User User { get; set; } = null!; + + /// Verifica si el token está activo (no expirado y no revocado) + public bool IsActive => !IsRevoked && DateTime.UtcNow < ExpiresAt; +} diff --git a/backend/src/Parhelion.Domain/Entities/Role.cs b/backend/src/Parhelion.Domain/Entities/Role.cs new file mode 100644 index 0000000..717d5b0 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Role.cs @@ -0,0 +1,16 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Roles del sistema: Admin, Driver, DemoUser. +/// No son multi-tenant (compartidos globalmente). +/// +public class Role : BaseEntity +{ + public string Name { get; set; } = null!; + public string? Description { get; set; } + + // Navigation Properties + public ICollection Users { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/RouteBlueprint.cs b/backend/src/Parhelion.Domain/Entities/RouteBlueprint.cs new file mode 100644 index 0000000..96af071 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/RouteBlueprint.cs @@ -0,0 +1,21 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Ruta predefinida con secuencia de paradas. +/// Usado para asignar envíos a rutas conocidas. +/// +public class RouteBlueprint : TenantEntity +{ + public string Name { get; set; } = null!; + public string? Description { get; set; } + public int TotalSteps { get; set; } + public TimeSpan TotalTransitTime { get; set; } + public bool IsActive { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public ICollection Steps { get; set; } = new List(); + public ICollection Shipments { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/RouteStep.cs b/backend/src/Parhelion.Domain/Entities/RouteStep.cs new file mode 100644 index 0000000..07673c8 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/RouteStep.cs @@ -0,0 +1,21 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Parada individual en una ruta predefinida. +/// Incluye orden y tiempo de tránsito desde la parada anterior. +/// +public class RouteStep : BaseEntity +{ + public Guid RouteBlueprintId { get; set; } + public Guid LocationId { get; set; } + public int StepOrder { get; set; } + public TimeSpan StandardTransitTime { get; set; } + public RouteStepType StepType { get; set; } + + // Navigation Properties + public RouteBlueprint RouteBlueprint { get; set; } = null!; + public Location Location { get; set; } = null!; +} diff --git a/backend/src/Parhelion.Domain/Entities/Shipment.cs b/backend/src/Parhelion.Domain/Entities/Shipment.cs new file mode 100644 index 0000000..9bfb987 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Shipment.cs @@ -0,0 +1,81 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Envío principal con origen, destino, ruta asignada y trazabilidad. +/// Genera tracking number único con formato PAR-XXXXXX. +/// +public class Shipment : TenantEntity +{ + public string TrackingNumber { get; set; } = null!; + public string QrCodeData { get; set; } = null!; + public Guid OriginLocationId { get; set; } + public Guid DestinationLocationId { get; set; } + + // ========== CLIENTE REMITENTE/DESTINATARIO ========== + + /// Cliente que envía el paquete (opcional, puede ser registro manual) + public Guid? SenderId { get; set; } + + /// Cliente que recibe el paquete (opcional, puede ser registro manual) + public Guid? RecipientClientId { get; set; } + + // Enrutamiento Hub & Spoke + public Guid? AssignedRouteId { get; set; } + public int? CurrentStepOrder { get; set; } + + // Datos de destinatario (para envíos sin cliente registrado) + public string RecipientName { get; set; } = null!; + public string? RecipientPhone { get; set; } + public decimal TotalWeightKg { get; set; } + public decimal TotalVolumeM3 { get; set; } + public decimal? DeclaredValue { get; set; } + + // Campos B2B (Documentación Legal) + public string? SatMerchandiseCode { get; set; } + public string? DeliveryInstructions { get; set; } + public string? RecipientSignatureUrl { get; set; } + + public ShipmentPriority Priority { get; set; } + public ShipmentStatus Status { get; set; } + public Guid? TruckId { get; set; } + public Guid? DriverId { get; set; } + + /// + /// True si la carga se realizó por escaneo QR. + /// + public bool WasQrScanned { get; set; } + + /// + /// True si hay retraso (avería, tráfico). + /// + public bool IsDelayed { get; set; } + + // Fechas y Ventanas + public DateTime? ScheduledDeparture { get; set; } + public DateTime? PickupWindowStart { get; set; } + public DateTime? PickupWindowEnd { get; set; } + public DateTime? EstimatedArrival { get; set; } + public DateTime? AssignedAt { get; set; } + public DateTime? DeliveredAt { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public Location OriginLocation { get; set; } = null!; + public Location DestinationLocation { get; set; } = null!; + public RouteBlueprint? AssignedRoute { get; set; } + public Truck? Truck { get; set; } + public Driver? Driver { get; set; } + + /// Cliente remitente (quien envía) + public Client? Sender { get; set; } + + /// Cliente destinatario (quien recibe) + public Client? RecipientClient { get; set; } + + public ICollection Items { get; set; } = new List(); + public ICollection History { get; set; } = new List(); + public ICollection Documents { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs b/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs new file mode 100644 index 0000000..50462b1 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs @@ -0,0 +1,42 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Evento de trazabilidad del envío. +/// INMUTABLE: Los checkpoints no se modifican, solo se agregan nuevos. +/// +public class ShipmentCheckpoint : BaseEntity +{ + public Guid ShipmentId { get; set; } + public Guid? LocationId { get; set; } + public CheckpointStatus StatusCode { get; set; } + public string? Remarks { get; set; } + public DateTime Timestamp { get; set; } + public Guid CreatedByUserId { get; set; } + + // ========== TRAZABILIDAD DE CARGUEROS ========== + + /// Chofer que manejó el paquete en este checkpoint + public Guid? HandledByDriverId { get; set; } + + /// Camión donde se cargó el paquete + public Guid? LoadedOntoTruckId { get; set; } + + /// Tipo de acción: Loaded, Unloaded, Transferred, Delivered, etc. + public string? ActionType { get; set; } + + /// Nombre del custodio anterior (quien entregó) + public string? PreviousCustodian { get; set; } + + /// Nombre del nuevo custodio (quien recibió) + public string? NewCustodian { get; set; } + + // Navigation Properties + public Shipment Shipment { get; set; } = null!; + public Location? Location { get; set; } + public User CreatedBy { get; set; } = null!; + public Driver? HandledByDriver { get; set; } + public Truck? LoadedOntoTruck { get; set; } +} diff --git a/backend/src/Parhelion.Domain/Entities/ShipmentDocument.cs b/backend/src/Parhelion.Domain/Entities/ShipmentDocument.cs new file mode 100644 index 0000000..d85c02c --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/ShipmentDocument.cs @@ -0,0 +1,26 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Documento B2B asociado a un envío. +/// Tipos: ServiceOrder, Waybill (Carta Porte), Manifest, TripSheet, POD. +/// +public class ShipmentDocument : BaseEntity +{ + public Guid ShipmentId { get; set; } + public DocumentType DocumentType { get; set; } + public string FileUrl { get; set; } = null!; + + /// + /// "System" para documentos automáticos, "User" para uploads manuales. + /// + public string GeneratedBy { get; set; } = null!; + + public DateTime GeneratedAt { get; set; } + public DateTime? ExpiresAt { get; set; } + + // Navigation Properties + public Shipment Shipment { get; set; } = null!; +} diff --git a/backend/src/Parhelion.Domain/Entities/ShipmentItem.cs b/backend/src/Parhelion.Domain/Entities/ShipmentItem.cs new file mode 100644 index 0000000..61d23db --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/ShipmentItem.cs @@ -0,0 +1,45 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Partida individual dentro de un envío (SKU, dimensiones, peso). +/// Incluye cálculo de peso volumétrico para cotizaciones. +/// +public class ShipmentItem : BaseEntity +{ + public Guid ShipmentId { get; set; } + public string? Sku { get; set; } + public string Description { get; set; } = null!; + public PackagingType PackagingType { get; set; } + public int Quantity { get; set; } + public decimal WeightKg { get; set; } + public decimal WidthCm { get; set; } + public decimal HeightCm { get; set; } + public decimal LengthCm { get; set; } + + /// + /// Volumen calculado en metros cúbicos. + /// + public decimal VolumeM3 => (WidthCm * HeightCm * LengthCm) / 1_000_000m; + + /// + /// Peso volumétrico para cotización. + /// Fórmula: (Largo × Ancho × Alto) / 5000 + /// + public decimal VolumetricWeightKg => (WidthCm * HeightCm * LengthCm) / 5000m; + + /// + /// Valor monetario para seguro. + /// + public decimal DeclaredValue { get; set; } + + public bool IsFragile { get; set; } + public bool IsHazardous { get; set; } + public bool RequiresRefrigeration { get; set; } + public string? StackingInstructions { get; set; } + + // Navigation Properties + public Shipment Shipment { get; set; } = null!; +} diff --git a/backend/src/Parhelion.Domain/Entities/Tenant.cs b/backend/src/Parhelion.Domain/Entities/Tenant.cs new file mode 100644 index 0000000..68abca5 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Tenant.cs @@ -0,0 +1,26 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Representa a cada cliente/empresa que usa el sistema. +/// Root de multi-tenancy - aísla todos los datos por cliente. +/// +public class Tenant : BaseEntity +{ + public string CompanyName { get; set; } = null!; + public string ContactEmail { get; set; } = null!; + public int FleetSize { get; set; } + public int DriverCount { get; set; } + public bool IsActive { get; set; } + + // Navigation Properties + public ICollection Users { get; set; } = new List(); + public ICollection Trucks { get; set; } = new List(); + public ICollection Drivers { get; set; } = new List(); + public ICollection Locations { get; set; } = new List(); + public ICollection Shipments { get; set; } = new List(); + public ICollection RouteBlueprints { get; set; } = new List(); + public ICollection NetworkLinks { get; set; } = new List(); + public ICollection FleetLogs { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/Truck.cs b/backend/src/Parhelion.Domain/Entities/Truck.cs new file mode 100644 index 0000000..3dbcd52 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Truck.cs @@ -0,0 +1,78 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Camión de la flotilla con capacidad máxima en kg y volumen en m³. +/// El tipo de camión determina qué mercancía puede transportar. +/// +public class Truck : TenantEntity +{ + // ========== DATOS BÁSICOS ========== + + public string Plate { get; set; } = null!; + public string Model { get; set; } = null!; + public TruckType Type { get; set; } + public decimal MaxCapacityKg { get; set; } + public decimal MaxVolumeM3 { get; set; } + public bool IsActive { get; set; } + + // ========== DATOS DEL VEHÍCULO ========== + + /// Número de Identificación Vehicular (VIN/Serie) + public string? Vin { get; set; } + + /// Número de motor + public string? EngineNumber { get; set; } + + /// Año del vehículo + public int? Year { get; set; } + + /// Color del vehículo + public string? Color { get; set; } + + // ========== DOCUMENTACIÓN LEGAL ========== + + /// Número de póliza de seguro + public string? InsurancePolicy { get; set; } + + /// Fecha de vencimiento del seguro + public DateTime? InsuranceExpiration { get; set; } + + /// Número de verificación vehicular + public string? VerificationNumber { get; set; } + + /// Fecha de vencimiento de verificación + public DateTime? VerificationExpiration { get; set; } + + // ========== MANTENIMIENTO ========== + + /// Fecha del último mantenimiento + public DateTime? LastMaintenanceDate { get; set; } + + /// Próximo mantenimiento programado + public DateTime? NextMaintenanceDate { get; set; } + + /// Odómetro actual en kilómetros + public decimal? CurrentOdometerKm { get; set; } + + // ========== NAVIGATION PROPERTIES ========== + + public Tenant Tenant { get; set; } = null!; + public ICollection Shipments { get; set; } = new List(); + + /// + /// Choferes que tienen este camión como asignación fija. + /// + public ICollection DefaultDrivers { get; set; } = new List(); + + /// + /// Choferes que actualmente conducen este camión. + /// + public ICollection CurrentDrivers { get; set; } = new List(); + + public ICollection OldTruckLogs { get; set; } = new List(); + public ICollection NewTruckLogs { get; set; } = new List(); + public ICollection LoadedCheckpoints { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/User.cs b/backend/src/Parhelion.Domain/Entities/User.cs new file mode 100644 index 0000000..6d9d278 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/User.cs @@ -0,0 +1,38 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Usuario del sistema (Admin, Chofer, Almacenista, Demo). +/// El password se hashea con BCrypt para usuarios normales +/// y Argon2id para credenciales administrativas. +/// +public class User : TenantEntity +{ + public string Email { get; set; } = null!; + public string PasswordHash { get; set; } = null!; + public string FullName { get; set; } = null!; + public Guid RoleId { get; set; } + + /// + /// True si el usuario fue creado para sesión de demo temporal. + /// + public bool IsDemoUser { get; set; } + + /// + /// True si las credenciales usan Argon2id (admin security). + /// False si usan BCrypt (estándar). + /// + public bool UsesArgon2 { get; set; } + + public DateTime? LastLogin { get; set; } + public bool IsActive { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public Role Role { get; set; } = null!; + public Driver? Driver { get; set; } + public ICollection CreatedCheckpoints { get; set; } = new List(); + public ICollection CreatedFleetLogs { get; set; } = new List(); + public ICollection RefreshTokens { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Enums/CheckpointStatus.cs b/backend/src/Parhelion.Domain/Enums/CheckpointStatus.cs new file mode 100644 index 0000000..48f96ab --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/CheckpointStatus.cs @@ -0,0 +1,31 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Estados de checkpoint para trazabilidad de envíos. +/// +public enum CheckpointStatus +{ + /// Paquete cargado en camión (manual) + Loaded, + + /// Paquete escaneado por chofer (cadena custodia) + QrScanned, + + /// Llegó a un Hub/CEDIS + ArrivedHub, + + /// Salió del Hub hacia siguiente destino + DepartedHub, + + /// En camino al destinatario final + OutForDelivery, + + /// Intento de entrega (puede incluir motivo) + DeliveryAttempt, + + /// Entregado exitosamente + Delivered, + + /// Problema: dirección incorrecta, rechazo, etc. + Exception +} diff --git a/backend/src/Parhelion.Domain/Enums/ClientPriority.cs b/backend/src/Parhelion.Domain/Enums/ClientPriority.cs new file mode 100644 index 0000000..df7e8f8 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/ClientPriority.cs @@ -0,0 +1,21 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Prioridad predeterminada para entregas del cliente. +/// Define la urgencia con la que normalmente se deben entregar los envíos de este cliente. +/// Nota: Cada envío puede tener su propia prioridad en ShipmentPriority. +/// +public enum ClientPriority +{ + /// Prioridad normal - Entrega estándar (3-5 días) + Normal = 1, + + /// Prioridad baja - Sin urgencia, puede esperar + Low = 2, + + /// Prioridad alta - Entregas más rápidas (1-2 días) + High = 3, + + /// Urgente - Entregas express/mismo día + Urgent = 4 +} diff --git a/backend/src/Parhelion.Domain/Enums/DocumentType.cs b/backend/src/Parhelion.Domain/Enums/DocumentType.cs new file mode 100644 index 0000000..eb17d15 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/DocumentType.cs @@ -0,0 +1,22 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipos de documento B2B para envíos. +/// +public enum DocumentType +{ + /// Orden de Servicio - Petición inicial de traslado + ServiceOrder, + + /// Carta Porte - Documento legal SAT para inspecciones + Waybill, + + /// Manifiesto - Checklist de carga con instrucciones + Manifest, + + /// Hoja de Ruta - Itinerario con ventanas de entrega + TripSheet, + + /// Prueba de Entrega - Firma digital del receptor + POD +} diff --git a/backend/src/Parhelion.Domain/Enums/DriverStatus.cs b/backend/src/Parhelion.Domain/Enums/DriverStatus.cs new file mode 100644 index 0000000..2c7b476 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/DriverStatus.cs @@ -0,0 +1,16 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Estatus del chofer. +/// +public enum DriverStatus +{ + /// Puede recibir nuevos envíos + Available, + + /// Actualmente entregando paquetes + OnRoute, + + /// No disponible (vacaciones, baja, etc.) + Inactive +} diff --git a/backend/src/Parhelion.Domain/Enums/FleetLogReason.cs b/backend/src/Parhelion.Domain/Enums/FleetLogReason.cs new file mode 100644 index 0000000..3a43ac8 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/FleetLogReason.cs @@ -0,0 +1,16 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Razones de cambio de vehículo en FleetLog. +/// +public enum FleetLogReason +{ + /// Cambio de turno, entrega de unidad + ShiftChange, + + /// Avería mecánica, cambio por emergencia + Breakdown, + + /// Reasignación administrativa por disponibilidad + Reassignment +} diff --git a/backend/src/Parhelion.Domain/Enums/LocationType.cs b/backend/src/Parhelion.Domain/Enums/LocationType.cs new file mode 100644 index 0000000..0ed9088 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/LocationType.cs @@ -0,0 +1,22 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipos de ubicación en la red logística. +/// +public enum LocationType +{ + /// Nodo central, recibe y despacha masivo + RegionalHub, + + /// Transferencia rápida sin almacenamiento + CrossDock, + + /// Bodega de almacenamiento prolongado + Warehouse, + + /// Punto de venta final, solo recibe + Store, + + /// Fábrica de origen, solo despacha + SupplierPlant +} diff --git a/backend/src/Parhelion.Domain/Enums/NetworkLinkType.cs b/backend/src/Parhelion.Domain/Enums/NetworkLinkType.cs new file mode 100644 index 0000000..909f7d3 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/NetworkLinkType.cs @@ -0,0 +1,16 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipos de enlace en la red logística Hub & Spoke. +/// +public enum NetworkLinkType +{ + /// Recolección: Cliente/Proveedor → Hub + FirstMile, + + /// Carretera: Hub → Hub (larga distancia) + LineHaul, + + /// Entrega: Hub → Cliente/Tienda + LastMile +} diff --git a/backend/src/Parhelion.Domain/Enums/PackagingType.cs b/backend/src/Parhelion.Domain/Enums/PackagingType.cs new file mode 100644 index 0000000..fe1c797 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/PackagingType.cs @@ -0,0 +1,12 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipos de empaque para ítems de envío. +/// +public enum PackagingType +{ + Pallet, + Box, + Drum, + Piece +} diff --git a/backend/src/Parhelion.Domain/Enums/Permission.cs b/backend/src/Parhelion.Domain/Enums/Permission.cs new file mode 100644 index 0000000..29686ea --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/Permission.cs @@ -0,0 +1,72 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Permisos granulares del sistema. +/// IMPORTANTE: Estos permisos son INMUTABLES en runtime. +/// Solo pueden modificarse cambiando el código fuente. +/// +public enum Permission +{ + // ========== USERS ========== + UsersRead = 100, + UsersCreate = 101, + UsersUpdate = 102, + UsersDelete = 103, + + // ========== TRUCKS ========== + TrucksRead = 200, + TrucksCreate = 201, + TrucksUpdate = 202, + TrucksDelete = 203, + + // ========== DRIVERS ========== + DriversRead = 300, + DriversCreate = 301, + DriversUpdate = 302, + DriversDelete = 303, + + // ========== CLIENTS ========== + ClientsRead = 400, + ClientsCreate = 401, + ClientsUpdate = 402, + ClientsDelete = 403, + + // ========== SHIPMENTS ========== + ShipmentsRead = 500, + ShipmentsCreate = 501, + ShipmentsUpdate = 502, + ShipmentsDelete = 503, + ShipmentsAssign = 504, + ShipmentsReadOwn = 510, // Driver: solo sus envíos + ShipmentsReadByLocation = 511, // Warehouse: envíos de su ubicación + + // ========== SHIPMENT ITEMS ========== + ShipmentItemsRead = 600, + ShipmentItemsCreate = 601, + ShipmentItemsUpdate = 602, + + // ========== CHECKPOINTS ========== + CheckpointsRead = 700, + CheckpointsCreate = 701, + + // ========== ROUTES ========== + RoutesRead = 800, + RoutesCreate = 801, + RoutesUpdate = 802, + RoutesDelete = 803, + + // ========== LOCATIONS ========== + LocationsRead = 900, + LocationsCreate = 901, + LocationsUpdate = 902, + LocationsDelete = 903, + + // ========== DOCUMENTS ========== + DocumentsRead = 1000, + DocumentsCreate = 1001, + DocumentsReadOwn = 1010, // Driver: solo sus documentos + + // ========== FLEET LOGS ========== + FleetLogsRead = 1100, + FleetLogsCreate = 1101 +} diff --git a/backend/src/Parhelion.Domain/Enums/RouteStepType.cs b/backend/src/Parhelion.Domain/Enums/RouteStepType.cs new file mode 100644 index 0000000..0ae3677 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/RouteStepType.cs @@ -0,0 +1,11 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipos de parada en una ruta predefinida. +/// +public enum RouteStepType +{ + Origin, + Intermediate, + Destination +} diff --git a/backend/src/Parhelion.Domain/Enums/ShipmentPriority.cs b/backend/src/Parhelion.Domain/Enums/ShipmentPriority.cs new file mode 100644 index 0000000..a4c248a --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/ShipmentPriority.cs @@ -0,0 +1,11 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Prioridad del envío. +/// +public enum ShipmentPriority +{ + Normal, + Urgent, + Express +} diff --git a/backend/src/Parhelion.Domain/Enums/ShipmentStatus.cs b/backend/src/Parhelion.Domain/Enums/ShipmentStatus.cs new file mode 100644 index 0000000..5d9e77f --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/ShipmentStatus.cs @@ -0,0 +1,31 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Estados del envío durante su ciclo de vida. +/// +public enum ShipmentStatus +{ + /// Orden de servicio esperando revisión + PendingApproval, + + /// Envío aprobado, listo para asignar + Approved, + + /// Paquete cargado en camión, listo para salir + Loaded, + + /// En movimiento entre ubicaciones + InTransit, + + /// Temporalmente en un centro de distribución + AtHub, + + /// En camino al destinatario final + OutForDelivery, + + /// Entrega confirmada, POD capturado + Delivered, + + /// Problema que requiere atención + Exception +} diff --git a/backend/src/Parhelion.Domain/Enums/TruckType.cs b/backend/src/Parhelion.Domain/Enums/TruckType.cs new file mode 100644 index 0000000..1f08f56 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/TruckType.cs @@ -0,0 +1,22 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipos de camión con requisitos especiales de carga. +/// +public enum TruckType +{ + /// Caja Seca - Carga estándar: cartón, ropa, electrónica + DryBox, + + /// Termo/Refrigerado - Cadena de frío: alimentos, farmacéuticos + Refrigerated, + + /// Pipa HAZMAT - Materiales peligrosos: químicos, combustible + HazmatTank, + + /// Plataforma - Carga pesada: acero, maquinaria, construcción + Flatbed, + + /// Blindado - Alto valor: electrónicos, valores, dinero + Armored +} diff --git a/backend/src/Parhelion.Infrastructure/Auth/JwtService.cs b/backend/src/Parhelion.Infrastructure/Auth/JwtService.cs new file mode 100644 index 0000000..e910767 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Auth/JwtService.cs @@ -0,0 +1,111 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using Parhelion.Application.Auth; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Auth; + +/// +/// Implementación del servicio JWT para generación y validación de tokens. +/// +public class JwtService : IJwtService +{ + private readonly IConfiguration _configuration; + private readonly SymmetricSecurityKey _signingKey; + private readonly int _accessTokenExpirationMinutes; + private readonly int _refreshTokenExpirationDays; + + public JwtService(IConfiguration configuration) + { + _configuration = configuration; + + var secretKey = _configuration["Jwt:SecretKey"] + ?? throw new InvalidOperationException("JWT SecretKey not configured"); + + _signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); + _accessTokenExpirationMinutes = int.Parse(_configuration["Jwt:AccessTokenExpirationMinutes"] ?? "120"); + _refreshTokenExpirationDays = int.Parse(_configuration["Jwt:RefreshTokenExpirationDays"] ?? "7"); + } + + /// + public string GenerateAccessToken(User user, string roleName) + { + var claims = new List + { + new(ClaimTypes.NameIdentifier, user.Id.ToString()), + new(ClaimTypes.Email, user.Email), + new(ClaimTypes.Name, user.FullName), + new(ClaimTypes.Role, roleName), + new("tenant_id", user.TenantId.ToString()), + new("is_demo", user.IsDemoUser.ToString().ToLower()) + }; + + // Agregar permisos del rol como claims + var permissions = RolePermissions.GetPermissions(roleName); + foreach (var permission in permissions) + { + claims.Add(new Claim("permission", permission.ToString())); + } + + var credentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256); + var expiration = GetAccessTokenExpiration(); + + var token = new JwtSecurityToken( + issuer: _configuration["Jwt:Issuer"] ?? "Parhelion", + audience: _configuration["Jwt:Audience"] ?? "ParhelionClient", + claims: claims, + expires: expiration, + signingCredentials: credentials + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + /// + public string GenerateRefreshToken() + { + var randomBytes = new byte[64]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(randomBytes); + return Convert.ToBase64String(randomBytes); + } + + /// + public ClaimsPrincipal? ValidateAccessToken(string token) + { + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = _configuration["Jwt:Issuer"] ?? "Parhelion", + ValidAudience = _configuration["Jwt:Audience"] ?? "ParhelionClient", + IssuerSigningKey = _signingKey, + ClockSkew = TimeSpan.Zero + }; + + var principal = tokenHandler.ValidateToken(token, validationParameters, out _); + return principal; + } + catch + { + return null; + } + } + + /// + public DateTime GetAccessTokenExpiration() + => DateTime.UtcNow.AddMinutes(_accessTokenExpirationMinutes); + + /// + public DateTime GetRefreshTokenExpiration() + => DateTime.UtcNow.AddDays(_refreshTokenExpirationDays); +} diff --git a/backend/src/Parhelion.Infrastructure/Auth/PasswordHasher.cs b/backend/src/Parhelion.Infrastructure/Auth/PasswordHasher.cs new file mode 100644 index 0000000..a989c6a --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Auth/PasswordHasher.cs @@ -0,0 +1,47 @@ +using Parhelion.Application.Auth; +using BCryptNet = BCrypt.Net.BCrypt; + +namespace Parhelion.Infrastructure.Auth; + +/// +/// Implementación del servicio de hashing de passwords. +/// Usa BCrypt para usuarios normales. +/// Nota: Argon2id se puede agregar después con el paquete Isopoh.Cryptography.Argon2 +/// +public class PasswordHasher : IPasswordHasher +{ + private const int WorkFactor = 12; // 2^12 = 4096 iterations + + /// + public string HashPassword(string password, bool useArgon2 = false) + { + if (useArgon2) + { + // TODO: Implementar Argon2id para admin cuando se agregue el paquete + // Por ahora usar BCrypt con mayor work factor + return BCryptNet.HashPassword(password, BCryptNet.GenerateSalt(14)); + } + + return BCryptNet.HashPassword(password, BCryptNet.GenerateSalt(WorkFactor)); + } + + /// + public bool VerifyPassword(string password, string passwordHash, bool usesArgon2 = false) + { + try + { + if (usesArgon2) + { + // TODO: Verificar con Argon2id cuando se implemente + // Por ahora verificar con BCrypt + return BCryptNet.Verify(password, passwordHash); + } + + return BCryptNet.Verify(password, passwordHash); + } + catch + { + return false; + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Class1.cs b/backend/src/Parhelion.Infrastructure/Class1.cs deleted file mode 100644 index 4a92927..0000000 --- a/backend/src/Parhelion.Infrastructure/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Parhelion.Infrastructure; - -public class Class1 -{ - -} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ClientConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ClientConfiguration.cs new file mode 100644 index 0000000..b146736 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ClientConfiguration.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class ClientConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Clients"); + + builder.HasKey(c => c.Id); + + // Basic data + builder.Property(c => c.CompanyName) + .IsRequired() + .HasMaxLength(200); + + builder.Property(c => c.TradeName) + .HasMaxLength(200); + + builder.Property(c => c.ContactName) + .IsRequired() + .HasMaxLength(150); + + builder.Property(c => c.Email) + .IsRequired() + .HasMaxLength(150); + + builder.Property(c => c.Phone) + .IsRequired() + .HasMaxLength(30); + + // Fiscal data + builder.Property(c => c.TaxId) + .HasMaxLength(20); // RFC mexicano + + builder.Property(c => c.LegalName) + .HasMaxLength(300); + + builder.Property(c => c.BillingAddress) + .HasMaxLength(500); + + // Shipping data + builder.Property(c => c.ShippingAddress) + .IsRequired() + .HasMaxLength(500); + + builder.Property(c => c.PreferredProductTypes) + .HasMaxLength(300); + + builder.Property(c => c.Priority) + .HasConversion() + .HasMaxLength(20); + + builder.Property(c => c.Notes) + .HasMaxLength(1000); + + // Soft Delete Query Filter + builder.HasQueryFilter(c => !c.IsDeleted); + + // Indexes + builder.HasIndex(c => c.TenantId); + builder.HasIndex(c => c.Email); + builder.HasIndex(c => new { c.TenantId, c.CompanyName }); + + // Relationships + builder.HasOne(c => c.Tenant) + .WithMany() + .HasForeignKey(c => c.TenantId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/DriverConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/DriverConfiguration.cs new file mode 100644 index 0000000..cd4482a --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/DriverConfiguration.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class DriverConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(d => d.Id); + + builder.Property(d => d.FullName) + .IsRequired() + .HasMaxLength(200); + + builder.Property(d => d.Phone) + .IsRequired() + .HasMaxLength(20); + + builder.Property(d => d.LicenseNumber) + .IsRequired() + .HasMaxLength(50); + + // Índice por tenant y status para dashboard + builder.HasIndex(d => new { d.TenantId, d.Status }); + + // Relaciones + builder.HasOne(d => d.Tenant) + .WithMany(t => t.Drivers) + .HasForeignKey(d => d.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(d => d.User) + .WithOne(u => u.Driver) + .HasForeignKey(d => d.UserId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(d => d.DefaultTruck) + .WithMany(t => t.DefaultDrivers) + .HasForeignKey(d => d.DefaultTruckId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(d => d.CurrentTruck) + .WithMany(t => t.CurrentDrivers) + .HasForeignKey(d => d.CurrentTruckId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/FleetLogConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/FleetLogConfiguration.cs new file mode 100644 index 0000000..ecb8f09 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/FleetLogConfiguration.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class FleetLogConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(fl => fl.Id); + + // Índice para historial de cambios por chofer + builder.HasIndex(fl => new { fl.DriverId, fl.Timestamp }); + + // Relaciones + builder.HasOne(fl => fl.Tenant) + .WithMany(t => t.FleetLogs) + .HasForeignKey(fl => fl.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(fl => fl.Driver) + .WithMany(d => d.FleetHistory) + .HasForeignKey(fl => fl.DriverId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(fl => fl.OldTruck) + .WithMany(t => t.OldTruckLogs) + .HasForeignKey(fl => fl.OldTruckId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(fl => fl.NewTruck) + .WithMany(t => t.NewTruckLogs) + .HasForeignKey(fl => fl.NewTruckId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(fl => fl.CreatedBy) + .WithMany(u => u.CreatedFleetLogs) + .HasForeignKey(fl => fl.CreatedByUserId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/LocationConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/LocationConfiguration.cs new file mode 100644 index 0000000..ec6c834 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/LocationConfiguration.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class LocationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(l => l.Id); + + builder.Property(l => l.Code) + .IsRequired() + .HasMaxLength(10); + + builder.Property(l => l.Name) + .IsRequired() + .HasMaxLength(100); + + builder.Property(l => l.FullAddress) + .IsRequired() + .HasMaxLength(500); + + // Código único por tenant (estilo aeropuerto: MTY, GDL, MM) + builder.HasIndex(l => new { l.TenantId, l.Code }).IsUnique(); + + // Índice por tipo para filtros + builder.HasIndex(l => new { l.TenantId, l.Type }); + + // Relación con Tenant + builder.HasOne(l => l.Tenant) + .WithMany(t => t.Locations) + .HasForeignKey(l => l.TenantId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/NetworkLinkConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/NetworkLinkConfiguration.cs new file mode 100644 index 0000000..683b908 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/NetworkLinkConfiguration.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class NetworkLinkConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(nl => nl.Id); + + // Índices para búsqueda de rutas + builder.HasIndex(nl => nl.OriginLocationId); + builder.HasIndex(nl => nl.DestinationLocationId); + + // Relaciones + builder.HasOne(nl => nl.Tenant) + .WithMany(t => t.NetworkLinks) + .HasForeignKey(nl => nl.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(nl => nl.OriginLocation) + .WithMany(l => l.OutgoingLinks) + .HasForeignKey(nl => nl.OriginLocationId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(nl => nl.DestinationLocation) + .WithMany(l => l.IncomingLinks) + .HasForeignKey(nl => nl.DestinationLocationId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/RefreshTokenConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/RefreshTokenConfiguration.cs new file mode 100644 index 0000000..10ca7c6 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/RefreshTokenConfiguration.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class RefreshTokenConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("RefreshTokens"); + + builder.HasKey(rt => rt.Id); + + builder.Property(rt => rt.TokenHash) + .IsRequired() + .HasMaxLength(256); + + builder.Property(rt => rt.ExpiresAt) + .IsRequired(); + + builder.Property(rt => rt.IsRevoked) + .HasDefaultValue(false); + + builder.Property(rt => rt.RevokedReason) + .HasMaxLength(200); + + builder.Property(rt => rt.CreatedFromIp) + .HasMaxLength(45); // IPv6 max length + + builder.Property(rt => rt.UserAgent) + .HasMaxLength(500); + + // Indexes + builder.HasIndex(rt => rt.UserId); + builder.HasIndex(rt => rt.TokenHash); + builder.HasIndex(rt => rt.ExpiresAt); + + // Relationships + builder.HasOne(rt => rt.User) + .WithMany(u => u.RefreshTokens) + .HasForeignKey(rt => rt.UserId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/RoleConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/RoleConfiguration.cs new file mode 100644 index 0000000..a84c910 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/RoleConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class RoleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(r => r.Id); + + builder.Property(r => r.Name) + .IsRequired() + .HasMaxLength(50); + + // Índice único para nombre de rol + builder.HasIndex(r => r.Name).IsUnique(); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/RouteBlueprintConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/RouteBlueprintConfiguration.cs new file mode 100644 index 0000000..0b1c95c --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/RouteBlueprintConfiguration.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class RouteBlueprintConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(rb => rb.Id); + + builder.Property(rb => rb.Name) + .IsRequired() + .HasMaxLength(100); + + builder.Property(rb => rb.Description) + .HasMaxLength(500); + + // Relación + builder.HasOne(rb => rb.Tenant) + .WithMany(t => t.RouteBlueprints) + .HasForeignKey(rb => rb.TenantId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/RouteStepConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/RouteStepConfiguration.cs new file mode 100644 index 0000000..3542627 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/RouteStepConfiguration.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class RouteStepConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(rs => rs.Id); + + // Índice para ordenar pasos de ruta + builder.HasIndex(rs => new { rs.RouteBlueprintId, rs.StepOrder }); + + // Relaciones + builder.HasOne(rs => rs.RouteBlueprint) + .WithMany(rb => rb.Steps) + .HasForeignKey(rs => rs.RouteBlueprintId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(rs => rs.Location) + .WithMany(l => l.RouteSteps) + .HasForeignKey(rs => rs.LocationId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentCheckpointConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentCheckpointConfiguration.cs new file mode 100644 index 0000000..269b506 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentCheckpointConfiguration.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class ShipmentCheckpointConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(sc => sc.Id); + + builder.Property(sc => sc.Remarks) + .HasMaxLength(1000); + + // Campos de trazabilidad de cargueros + builder.Property(sc => sc.ActionType) + .HasMaxLength(50); + + builder.Property(sc => sc.PreviousCustodian) + .HasMaxLength(200); + + builder.Property(sc => sc.NewCustodian) + .HasMaxLength(200); + + // Índices para trazabilidad + builder.HasIndex(sc => sc.ShipmentId); + builder.HasIndex(sc => sc.Timestamp); + builder.HasIndex(sc => sc.HandledByDriverId); + builder.HasIndex(sc => sc.LoadedOntoTruckId); + + // Relaciones + builder.HasOne(sc => sc.Shipment) + .WithMany(s => s.History) + .HasForeignKey(sc => sc.ShipmentId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(sc => sc.Location) + .WithMany(l => l.Checkpoints) + .HasForeignKey(sc => sc.LocationId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(sc => sc.CreatedBy) + .WithMany(u => u.CreatedCheckpoints) + .HasForeignKey(sc => sc.CreatedByUserId) + .OnDelete(DeleteBehavior.Restrict); + + // Trazabilidad de cargueros + builder.HasOne(sc => sc.HandledByDriver) + .WithMany(d => d.HandledCheckpoints) + .HasForeignKey(sc => sc.HandledByDriverId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(sc => sc.LoadedOntoTruck) + .WithMany(t => t.LoadedCheckpoints) + .HasForeignKey(sc => sc.LoadedOntoTruckId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentConfiguration.cs new file mode 100644 index 0000000..7b19636 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentConfiguration.cs @@ -0,0 +1,101 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class ShipmentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(s => s.Id); + + builder.Property(s => s.TrackingNumber) + .IsRequired() + .HasMaxLength(20); + + builder.Property(s => s.QrCodeData) + .IsRequired() + .HasMaxLength(100); + + builder.Property(s => s.RecipientName) + .IsRequired() + .HasMaxLength(200); + + builder.Property(s => s.RecipientPhone) + .HasMaxLength(20); + + builder.Property(s => s.TotalWeightKg) + .HasPrecision(10, 2); + + builder.Property(s => s.TotalVolumeM3) + .HasPrecision(10, 3); + + builder.Property(s => s.DeclaredValue) + .HasPrecision(18, 2); + + builder.Property(s => s.SatMerchandiseCode) + .HasMaxLength(20); + + builder.Property(s => s.DeliveryInstructions) + .HasMaxLength(1000); + + builder.Property(s => s.RecipientSignatureUrl) + .HasMaxLength(500); + + // SEGURIDAD: Tracking number único global + builder.HasIndex(s => s.TrackingNumber).IsUnique(); + + // Índices para dashboard y filtros frecuentes + builder.HasIndex(s => new { s.TenantId, s.Status }); + builder.HasIndex(s => new { s.TenantId, s.CreatedAt }); + builder.HasIndex(s => new { s.TenantId, s.IsDelayed }) + .HasFilter("\"IsDelayed\" = true"); + builder.HasIndex(s => s.DriverId); + builder.HasIndex(s => s.OriginLocationId); + builder.HasIndex(s => s.DestinationLocationId); + + // Relaciones + builder.HasOne(s => s.Tenant) + .WithMany(t => t.Shipments) + .HasForeignKey(s => s.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(s => s.OriginLocation) + .WithMany(l => l.OriginShipments) + .HasForeignKey(s => s.OriginLocationId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(s => s.DestinationLocation) + .WithMany(l => l.DestinationShipments) + .HasForeignKey(s => s.DestinationLocationId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(s => s.AssignedRoute) + .WithMany(r => r.Shipments) + .HasForeignKey(s => s.AssignedRouteId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(s => s.Truck) + .WithMany(t => t.Shipments) + .HasForeignKey(s => s.TruckId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(s => s.Driver) + .WithMany(d => d.Shipments) + .HasForeignKey(s => s.DriverId) + .OnDelete(DeleteBehavior.SetNull); + + // Relación con Client (remitente) + builder.HasOne(s => s.Sender) + .WithMany(c => c.ShipmentsAsSender) + .HasForeignKey(s => s.SenderId) + .OnDelete(DeleteBehavior.SetNull); + + // Relación con Client (destinatario) + builder.HasOne(s => s.RecipientClient) + .WithMany(c => c.ShipmentsAsRecipient) + .HasForeignKey(s => s.RecipientClientId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentDocumentConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentDocumentConfiguration.cs new file mode 100644 index 0000000..0d7073d --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentDocumentConfiguration.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class ShipmentDocumentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(sd => sd.Id); + + builder.Property(sd => sd.FileUrl) + .IsRequired() + .HasMaxLength(500); + + builder.Property(sd => sd.GeneratedBy) + .IsRequired() + .HasMaxLength(50); + + // Relación + builder.HasOne(sd => sd.Shipment) + .WithMany(s => s.Documents) + .HasForeignKey(sd => sd.ShipmentId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentItemConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentItemConfiguration.cs new file mode 100644 index 0000000..ceb3e55 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentItemConfiguration.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class ShipmentItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(si => si.Id); + + builder.Property(si => si.Sku) + .HasMaxLength(50); + + builder.Property(si => si.Description) + .IsRequired() + .HasMaxLength(500); + + builder.Property(si => si.WeightKg) + .HasPrecision(10, 2); + + builder.Property(si => si.WidthCm) + .HasPrecision(10, 2); + + builder.Property(si => si.HeightCm) + .HasPrecision(10, 2); + + builder.Property(si => si.LengthCm) + .HasPrecision(10, 2); + + builder.Property(si => si.DeclaredValue) + .HasPrecision(18, 2); + + builder.Property(si => si.StackingInstructions) + .HasMaxLength(500); + + // VolumeM3 es una propiedad calculada, no se mapea a columna + builder.Ignore(si => si.VolumeM3); + builder.Ignore(si => si.VolumetricWeightKg); + + // Relación + builder.HasOne(si => si.Shipment) + .WithMany(s => s.Items) + .HasForeignKey(si => si.ShipmentId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/TenantConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/TenantConfiguration.cs new file mode 100644 index 0000000..749970b --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/TenantConfiguration.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class TenantConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(t => t.Id); + + builder.Property(t => t.CompanyName) + .IsRequired() + .HasMaxLength(200); + + builder.Property(t => t.ContactEmail) + .IsRequired() + .HasMaxLength(256); + + // Índice para búsqueda rápida de tenants activos + builder.HasIndex(t => t.IsActive); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/TruckConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/TruckConfiguration.cs new file mode 100644 index 0000000..be94fb2 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/TruckConfiguration.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class TruckConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(t => t.Id); + + builder.Property(t => t.Plate) + .IsRequired() + .HasMaxLength(20); + + builder.Property(t => t.Model) + .IsRequired() + .HasMaxLength(100); + + builder.Property(t => t.MaxCapacityKg) + .HasPrecision(10, 2); + + builder.Property(t => t.MaxVolumeM3) + .HasPrecision(10, 2); + + // Placa única por tenant + builder.HasIndex(t => new { t.TenantId, t.Plate }).IsUnique(); + + // Índice por tipo para filtros de compatibilidad + builder.HasIndex(t => new { t.TenantId, t.Type }); + + // Relación con Tenant + builder.HasOne(t => t.Tenant) + .WithMany(te => te.Trucks) + .HasForeignKey(t => t.TenantId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/UserConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/UserConfiguration.cs new file mode 100644 index 0000000..5de333a --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/UserConfiguration.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class UserConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(u => u.Id); + + builder.Property(u => u.Email) + .IsRequired() + .HasMaxLength(256); + + builder.Property(u => u.PasswordHash) + .IsRequired() + .HasMaxLength(500); + + builder.Property(u => u.FullName) + .IsRequired() + .HasMaxLength(200); + + // SEGURIDAD: Email único global (no por tenant) + builder.HasIndex(u => u.Email).IsUnique(); + + // Índice para búsqueda por tenant + builder.HasIndex(u => u.TenantId); + + // Relaciones + builder.HasOne(u => u.Tenant) + .WithMany(t => t.Users) + .HasForeignKey(u => u.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(u => u.Role) + .WithMany(r => r.Users) + .HasForeignKey(u => u.RoleId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213001913_InitialCreate.Designer.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213001913_InitialCreate.Designer.cs new file mode 100644 index 0000000..01b16de --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213001913_InitialCreate.Designer.cs @@ -0,0 +1,1204 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + [Migration("20251213001913_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Drivers") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Driver"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213001913_InitialCreate.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213001913_InitialCreate.cs new file mode 100644 index 0000000..fe46e98 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213001913_InitialCreate.cs @@ -0,0 +1,771 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + Description = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tenants", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CompanyName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + ContactEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + FleetSize = table.Column(type: "integer", nullable: false), + DriverCount = table.Column(type: "integer", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tenants", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Locations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Type = table.Column(type: "integer", nullable: false), + FullAddress = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + CanReceive = table.Column(type: "boolean", nullable: false), + CanDispatch = table.Column(type: "boolean", nullable: false), + IsInternal = table.Column(type: "boolean", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Locations", x => x.Id); + table.ForeignKey( + name: "FK_Locations_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "RouteBlueprints", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + TotalSteps = table.Column(type: "integer", nullable: false), + TotalTransitTime = table.Column(type: "interval", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RouteBlueprints", x => x.Id); + table.ForeignKey( + name: "FK_RouteBlueprints_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Trucks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Plate = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Model = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Type = table.Column(type: "integer", nullable: false), + MaxCapacityKg = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + MaxVolumeM3 = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Trucks", x => x.Id); + table.ForeignKey( + name: "FK_Trucks_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + PasswordHash = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + FullName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + RoleId = table.Column(type: "uuid", nullable: false), + IsDemoUser = table.Column(type: "boolean", nullable: false), + UsesArgon2 = table.Column(type: "boolean", nullable: false), + LastLogin = table.Column(type: "timestamp with time zone", nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Users_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "NetworkLinks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OriginLocationId = table.Column(type: "uuid", nullable: false), + DestinationLocationId = table.Column(type: "uuid", nullable: false), + LinkType = table.Column(type: "integer", nullable: false), + TransitTime = table.Column(type: "interval", nullable: false), + IsBidirectional = table.Column(type: "boolean", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_NetworkLinks", x => x.Id); + table.ForeignKey( + name: "FK_NetworkLinks_Locations_DestinationLocationId", + column: x => x.DestinationLocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_NetworkLinks_Locations_OriginLocationId", + column: x => x.OriginLocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_NetworkLinks_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "RouteSteps", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + RouteBlueprintId = table.Column(type: "uuid", nullable: false), + LocationId = table.Column(type: "uuid", nullable: false), + StepOrder = table.Column(type: "integer", nullable: false), + StandardTransitTime = table.Column(type: "interval", nullable: false), + StepType = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RouteSteps", x => x.Id); + table.ForeignKey( + name: "FK_RouteSteps_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_RouteSteps_RouteBlueprints_RouteBlueprintId", + column: x => x.RouteBlueprintId, + principalTable: "RouteBlueprints", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Drivers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + FullName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + LicenseNumber = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + DefaultTruckId = table.Column(type: "uuid", nullable: true), + CurrentTruckId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Drivers", x => x.Id); + table.ForeignKey( + name: "FK_Drivers_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Drivers_Trucks_CurrentTruckId", + column: x => x.CurrentTruckId, + principalTable: "Trucks", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Drivers_Trucks_DefaultTruckId", + column: x => x.DefaultTruckId, + principalTable: "Trucks", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Drivers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "FleetLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + DriverId = table.Column(type: "uuid", nullable: false), + OldTruckId = table.Column(type: "uuid", nullable: true), + NewTruckId = table.Column(type: "uuid", nullable: false), + Reason = table.Column(type: "integer", nullable: false), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + CreatedByUserId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FleetLogs", x => x.Id); + table.ForeignKey( + name: "FK_FleetLogs_Drivers_DriverId", + column: x => x.DriverId, + principalTable: "Drivers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_FleetLogs_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_FleetLogs_Trucks_NewTruckId", + column: x => x.NewTruckId, + principalTable: "Trucks", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_FleetLogs_Trucks_OldTruckId", + column: x => x.OldTruckId, + principalTable: "Trucks", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_FleetLogs_Users_CreatedByUserId", + column: x => x.CreatedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Shipments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TrackingNumber = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + QrCodeData = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + OriginLocationId = table.Column(type: "uuid", nullable: false), + DestinationLocationId = table.Column(type: "uuid", nullable: false), + AssignedRouteId = table.Column(type: "uuid", nullable: true), + CurrentStepOrder = table.Column(type: "integer", nullable: true), + RecipientName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + RecipientPhone = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), + TotalWeightKg = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + TotalVolumeM3 = table.Column(type: "numeric(10,3)", precision: 10, scale: 3, nullable: false), + DeclaredValue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + SatMerchandiseCode = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), + DeliveryInstructions = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + RecipientSignatureUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + Priority = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + TruckId = table.Column(type: "uuid", nullable: true), + DriverId = table.Column(type: "uuid", nullable: true), + WasQrScanned = table.Column(type: "boolean", nullable: false), + IsDelayed = table.Column(type: "boolean", nullable: false), + ScheduledDeparture = table.Column(type: "timestamp with time zone", nullable: true), + PickupWindowStart = table.Column(type: "timestamp with time zone", nullable: true), + PickupWindowEnd = table.Column(type: "timestamp with time zone", nullable: true), + EstimatedArrival = table.Column(type: "timestamp with time zone", nullable: true), + AssignedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeliveredAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Shipments", x => x.Id); + table.ForeignKey( + name: "FK_Shipments_Drivers_DriverId", + column: x => x.DriverId, + principalTable: "Drivers", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Shipments_Locations_DestinationLocationId", + column: x => x.DestinationLocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shipments_Locations_OriginLocationId", + column: x => x.OriginLocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shipments_RouteBlueprints_AssignedRouteId", + column: x => x.AssignedRouteId, + principalTable: "RouteBlueprints", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Shipments_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shipments_Trucks_TruckId", + column: x => x.TruckId, + principalTable: "Trucks", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ShipmentCheckpoints", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ShipmentId = table.Column(type: "uuid", nullable: false), + LocationId = table.Column(type: "uuid", nullable: true), + StatusCode = table.Column(type: "integer", nullable: false), + Remarks = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + CreatedByUserId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShipmentCheckpoints", x => x.Id); + table.ForeignKey( + name: "FK_ShipmentCheckpoints_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ShipmentCheckpoints_Shipments_ShipmentId", + column: x => x.ShipmentId, + principalTable: "Shipments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShipmentCheckpoints_Users_CreatedByUserId", + column: x => x.CreatedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ShipmentDocuments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ShipmentId = table.Column(type: "uuid", nullable: false), + DocumentType = table.Column(type: "integer", nullable: false), + FileUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + GeneratedBy = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + GeneratedAt = table.Column(type: "timestamp with time zone", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShipmentDocuments", x => x.Id); + table.ForeignKey( + name: "FK_ShipmentDocuments_Shipments_ShipmentId", + column: x => x.ShipmentId, + principalTable: "Shipments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShipmentItems", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ShipmentId = table.Column(type: "uuid", nullable: false), + Sku = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + PackagingType = table.Column(type: "integer", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + WeightKg = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + WidthCm = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + HeightCm = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + LengthCm = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + DeclaredValue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + IsFragile = table.Column(type: "boolean", nullable: false), + IsHazardous = table.Column(type: "boolean", nullable: false), + RequiresRefrigeration = table.Column(type: "boolean", nullable: false), + StackingInstructions = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShipmentItems", x => x.Id); + table.ForeignKey( + name: "FK_ShipmentItems_Shipments_ShipmentId", + column: x => x.ShipmentId, + principalTable: "Shipments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_CurrentTruckId", + table: "Drivers", + column: "CurrentTruckId"); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_DefaultTruckId", + table: "Drivers", + column: "DefaultTruckId"); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_TenantId_Status", + table: "Drivers", + columns: new[] { "TenantId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_UserId", + table: "Drivers", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FleetLogs_CreatedByUserId", + table: "FleetLogs", + column: "CreatedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_FleetLogs_DriverId_Timestamp", + table: "FleetLogs", + columns: new[] { "DriverId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_FleetLogs_NewTruckId", + table: "FleetLogs", + column: "NewTruckId"); + + migrationBuilder.CreateIndex( + name: "IX_FleetLogs_OldTruckId", + table: "FleetLogs", + column: "OldTruckId"); + + migrationBuilder.CreateIndex( + name: "IX_FleetLogs_TenantId", + table: "FleetLogs", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Locations_TenantId_Code", + table: "Locations", + columns: new[] { "TenantId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Locations_TenantId_Type", + table: "Locations", + columns: new[] { "TenantId", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_NetworkLinks_DestinationLocationId", + table: "NetworkLinks", + column: "DestinationLocationId"); + + migrationBuilder.CreateIndex( + name: "IX_NetworkLinks_OriginLocationId", + table: "NetworkLinks", + column: "OriginLocationId"); + + migrationBuilder.CreateIndex( + name: "IX_NetworkLinks_TenantId", + table: "NetworkLinks", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Roles_Name", + table: "Roles", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RouteBlueprints_TenantId", + table: "RouteBlueprints", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_RouteSteps_LocationId", + table: "RouteSteps", + column: "LocationId"); + + migrationBuilder.CreateIndex( + name: "IX_RouteSteps_RouteBlueprintId_StepOrder", + table: "RouteSteps", + columns: new[] { "RouteBlueprintId", "StepOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentCheckpoints_CreatedByUserId", + table: "ShipmentCheckpoints", + column: "CreatedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentCheckpoints_LocationId", + table: "ShipmentCheckpoints", + column: "LocationId"); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentCheckpoints_ShipmentId", + table: "ShipmentCheckpoints", + column: "ShipmentId"); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentCheckpoints_Timestamp", + table: "ShipmentCheckpoints", + column: "Timestamp"); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentDocuments_ShipmentId", + table: "ShipmentDocuments", + column: "ShipmentId"); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentItems_ShipmentId", + table: "ShipmentItems", + column: "ShipmentId"); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_AssignedRouteId", + table: "Shipments", + column: "AssignedRouteId"); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_DestinationLocationId", + table: "Shipments", + column: "DestinationLocationId"); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_DriverId", + table: "Shipments", + column: "DriverId"); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_OriginLocationId", + table: "Shipments", + column: "OriginLocationId"); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_TenantId_CreatedAt", + table: "Shipments", + columns: new[] { "TenantId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_TenantId_IsDelayed", + table: "Shipments", + columns: new[] { "TenantId", "IsDelayed" }, + filter: "\"IsDelayed\" = true"); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_TenantId_Status", + table: "Shipments", + columns: new[] { "TenantId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_TrackingNumber", + table: "Shipments", + column: "TrackingNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_TruckId", + table: "Shipments", + column: "TruckId"); + + migrationBuilder.CreateIndex( + name: "IX_Tenants_IsActive", + table: "Tenants", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_Trucks_TenantId_Plate", + table: "Trucks", + columns: new[] { "TenantId", "Plate" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Trucks_TenantId_Type", + table: "Trucks", + columns: new[] { "TenantId", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_RoleId", + table: "Users", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_TenantId", + table: "Users", + column: "TenantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FleetLogs"); + + migrationBuilder.DropTable( + name: "NetworkLinks"); + + migrationBuilder.DropTable( + name: "RouteSteps"); + + migrationBuilder.DropTable( + name: "ShipmentCheckpoints"); + + migrationBuilder.DropTable( + name: "ShipmentDocuments"); + + migrationBuilder.DropTable( + name: "ShipmentItems"); + + migrationBuilder.DropTable( + name: "Shipments"); + + migrationBuilder.DropTable( + name: "Drivers"); + + migrationBuilder.DropTable( + name: "Locations"); + + migrationBuilder.DropTable( + name: "RouteBlueprints"); + + migrationBuilder.DropTable( + name: "Trucks"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Roles"); + + migrationBuilder.DropTable( + name: "Tenants"); + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213030538_AddAuthAndClients.Designer.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213030538_AddAuthAndClients.Designer.cs new file mode 100644 index 0000000..6c28c13 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213030538_AddAuthAndClients.Designer.cs @@ -0,0 +1,1505 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + [Migration("20251213030538_AddAuthAndClients")] + partial class AddAuthAndClients + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PreferredProductTypes") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TaxId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "CompanyName"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Curp") + .HasColumnType("text"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmergencyContact") + .HasColumnType("text"); + + b.Property("EmergencyPhone") + .HasColumnType("text"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LicenseExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LicenseType") + .HasColumnType("text"); + + b.Property("Nss") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Rfc") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientClientId") + .HasColumnType("uuid"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("RecipientClientId"); + + b.HasIndex("SenderId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HandledByDriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LoadedOntoTruckId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("NewCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PreviousCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("HandledByDriverId"); + + b.HasIndex("LoadedOntoTruckId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentOdometerKm") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineNumber") + .HasColumnType("text"); + + b.Property("InsuranceExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("InsurancePolicy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NextMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationNumber") + .HasColumnType("text"); + + b.Property("Vin") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Drivers") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Client", "RecipientClient") + .WithMany("ShipmentsAsRecipient") + .HasForeignKey("RecipientClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Client", "Sender") + .WithMany("ShipmentsAsSender") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("RecipientClient"); + + b.Navigation("Sender"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "HandledByDriver") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByDriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") + .WithMany("LoadedCheckpoints") + .HasForeignKey("LoadedOntoTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("HandledByDriver"); + + b.Navigation("LoadedOntoTruck"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Navigation("ShipmentsAsRecipient"); + + b.Navigation("ShipmentsAsSender"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("HandledCheckpoints"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("LoadedCheckpoints"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Driver"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213030538_AddAuthAndClients.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213030538_AddAuthAndClients.cs new file mode 100644 index 0000000..e428517 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213030538_AddAuthAndClients.cs @@ -0,0 +1,466 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + /// + public partial class AddAuthAndClients : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Color", + table: "Trucks", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "CurrentOdometerKm", + table: "Trucks", + type: "numeric", + nullable: true); + + migrationBuilder.AddColumn( + name: "EngineNumber", + table: "Trucks", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InsuranceExpiration", + table: "Trucks", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "InsurancePolicy", + table: "Trucks", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastMaintenanceDate", + table: "Trucks", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "NextMaintenanceDate", + table: "Trucks", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "VerificationExpiration", + table: "Trucks", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "VerificationNumber", + table: "Trucks", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Vin", + table: "Trucks", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Year", + table: "Trucks", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "RecipientClientId", + table: "Shipments", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "SenderId", + table: "Shipments", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "ActionType", + table: "ShipmentCheckpoints", + type: "character varying(50)", + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "HandledByDriverId", + table: "ShipmentCheckpoints", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LoadedOntoTruckId", + table: "ShipmentCheckpoints", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "NewCustodian", + table: "ShipmentCheckpoints", + type: "character varying(200)", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "PreviousCustodian", + table: "ShipmentCheckpoints", + type: "character varying(200)", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "Curp", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "EmergencyContact", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "EmergencyPhone", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "HireDate", + table: "Drivers", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LicenseExpiration", + table: "Drivers", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LicenseType", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Nss", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Rfc", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "Clients", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CompanyName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + TradeName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + ContactName = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), + Email = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), + Phone = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + TaxId = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), + LegalName = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + BillingAddress = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + ShippingAddress = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + PreferredProductTypes = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + Priority = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + Notes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Clients", x => x.Id); + table.ForeignKey( + name: "FK_Clients_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "RefreshTokens", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + TokenHash = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + IsRevoked = table.Column(type: "boolean", nullable: false, defaultValue: false), + RevokedAt = table.Column(type: "timestamp with time zone", nullable: true), + RevokedReason = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + CreatedFromIp = table.Column(type: "character varying(45)", maxLength: 45, nullable: true), + UserAgent = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshTokens", x => x.Id); + table.ForeignKey( + name: "FK_RefreshTokens_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_RecipientClientId", + table: "Shipments", + column: "RecipientClientId"); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_SenderId", + table: "Shipments", + column: "SenderId"); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentCheckpoints_HandledByDriverId", + table: "ShipmentCheckpoints", + column: "HandledByDriverId"); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentCheckpoints_LoadedOntoTruckId", + table: "ShipmentCheckpoints", + column: "LoadedOntoTruckId"); + + migrationBuilder.CreateIndex( + name: "IX_Clients_Email", + table: "Clients", + column: "Email"); + + migrationBuilder.CreateIndex( + name: "IX_Clients_TenantId", + table: "Clients", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Clients_TenantId_CompanyName", + table: "Clients", + columns: new[] { "TenantId", "CompanyName" }); + + migrationBuilder.CreateIndex( + name: "IX_RefreshTokens_ExpiresAt", + table: "RefreshTokens", + column: "ExpiresAt"); + + migrationBuilder.CreateIndex( + name: "IX_RefreshTokens_TokenHash", + table: "RefreshTokens", + column: "TokenHash"); + + migrationBuilder.CreateIndex( + name: "IX_RefreshTokens_UserId", + table: "RefreshTokens", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_ShipmentCheckpoints_Drivers_HandledByDriverId", + table: "ShipmentCheckpoints", + column: "HandledByDriverId", + principalTable: "Drivers", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_ShipmentCheckpoints_Trucks_LoadedOntoTruckId", + table: "ShipmentCheckpoints", + column: "LoadedOntoTruckId", + principalTable: "Trucks", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Shipments_Clients_RecipientClientId", + table: "Shipments", + column: "RecipientClientId", + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Shipments_Clients_SenderId", + table: "Shipments", + column: "SenderId", + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ShipmentCheckpoints_Drivers_HandledByDriverId", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropForeignKey( + name: "FK_ShipmentCheckpoints_Trucks_LoadedOntoTruckId", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropForeignKey( + name: "FK_Shipments_Clients_RecipientClientId", + table: "Shipments"); + + migrationBuilder.DropForeignKey( + name: "FK_Shipments_Clients_SenderId", + table: "Shipments"); + + migrationBuilder.DropTable( + name: "Clients"); + + migrationBuilder.DropTable( + name: "RefreshTokens"); + + migrationBuilder.DropIndex( + name: "IX_Shipments_RecipientClientId", + table: "Shipments"); + + migrationBuilder.DropIndex( + name: "IX_Shipments_SenderId", + table: "Shipments"); + + migrationBuilder.DropIndex( + name: "IX_ShipmentCheckpoints_HandledByDriverId", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropIndex( + name: "IX_ShipmentCheckpoints_LoadedOntoTruckId", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "Color", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "CurrentOdometerKm", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "EngineNumber", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "InsuranceExpiration", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "InsurancePolicy", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "LastMaintenanceDate", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "NextMaintenanceDate", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "VerificationExpiration", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "VerificationNumber", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "Vin", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "Year", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "RecipientClientId", + table: "Shipments"); + + migrationBuilder.DropColumn( + name: "SenderId", + table: "Shipments"); + + migrationBuilder.DropColumn( + name: "ActionType", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "HandledByDriverId", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "LoadedOntoTruckId", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "NewCustodian", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "PreviousCustodian", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "Curp", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "EmergencyContact", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "EmergencyPhone", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "HireDate", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "LicenseExpiration", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "LicenseType", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "Nss", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "Rfc", + table: "Drivers"); + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs new file mode 100644 index 0000000..81df1f3 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs @@ -0,0 +1,1502 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + partial class ParhelionDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PreferredProductTypes") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TaxId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "CompanyName"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Curp") + .HasColumnType("text"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmergencyContact") + .HasColumnType("text"); + + b.Property("EmergencyPhone") + .HasColumnType("text"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LicenseExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LicenseType") + .HasColumnType("text"); + + b.Property("Nss") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Rfc") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientClientId") + .HasColumnType("uuid"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("RecipientClientId"); + + b.HasIndex("SenderId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HandledByDriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LoadedOntoTruckId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("NewCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PreviousCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("HandledByDriverId"); + + b.HasIndex("LoadedOntoTruckId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentOdometerKm") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineNumber") + .HasColumnType("text"); + + b.Property("InsuranceExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("InsurancePolicy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NextMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationNumber") + .HasColumnType("text"); + + b.Property("Vin") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Drivers") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Client", "RecipientClient") + .WithMany("ShipmentsAsRecipient") + .HasForeignKey("RecipientClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Client", "Sender") + .WithMany("ShipmentsAsSender") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("RecipientClient"); + + b.Navigation("Sender"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "HandledByDriver") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByDriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") + .WithMany("LoadedCheckpoints") + .HasForeignKey("LoadedOntoTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("HandledByDriver"); + + b.Navigation("LoadedOntoTruck"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Navigation("ShipmentsAsRecipient"); + + b.Navigation("ShipmentsAsSender"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("HandledCheckpoints"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("LoadedCheckpoints"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Driver"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs b/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs new file mode 100644 index 0000000..41acaf7 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs @@ -0,0 +1,163 @@ +using Microsoft.EntityFrameworkCore; +using Parhelion.Domain.Common; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data; + +/// +/// DbContext principal de Parhelion Logistics. +/// Implementa Query Filters globales para: +/// - Multi-tenancy: Todas las entidades TenantEntity filtran por TenantId +/// - Soft Delete: Todas las entidades filtran por IsDeleted = false +/// +public class ParhelionDbContext : DbContext +{ + private readonly Guid? _tenantId; + + public ParhelionDbContext(DbContextOptions options) + : base(options) + { + } + + /// + /// Constructor con tenant ID para multi-tenancy. + /// El TenantId se inyecta desde el middleware/servicio de autenticación. + /// + public ParhelionDbContext(DbContextOptions options, Guid? tenantId) + : base(options) + { + _tenantId = tenantId; + } + + // ========== DbSets ========== + + // Core + public DbSet Tenants => Set(); + public DbSet Users => Set(); + public DbSet Roles => Set(); + public DbSet RefreshTokens => Set(); + + // Clientes (remitentes/destinatarios) + public DbSet Clients => Set(); + + // Flotilla + public DbSet Drivers => Set(); + public DbSet Trucks => Set(); + public DbSet FleetLogs => Set(); + + // Red Logística + public DbSet Locations => Set(); + public DbSet NetworkLinks => Set(); + public DbSet RouteBlueprints => Set(); + public DbSet RouteSteps => Set(); + + // Envíos + public DbSet Shipments => Set(); + public DbSet ShipmentItems => Set(); + public DbSet ShipmentCheckpoints => Set(); + public DbSet ShipmentDocuments => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Aplicar configuraciones de Fluent API desde el assembly + modelBuilder.ApplyConfigurationsFromAssembly(typeof(ParhelionDbContext).Assembly); + + // ========== QUERY FILTERS GLOBALES ========== + + // Soft Delete filter para TODAS las entidades que heredan de BaseEntity + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType)) + { + var method = typeof(ParhelionDbContext) + .GetMethod(nameof(SetSoftDeleteFilter), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)? + .MakeGenericMethod(entityType.ClrType); + + method?.Invoke(null, new object[] { modelBuilder }); + } + } + + // Multi-Tenant filter para entidades TenantEntity + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + if (typeof(TenantEntity).IsAssignableFrom(entityType.ClrType) && !entityType.IsOwned()) + { + var method = typeof(ParhelionDbContext) + .GetMethod(nameof(SetTenantFilter), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)? + .MakeGenericMethod(entityType.ClrType); + + method?.Invoke(this, new object[] { modelBuilder }); + } + } + } + + /// + /// Aplica filtro de Soft Delete a una entidad. + /// + private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) + where TEntity : BaseEntity + { + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + } + + /// + /// Aplica filtro de Multi-Tenant a una entidad. + /// + private void SetTenantFilter(ModelBuilder modelBuilder) + where TEntity : TenantEntity + { + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted && (_tenantId == null || e.TenantId == _tenantId)); + } + + /// + /// Override de SaveChanges para Audit Trail automático. + /// + public override int SaveChanges() + { + UpdateAuditFields(); + return base.SaveChanges(); + } + + /// + /// Override de SaveChangesAsync para Audit Trail automático. + /// + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + UpdateAuditFields(); + return base.SaveChangesAsync(cancellationToken); + } + + /// + /// Actualiza automáticamente CreatedAt, UpdatedAt, DeletedAt. + /// + private void UpdateAuditFields() + { + var entries = ChangeTracker.Entries(); + var now = DateTime.UtcNow; + + foreach (var entry in entries) + { + switch (entry.State) + { + case EntityState.Added: + entry.Entity.CreatedAt = now; + entry.Entity.IsDeleted = false; + break; + + case EntityState.Modified: + entry.Entity.UpdatedAt = now; + + // Si se está eliminando lógicamente + if (entry.Entity.IsDeleted && entry.Entity.DeletedAt == null) + { + entry.Entity.DeletedAt = now; + } + break; + } + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/SeedData.cs b/backend/src/Parhelion.Infrastructure/Data/SeedData.cs new file mode 100644 index 0000000..e128287 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/SeedData.cs @@ -0,0 +1,195 @@ +using Microsoft.EntityFrameworkCore; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Data; + +/// +/// Seed Data para inicialización de la base de datos. +/// Incluye roles del sistema y opcionalmente un tenant demo. +/// +public static class SeedData +{ + // IDs fijos para roles del sistema (nunca cambian) + public static readonly Guid AdminRoleId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + public static readonly Guid DriverRoleId = Guid.Parse("22222222-2222-2222-2222-222222222222"); + public static readonly Guid DemoUserRoleId = Guid.Parse("33333333-3333-3333-3333-333333333333"); + public static readonly Guid WarehouseRoleId = Guid.Parse("44444444-4444-4444-4444-444444444444"); + + /// + /// Inicializa la base de datos con seed data. + /// Es seguro llamar múltiples veces (idempotente). + /// + public static async Task InitializeAsync(ParhelionDbContext context) + { + await SeedRolesAsync(context); + await context.SaveChangesAsync(); + } + + /// + /// Seed de roles del sistema con IDs fijos. + /// Admin, Driver, Warehouse, DemoUser. + /// + private static async Task SeedRolesAsync(ParhelionDbContext context) + { + var roles = new[] + { + new Role + { + Id = AdminRoleId, + Name = "Admin", + Description = "Gerente de Tráfico - Acceso total al sistema" + }, + new Role + { + Id = DriverRoleId, + Name = "Driver", + Description = "Chofer - Solo ve sus envíos asignados" + }, + new Role + { + Id = WarehouseRoleId, + Name = "Warehouse", + Description = "Almacenista - Gestiona carga y descarga de camiones" + }, + new Role + { + Id = DemoUserRoleId, + Name = "DemoUser", + Description = "Usuario de demostración temporal (24-48h)" + } + }; + + foreach (var role in roles) + { + var existingRole = await context.Roles + .IgnoreQueryFilters() + .FirstOrDefaultAsync(r => r.Id == role.Id); + + if (existingRole == null) + { + context.Roles.Add(role); + } + } + } + + /// + /// Crea un tenant demo con datos de prueba. + /// Usado para la Demo Pública del portafolio. + /// + public static async Task CreateDemoTenantAsync(ParhelionDbContext context, string companyName = "Demo Company") + { + var tenant = new Tenant + { + Id = Guid.NewGuid(), + CompanyName = companyName, + ContactEmail = "demo@parhelion.lat", + FleetSize = 5, + DriverCount = 3, + IsActive = true + }; + + context.Tenants.Add(tenant); + await context.SaveChangesAsync(); + + // Seed ubicaciones demo + await SeedDemoLocationsAsync(context, tenant.Id); + + // Seed camiones demo + await SeedDemoTrucksAsync(context, tenant.Id); + + return tenant; + } + + private static async Task SeedDemoLocationsAsync(ParhelionDbContext context, Guid tenantId) + { + var locations = new[] + { + new Location + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Code = "MTY", + Name = "CEDIS Monterrey", + Type = LocationType.RegionalHub, + FullAddress = "Av. Eugenio Garza Sada 2501, Tecnológico, Monterrey, N.L.", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true + }, + new Location + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Code = "GDL", + Name = "Hub Guadalajara", + Type = LocationType.CrossDock, + FullAddress = "Av. Patria 1501, Zapopan, Jalisco", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true + }, + new Location + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Code = "CDMX", + Name = "Almacén Ciudad de México", + Type = LocationType.Warehouse, + FullAddress = "Calle Río Churubusco 350, Iztapalapa, CDMX", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true + } + }; + + context.Locations.AddRange(locations); + await context.SaveChangesAsync(); + } + + private static async Task SeedDemoTrucksAsync(ParhelionDbContext context, Guid tenantId) + { + var trucks = new[] + { + new Truck + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Plate = "NL-001-X", + Model = "Kenworth T680", + Type = TruckType.DryBox, + MaxCapacityKg = 25000, + MaxVolumeM3 = 80, + IsActive = true + }, + new Truck + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Plate = "NL-002-R", + Model = "Freightliner Cascadia", + Type = TruckType.Refrigerated, + MaxCapacityKg = 20000, + MaxVolumeM3 = 60, + IsActive = true + }, + new Truck + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Plate = "NL-003-H", + Model = "Peterbilt 579", + Type = TruckType.HazmatTank, + MaxCapacityKg = 30000, + MaxVolumeM3 = 40, + IsActive = true + } + }; + + context.Trucks.AddRange(trucks); + await context.SaveChangesAsync(); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Parhelion.Infrastructure.csproj b/backend/src/Parhelion.Infrastructure/Parhelion.Infrastructure.csproj index fa71b7a..9ba2965 100644 --- a/backend/src/Parhelion.Infrastructure/Parhelion.Infrastructure.csproj +++ b/backend/src/Parhelion.Infrastructure/Parhelion.Infrastructure.csproj @@ -6,4 +6,15 @@ enable + + + + + + + + + + + diff --git a/database-schema.md b/database-schema.md index 81329f3..f847f56 100644 --- a/database-schema.md +++ b/database-schema.md @@ -50,6 +50,14 @@ erDiagram %% ========== HISTÓRICOS ========== DRIVER ||--o{ FLEET_LOG : "historial cambios vehículo" + %% ========== CLIENTES Y AUTH (v0.4.2) ========== + TENANT ||--o{ CLIENT : "tiene clientes" + CLIENT ||--o{ SHIPMENT : "envía como remitente" + CLIENT ||--o{ SHIPMENT : "recibe como destinatario" + USER ||--o{ REFRESH_TOKEN : "tiene tokens" + DRIVER ||--o{ SHIPMENT_CHECKPOINT : "maneja paquetes" + TRUCK ||--o{ SHIPMENT_CHECKPOINT : "carga paquetes" + %% ========== ENTIDADES CORE ========== TENANT { uuid id PK @@ -90,6 +98,14 @@ erDiagram uuid default_truck_id FK "nullable - asignación fija" uuid current_truck_id FK "nullable - camión actual" string status "Available|OnRoute|Inactive" + string rfc "nullable - RFC fiscal" + string nss "nullable - Número de Seguro Social" + string curp "nullable - CURP" + string license_type "nullable - A|B|C|D|E" + datetime license_expiration "nullable" + string emergency_contact "nullable" + string emergency_phone "nullable" + datetime hire_date "nullable - Fecha de contratación" datetime created_at } @@ -102,6 +118,17 @@ erDiagram decimal max_capacity_kg decimal max_volume_m3 boolean is_active + string vin "nullable - VIN" + string engine_number "nullable" + int year "nullable - Año del vehículo" + string color "nullable" + string insurance_policy "nullable - Número de póliza" + datetime insurance_expiration "nullable" + string verification_number "nullable - Verificación vehicular" + datetime verification_expiration "nullable" + datetime last_maintenance_date "nullable" + datetime next_maintenance_date "nullable" + decimal current_odometer_km "nullable" datetime created_at } @@ -203,6 +230,36 @@ erDiagram datetime expires_at "nullable - para documentos temporales" } + %% ========== CLIENTES (v0.4.2) ========== + CLIENT { + uuid id PK + uuid tenant_id FK + string company_name "Nombre de la empresa" + string trade_name "nullable - Nombre comercial" + string contact_name "Contacto principal" + string email + string phone + string tax_id "nullable - RFC" + string legal_name "nullable - Razón Social" + string billing_address "nullable" + string shipping_address "Dirección de envío" + string preferred_product_types "nullable - Tipos de productos" + string priority "Normal|Low|High|Urgent" + boolean is_active + } + + REFRESH_TOKEN { + uuid id PK + uuid user_id FK + string token_hash "Hash del token (nunca texto plano)" + datetime expires_at + boolean is_revoked + datetime revoked_at "nullable" + string revoked_reason "nullable" + string created_from_ip "nullable" + string user_agent "nullable" + } + %% ========== ENRUTAMIENTO (HUB & SPOKE) ========== ROUTE_BLUEPRINT { uuid id PK @@ -1202,4 +1259,174 @@ public class Driver --- -**Siguiente Paso:** Usar este esquema para generar las migraciones de Entity Framework Core con `dotnet ef migrations add InitialCreate`. +## 12. Metodología de Implementación (Detalles Técnicos) + +> **Estado:** ✅ Implementado en v0.4.0 + v0.4.1 + +### 12.1 Tecnologías Utilizadas + +| Componente | Tecnología | Versión | +| --------------------- | ------------------------------------- | ----------- | +| ORM | Entity Framework Core | 8.0.10 | +| Database Provider | Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.10 | +| Base de Datos | PostgreSQL | 17 (Docker) | +| Contenedor | postgres_db (Docker Compose) | Up 2 months | +| Entorno de Desarrollo | parhelion_dev | Created | +| Entorno de Producción | parhelion_prod | Pendiente | + +### 12.2 Naming Convention + +``` +┌───────────────────────────────────────────────────────────────┐ +│ NAMING CONVENTION │ +├───────────────────────────────────────────────────────────────┤ +│ C# Entity Classes → PascalCase (e.g., ShipmentItem) │ +│ PostgreSQL Tables → PascalCase (preservado por EF) │ +│ PostgreSQL Columns → PascalCase (e.g., "TenantId") │ +│ Indexes → IX_TableName_ColumnName │ +│ Foreign Keys → FK_TableName_RelatedTable_Column │ +└───────────────────────────────────────────────────────────────┘ +``` + +**Nota:** PostgreSQL es case-sensitive cuando usa comillas dobles. EF Core automáticamente genera nombres con comillas, por ejemplo: `"IsDelayed"`. + +### 12.3 Arquitectura de Capas + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Parhelion.API │ +│ ├── Program.cs (DI, Middleware, Endpoints) │ +│ ├── appsettings.json (Connection Strings) │ +│ └── Controllers/ (futuro) │ +├─────────────────────────────────────────────────────────────┤ +│ Parhelion.Application │ +│ ├── Services/ (Business Logic - futuro) │ +│ └── DTOs/ (Data Transfer Objects - futuro) │ +├─────────────────────────────────────────────────────────────┤ +│ Parhelion.Domain │ +│ ├── Common/ │ +│ │ └── BaseEntity.cs, TenantEntity.cs │ +│ ├── Entities/ (14 entidades) │ +│ │ └── Tenant, User, Role, Driver, Truck, Location... │ +│ └── Enums/ (11 enumeraciones) │ +│ └── ShipmentStatus, TruckType, LocationType... │ +├─────────────────────────────────────────────────────────────┤ +│ Parhelion.Infrastructure │ +│ ├── Data/ │ +│ │ ├── ParhelionDbContext.cs │ +│ │ ├── SeedData.cs │ +│ │ ├── Configurations/ (14 IEntityTypeConfiguration) │ +│ │ └── Migrations/ │ +│ │ └── 20251213001913_InitialCreate.cs │ +│ └── Services/ (Repositories - futuro) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 12.4 Query Filters Globales + +El `ParhelionDbContext` implementa filtros automáticos aplicados a **todas** las consultas: + +```csharp +// Soft Delete: Excluye registros eliminados +modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + +// Multi-Tenancy: Filtra por tenant del usuario actual +modelBuilder.Entity().HasQueryFilter(e => + !e.IsDeleted && (_tenantId == null || e.TenantId == _tenantId) +); +``` + +**Beneficios:** + +- ✅ SQL Injection Prevention: Queries siempre parameterizadas +- ✅ Tenant Isolation: Datos nunca se mezclan entre clientes +- ✅ Soft Delete: Datos nunca se pierden, solo se marcan + +### 12.5 Audit Trail Automático + +```csharp +public override int SaveChanges() +{ + var entries = ChangeTracker.Entries(); + var now = DateTime.UtcNow; + + foreach (var entry in entries) + { + switch (entry.State) + { + case EntityState.Added: + entry.Entity.CreatedAt = now; + entry.Entity.IsDeleted = false; + break; + + case EntityState.Modified: + entry.Entity.UpdatedAt = now; + if (entry.Entity.IsDeleted && entry.Entity.DeletedAt == null) + entry.Entity.DeletedAt = now; + break; + } + } + return base.SaveChanges(); +} +``` + +### 12.6 Migración Aplicada + +```bash +# Generar migración +dotnet ef migrations add InitialCreate \ + --project src/Parhelion.Infrastructure \ + --startup-project src/Parhelion.API \ + --output-dir Data/Migrations + +# Aplicar a PostgreSQL +dotnet ef database update \ + --project src/Parhelion.Infrastructure \ + --startup-project src/Parhelion.API +``` + +**Resultado:** 14 tablas + 1 tabla de migraciones creadas: + +- `Tenants`, `Users`, `Roles` +- `Drivers`, `Trucks`, `FleetLogs` +- `Locations`, `NetworkLinks`, `RouteBlueprints`, `RouteSteps` +- `Shipments`, `ShipmentItems`, `ShipmentCheckpoints`, `ShipmentDocuments` +- `__EFMigrationsHistory` + +### 12.7 Seed Data + +Roles del sistema con IDs fijos (idempotente): + +| Role ID | Name | Description | +| ------------------------------------ | --------- | --------------------------------- | +| 11111111-1111-1111-1111-111111111111 | Admin | Gerente de Tráfico - Acceso total | +| 22222222-2222-2222-2222-222222222222 | Driver | Chofer - Solo sus envíos | +| 33333333-3333-3333-3333-333333333333 | DemoUser | Usuario de demostración | +| 44444444-4444-4444-4444-444444444444 | Warehouse | Almacenista - Carga/Descarga | + +### 12.8 Connection String + +```json +// appsettings.json (desarrollo) +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=parhelion_dev;Username=MetaCodeX;Password=***" + } +} +``` + +```yaml +# docker-compose.yml (producción) +environment: + - ConnectionStrings__DefaultConnection=Host=postgres;Database=parhelion_db;Username=parhelion_user;Password=${DB_PASSWORD} +``` + +--- + +**Estado de Implementación:** + +- ✅ Domain Layer completo (14 entidades, 11 enums) +- ✅ Infrastructure Layer completo (DbContext, Configurations, Migrations) +- ✅ Base de datos creada y tablas verificadas +- ⏳ API Endpoints CRUD (próximo) +- ⏳ Autenticación JWT (próximo) From 2250a6c6c0bb8251393d1af740d2ae8efc880d9f Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sat, 13 Dec 2025 20:06:45 +0000 Subject: [PATCH 11/34] feat(v0.4.3): Employee Layer - Centralizacion de datos de empleado NUEVAS ENTIDADES: - Employee: Datos legales (RFC, NSS, CURP) centralizados para todos los roles - Shift: Turnos de trabajo con horarios y dias de semana - WarehouseZone: Zonas funcionales de bodega (Receiving, Storage, ColdChain, Hazmat) - WarehouseOperator: Extension de Employee para almacenistas MODIFICACIONES: - User: Agregado flag IsSuperAdmin, navigation a Employee - Driver: Refactorizado de TenantEntity a BaseEntity, UserId a EmployeeId - Location: Agregadas collections Zones y AssignedWarehouseOperators - ShipmentCheckpoint: Agregado HandledByWarehouseOperatorId - Permission.cs: 20 nuevos permisos (Employees, Shifts, Zones, Operators, Tenants) - SeedData: Agregado rol SystemAdmin TESTS E2E (8 tests, 100% passed): - FullSystemE2ETest: Flujo completo desde SuperAdmin hasta entrega final - Cobertura de las 19 tablas del sistema - Tests usan InMemory DB, no afectan datos de produccion INFRASTRUCTURE: - Migracion AddEmployeeLayerV043 aplicada a PostgreSQL - GitHub Actions: Tests de integracion con upload de resultados - database-schema.md: Actualizado a v2.4 con nuevas entidades en diagrama ER --- .github/workflows/ci.yml | 12 +- CHANGELOG.md | 57 + backend/Parhelion.sln | 9 + .../src/Parhelion.Domain/Entities/Driver.cs | 43 +- .../src/Parhelion.Domain/Entities/Employee.cs | 58 + .../src/Parhelion.Domain/Entities/Location.cs | 6 + .../src/Parhelion.Domain/Entities/Shift.cs | 33 + .../Entities/ShipmentCheckpoint.cs | 4 + backend/src/Parhelion.Domain/Entities/User.cs | 10 +- .../Entities/WarehouseOperator.cs | 25 + .../Entities/WarehouseZone.cs | 30 + .../src/Parhelion.Domain/Enums/Permission.cs | 32 +- .../Enums/WarehouseZoneType.cs | 25 + .../Configurations/DriverConfiguration.cs | 35 +- .../Configurations/EmployeeConfiguration.cs | 59 + .../Data/Configurations/ShiftConfiguration.cs | 30 + .../WarehouseOperatorConfiguration.cs | 34 + .../WarehouseZoneConfiguration.cs | 30 + ...213194319_AddEmployeeLayerV043.Designer.cs | 1792 +++++++++++++++++ .../20251213194319_AddEmployeeLayerV043.cs | 464 +++++ .../ParhelionDbContextModelSnapshot.cs | 353 +++- .../Data/ParhelionDbContext.cs | 8 + .../Parhelion.Infrastructure/Data/SeedData.cs | 7 + .../EmployeeLayerIntegrationTests.cs | 482 +++++ .../Parhelion.Tests/FullSystemE2ETest.cs | 942 +++++++++ backend/tests/Parhelion.Tests/GlobalUsings.cs | 1 + .../Parhelion.Tests/Parhelion.Tests.csproj | 33 + database-schema.md | 230 ++- 28 files changed, 4734 insertions(+), 110 deletions(-) create mode 100644 backend/src/Parhelion.Domain/Entities/Employee.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Shift.cs create mode 100644 backend/src/Parhelion.Domain/Entities/WarehouseOperator.cs create mode 100644 backend/src/Parhelion.Domain/Entities/WarehouseZone.cs create mode 100644 backend/src/Parhelion.Domain/Enums/WarehouseZoneType.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/EmployeeConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/ShiftConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/WarehouseOperatorConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/WarehouseZoneConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251213194319_AddEmployeeLayerV043.Designer.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251213194319_AddEmployeeLayerV043.cs create mode 100644 backend/tests/Parhelion.Tests/EmployeeLayerIntegrationTests.cs create mode 100644 backend/tests/Parhelion.Tests/FullSystemE2ETest.cs create mode 100644 backend/tests/Parhelion.Tests/GlobalUsings.cs create mode 100644 backend/tests/Parhelion.Tests/Parhelion.Tests.csproj diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab4c702..790cada 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,16 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal || true + # E2E Integration Tests - Validates entire system flow + - name: Run Integration Tests + run: dotnet test --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: backend/tests/**/test-results.trx # ===== FRONTEND ADMIN (Angular) ===== frontend-admin: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3328692..6b7f924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,63 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.4.3] - 2025-12-13 + +### Agregado + +- **Employee Layer (Centralización de Datos de Empleado)**: + + - Nueva entidad `Employee` con datos legales (RFC, NSS, CURP) + - Contacto de emergencia, fecha de contratación, departamento + - Relación 1:1 con `User` (usuario del sistema) + +- **Sistema de Turnos (`Shift`)**: + + - Nuevo registro de turnos de trabajo por tenant + - Campos: StartTime, EndTime, DaysOfWeek + - Asignación opcional a empleados + +- **Zonas de Bodega (`WarehouseZone`)**: + + - Divisiones internas de ubicaciones (Receiving, Storage, ColdChain, etc.) + - Enum `WarehouseZoneType` con 6 tipos de zona + - Asignación a operadores de almacén + +- **Extensión WarehouseOperator**: + + - Similar a Driver pero para almacenistas + - Ubicación asignada, zona primaria + - FK en `ShipmentCheckpoint.HandledByWarehouseOperatorId` + +- **Super Admin (IsSuperAdmin)**: + + - Flag en `User` para administradores del sistema + - Correo format: `nombre@parhelion.com` + - Nuevo rol `SystemAdmin` en SeedData + +- **20 Nuevos Permisos**: + + - Employees: Read, Create, Update, Delete + - Shifts: Read, Create, Update, Delete + - WarehouseZones: Read, Create, Update, Delete + - WarehouseOperators: Read, Create, Update, Delete + - Tenants: Read, Create, Update, Deactivate + +### Modificado + +- `Driver`: Refactorizado de `TenantEntity` a `BaseEntity` + - `UserId` → `EmployeeId` (datos legales movidos a Employee) +- `User`: Agregado `IsSuperAdmin`, `Employee` navigation +- `Location`: Agregado `Zones` y `AssignedWarehouseOperators` +- `ShipmentCheckpoint`: Agregado `HandledByWarehouseOperatorId` + +### Tests + +- 7 tests de integración E2E para Employee Layer +- Cobertura: Tenant, User, Employee, Driver, WarehouseOperator, Shift, Checkpoint + +--- + ## [0.4.2] - 2025-12-13 ### Agregado diff --git a/backend/Parhelion.sln b/backend/Parhelion.sln index ec3ca27..de00bfd 100644 --- a/backend/Parhelion.sln +++ b/backend/Parhelion.sln @@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parhelion.Infrastructure", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parhelion.API", "src\Parhelion.API\Parhelion.API.csproj", "{7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{20CC5A12-6001-4C06-87DA-DE72502712C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parhelion.Tests", "tests\Parhelion.Tests\Parhelion.Tests.csproj", "{59145160-762F-4941-8B1D-429BEE5DB8FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,11 +42,16 @@ Global {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}.Debug|Any CPU.Build.0 = Debug|Any CPU {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}.Release|Any CPU.ActiveCfg = Release|Any CPU {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02}.Release|Any CPU.Build.0 = Release|Any CPU + {59145160-762F-4941-8B1D-429BEE5DB8FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59145160-762F-4941-8B1D-429BEE5DB8FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59145160-762F-4941-8B1D-429BEE5DB8FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59145160-762F-4941-8B1D-429BEE5DB8FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {9CE90642-26E9-41D1-A0FC-E221B0926E21} = {0DB876F5-F853-4A2B-B99D-B70B16F3B8DD} {145355DE-4C48-467D-8E8E-300BADDA0427} = {0DB876F5-F853-4A2B-B99D-B70B16F3B8DD} {6DE6AD38-2121-4375-BA34-37389D4E9675} = {0DB876F5-F853-4A2B-B99D-B70B16F3B8DD} {7DFDCA95-1B96-42EB-9CFA-3CC2A9572E02} = {0DB876F5-F853-4A2B-B99D-B70B16F3B8DD} + {59145160-762F-4941-8B1D-429BEE5DB8FA} = {20CC5A12-6001-4C06-87DA-DE72502712C2} EndGlobalSection EndGlobal diff --git a/backend/src/Parhelion.Domain/Entities/Driver.cs b/backend/src/Parhelion.Domain/Entities/Driver.cs index f29ea95..6eb2650 100644 --- a/backend/src/Parhelion.Domain/Entities/Driver.cs +++ b/backend/src/Parhelion.Domain/Entities/Driver.cs @@ -4,26 +4,16 @@ namespace Parhelion.Domain.Entities; /// -/// Chofer de la flotilla con asignación híbrida de camiones. -/// - DefaultTruckId: Camión fijo asignado ("su unidad") -/// - CurrentTruckId: Camión que conduce actualmente (puede diferir) +/// Extensión de Employee para choferes. +/// Contiene datos específicos de licencia de conducir y asignación de camiones. +/// Los datos legales (RFC, NSS, CURP, etc.) están en Employee. /// -public class Driver : TenantEntity +public class Driver : BaseEntity { - public Guid UserId { get; set; } - public string FullName { get; set; } = null!; - public string Phone { get; set; } = null!; + /// FK a Employee (datos de empleado) + public Guid EmployeeId { get; set; } - // ========== DATOS LEGALES ========== - - /// RFC del chofer para nómina - public string? Rfc { get; set; } - - /// Número de Seguro Social (IMSS) - public string? Nss { get; set; } - - /// CURP del chofer - public string? Curp { get; set; } + // ========== DATOS DE LICENCIA ========== /// Número de licencia de conducir public string LicenseNumber { get; set; } = null!; @@ -34,19 +24,6 @@ public class Driver : TenantEntity /// Fecha de vencimiento de la licencia public DateTime? LicenseExpiration { get; set; } - // ========== CONTACTO DE EMERGENCIA ========== - - /// Nombre del contacto de emergencia - public string? EmergencyContact { get; set; } - - /// Teléfono del contacto de emergencia - public string? EmergencyPhone { get; set; } - - // ========== INFORMACIÓN LABORAL ========== - - /// Fecha de contratación - public DateTime? HireDate { get; set; } - // ========== ASIGNACIÓN DE CAMIONES ========== /// @@ -62,9 +39,9 @@ public class Driver : TenantEntity public DriverStatus Status { get; set; } - // Navigation Properties - public Tenant Tenant { get; set; } = null!; - public User User { get; set; } = null!; + // ========== NAVIGATION PROPERTIES ========== + + public Employee Employee { get; set; } = null!; public Truck? DefaultTruck { get; set; } public Truck? CurrentTruck { get; set; } public ICollection Shipments { get; set; } = new List(); diff --git a/backend/src/Parhelion.Domain/Entities/Employee.cs b/backend/src/Parhelion.Domain/Entities/Employee.cs new file mode 100644 index 0000000..6ad8b7d --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Employee.cs @@ -0,0 +1,58 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Datos de empleado para todos los roles (Admin, Driver, Warehouse). +/// Vinculado 1:1 con User cuando es empleado del tenant. +/// Centraliza datos legales que antes solo tenía Driver. +/// +public class Employee : TenantEntity +{ + public Guid UserId { get; set; } + + /// Teléfono personal del empleado + public string Phone { get; set; } = null!; + + // ========== DATOS LEGALES (México) ========== + + /// RFC para nómina/facturación + public string? Rfc { get; set; } + + /// Número de Seguro Social (IMSS) + public string? Nss { get; set; } + + /// Clave Única de Registro de Población + public string? Curp { get; set; } + + // ========== CONTACTO DE EMERGENCIA ========== + + /// Nombre del contacto de emergencia + public string? EmergencyContact { get; set; } + + /// Teléfono del contacto de emergencia + public string? EmergencyPhone { get; set; } + + // ========== INFORMACIÓN LABORAL ========== + + /// Fecha de contratación + public DateTime? HireDate { get; set; } + + /// Turno asignado (nullable) + public Guid? ShiftId { get; set; } + + /// Departamento: Admin, Operations, Field + public string? Department { get; set; } + + // ========== NAVIGATION PROPERTIES ========== + + public Tenant Tenant { get; set; } = null!; + public User User { get; set; } = null!; + public Shift? Shift { get; set; } + + /// Extensión Driver (si es chofer) + public Driver? Driver { get; set; } + + /// Extensión WarehouseOperator (si es almacenista) + public WarehouseOperator? WarehouseOperator { get; set; } +} diff --git a/backend/src/Parhelion.Domain/Entities/Location.cs b/backend/src/Parhelion.Domain/Entities/Location.cs index bdaacf9..5a667bd 100644 --- a/backend/src/Parhelion.Domain/Entities/Location.cs +++ b/backend/src/Parhelion.Domain/Entities/Location.cs @@ -39,4 +39,10 @@ public class Location : TenantEntity public ICollection RouteSteps { get; set; } = new List(); public ICollection OutgoingLinks { get; set; } = new List(); public ICollection IncomingLinks { get; set; } = new List(); + + /// Zonas internas de la bodega (si aplica) + public ICollection Zones { get; set; } = new List(); + + /// Almacenistas asignados a esta ubicación + public ICollection AssignedWarehouseOperators { get; set; } = new List(); } diff --git a/backend/src/Parhelion.Domain/Entities/Shift.cs b/backend/src/Parhelion.Domain/Entities/Shift.cs new file mode 100644 index 0000000..c748b27 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Shift.cs @@ -0,0 +1,33 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Turno de trabajo para empleados. +/// Permite asignar horarios predefinidos a los empleados del tenant. +/// +public class Shift : TenantEntity +{ + /// Nombre del turno (Matutino, Vespertino, Nocturno) + public string Name { get; set; } = null!; + + /// Hora de inicio del turno + public TimeOnly StartTime { get; set; } + + /// Hora de fin del turno + public TimeOnly EndTime { get; set; } + + /// + /// Días de la semana en que aplica el turno. + /// Formato: "Mon,Tue,Wed,Thu,Fri" o "Sat,Sun" + /// + public string DaysOfWeek { get; set; } = "Mon,Tue,Wed,Thu,Fri"; + + /// Si el turno está activo para asignación + public bool IsActive { get; set; } = true; + + // ========== NAVIGATION PROPERTIES ========== + + public Tenant Tenant { get; set; } = null!; + public ICollection Employees { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs b/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs index 50462b1..b7a662a 100644 --- a/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs +++ b/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs @@ -32,11 +32,15 @@ public class ShipmentCheckpoint : BaseEntity /// Nombre del nuevo custodio (quien recibió) public string? NewCustodian { get; set; } + + /// Almacenista que manejó el paquete en este checkpoint + public Guid? HandledByWarehouseOperatorId { get; set; } // Navigation Properties public Shipment Shipment { get; set; } = null!; public Location? Location { get; set; } public User CreatedBy { get; set; } = null!; public Driver? HandledByDriver { get; set; } + public WarehouseOperator? HandledByWarehouseOperator { get; set; } public Truck? LoadedOntoTruck { get; set; } } diff --git a/backend/src/Parhelion.Domain/Entities/User.cs b/backend/src/Parhelion.Domain/Entities/User.cs index 6d9d278..d875fe5 100644 --- a/backend/src/Parhelion.Domain/Entities/User.cs +++ b/backend/src/Parhelion.Domain/Entities/User.cs @@ -25,13 +25,21 @@ public class User : TenantEntity /// public bool UsesArgon2 { get; set; } + /// + /// Super Admin del sistema (TenantId será NULL). + /// Puede crear tenants y asignar administradores. + /// Email format: nombre@parhelion.com + /// + public bool IsSuperAdmin { get; set; } + public DateTime? LastLogin { get; set; } public bool IsActive { get; set; } // Navigation Properties public Tenant Tenant { get; set; } = null!; public Role Role { get; set; } = null!; - public Driver? Driver { get; set; } + /// Datos de empleado (si es empleado del tenant) + public Employee? Employee { get; set; } public ICollection CreatedCheckpoints { get; set; } = new List(); public ICollection CreatedFleetLogs { get; set; } = new List(); public ICollection RefreshTokens { get; set; } = new List(); diff --git a/backend/src/Parhelion.Domain/Entities/WarehouseOperator.cs b/backend/src/Parhelion.Domain/Entities/WarehouseOperator.cs new file mode 100644 index 0000000..6d6a10e --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/WarehouseOperator.cs @@ -0,0 +1,25 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Extensión de Employee para almacenistas. +/// Similar a Driver pero para operadores de bodega. +/// +public class WarehouseOperator : BaseEntity +{ + public Guid EmployeeId { get; set; } + + /// Ubicación (bodega) donde trabaja + public Guid AssignedLocationId { get; set; } + + /// Zona principal de responsabilidad (nullable) + public Guid? PrimaryZoneId { get; set; } + + // ========== NAVIGATION PROPERTIES ========== + + public Employee Employee { get; set; } = null!; + public Location AssignedLocation { get; set; } = null!; + public WarehouseZone? PrimaryZone { get; set; } + public ICollection HandledCheckpoints { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/WarehouseZone.cs b/backend/src/Parhelion.Domain/Entities/WarehouseZone.cs new file mode 100644 index 0000000..ee27093 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/WarehouseZone.cs @@ -0,0 +1,30 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Zona dentro de una ubicación (bodega/almacén). +/// Permite dividir las ubicaciones en áreas funcionales. +/// +public class WarehouseZone : BaseEntity +{ + public Guid LocationId { get; set; } + + /// Código corto de la zona (A1, B2, COLD-1) + public string Code { get; set; } = null!; + + /// Nombre descriptivo de la zona + public string Name { get; set; } = null!; + + /// Tipo funcional de la zona + public WarehouseZoneType Type { get; set; } + + /// Si la zona está activa + public bool IsActive { get; set; } = true; + + // ========== NAVIGATION PROPERTIES ========== + + public Location Location { get; set; } = null!; + public ICollection AssignedOperators { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Enums/Permission.cs b/backend/src/Parhelion.Domain/Enums/Permission.cs index 29686ea..514943d 100644 --- a/backend/src/Parhelion.Domain/Enums/Permission.cs +++ b/backend/src/Parhelion.Domain/Enums/Permission.cs @@ -68,5 +68,35 @@ public enum Permission // ========== FLEET LOGS ========== FleetLogsRead = 1100, - FleetLogsCreate = 1101 + FleetLogsCreate = 1101, + + // ========== EMPLOYEES ========== + EmployeesRead = 1200, + EmployeesCreate = 1201, + EmployeesUpdate = 1202, + EmployeesDelete = 1203, + + // ========== SHIFTS ========== + ShiftsRead = 1300, + ShiftsCreate = 1301, + ShiftsUpdate = 1302, + ShiftsDelete = 1303, + + // ========== WAREHOUSE ZONES ========== + WarehouseZonesRead = 1400, + WarehouseZonesCreate = 1401, + WarehouseZonesUpdate = 1402, + WarehouseZonesDelete = 1403, + + // ========== WAREHOUSE OPERATORS ========== + WarehouseOperatorsRead = 1500, + WarehouseOperatorsCreate = 1501, + WarehouseOperatorsUpdate = 1502, + WarehouseOperatorsDelete = 1503, + + // ========== TENANTS (SuperAdmin only) ========== + TenantsRead = 1600, + TenantsCreate = 1601, + TenantsUpdate = 1602, + TenantsDeactivate = 1603 // No Delete, soft-deactivate } diff --git a/backend/src/Parhelion.Domain/Enums/WarehouseZoneType.cs b/backend/src/Parhelion.Domain/Enums/WarehouseZoneType.cs new file mode 100644 index 0000000..ace5b7f --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/WarehouseZoneType.cs @@ -0,0 +1,25 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipo de zona dentro de una bodega/almacén. +/// +public enum WarehouseZoneType +{ + /// Área de recepción de mercancía + Receiving, + + /// Almacenamiento general + Storage, + + /// Área de preparación para despacho + Staging, + + /// Andén de salida + Shipping, + + /// Cuarto frío / Cadena de frío + ColdChain, + + /// Materiales peligrosos + Hazmat +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/DriverConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/DriverConfiguration.cs index cd4482a..717e6c6 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Configurations/DriverConfiguration.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/DriverConfiguration.cs @@ -10,31 +10,24 @@ public void Configure(EntityTypeBuilder builder) { builder.HasKey(d => d.Id); - builder.Property(d => d.FullName) - .IsRequired() - .HasMaxLength(200); - - builder.Property(d => d.Phone) - .IsRequired() - .HasMaxLength(20); - builder.Property(d => d.LicenseNumber) .IsRequired() .HasMaxLength(50); - - // Índice por tenant y status para dashboard - builder.HasIndex(d => new { d.TenantId, d.Status }); - - // Relaciones - builder.HasOne(d => d.Tenant) - .WithMany(t => t.Drivers) - .HasForeignKey(d => d.TenantId) - .OnDelete(DeleteBehavior.Restrict); - builder.HasOne(d => d.User) - .WithOne(u => u.Driver) - .HasForeignKey(d => d.UserId) - .OnDelete(DeleteBehavior.Restrict); + builder.Property(d => d.LicenseType) + .HasMaxLength(10); + + // Índice único: Un empleado solo puede tener un perfil de driver + builder.HasIndex(d => d.EmployeeId).IsUnique(); + + // Índice por status para dashboard + builder.HasIndex(d => d.Status); + + // Relación 1:1 con Employee + builder.HasOne(d => d.Employee) + .WithOne(e => e.Driver) + .HasForeignKey(d => d.EmployeeId) + .OnDelete(DeleteBehavior.Cascade); builder.HasOne(d => d.DefaultTruck) .WithMany(t => t.DefaultDrivers) diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/EmployeeConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/EmployeeConfiguration.cs new file mode 100644 index 0000000..5aabe50 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/EmployeeConfiguration.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class EmployeeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => e.Id); + + builder.Property(e => e.Phone) + .IsRequired() + .HasMaxLength(20); + + builder.Property(e => e.Rfc) + .HasMaxLength(13); + + builder.Property(e => e.Nss) + .HasMaxLength(11); + + builder.Property(e => e.Curp) + .HasMaxLength(18); + + builder.Property(e => e.EmergencyContact) + .HasMaxLength(200); + + builder.Property(e => e.EmergencyPhone) + .HasMaxLength(20); + + builder.Property(e => e.Department) + .HasMaxLength(50); + + // Índice único: Un usuario solo puede tener un perfil de empleado + builder.HasIndex(e => e.UserId).IsUnique(); + + // Índice por tenant para listados + builder.HasIndex(e => new { e.TenantId, e.Department }); + + // Relación con Tenant + builder.HasOne(e => e.Tenant) + .WithMany() + .HasForeignKey(e => e.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + // Relación 1:1 con User + builder.HasOne(e => e.User) + .WithOne(u => u.Employee) + .HasForeignKey(e => e.UserId) + .OnDelete(DeleteBehavior.Restrict); + + // Relación con Shift (opcional) + builder.HasOne(e => e.Shift) + .WithMany(s => s.Employees) + .HasForeignKey(e => e.ShiftId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShiftConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShiftConfiguration.cs new file mode 100644 index 0000000..dce3058 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShiftConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class ShiftConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(s => s.Id); + + builder.Property(s => s.Name) + .IsRequired() + .HasMaxLength(100); + + builder.Property(s => s.DaysOfWeek) + .IsRequired() + .HasMaxLength(50); + + // Índice por tenant para listados + builder.HasIndex(s => new { s.TenantId, s.IsActive }); + + // Relación con Tenant + builder.HasOne(s => s.Tenant) + .WithMany() + .HasForeignKey(s => s.TenantId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/WarehouseOperatorConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/WarehouseOperatorConfiguration.cs new file mode 100644 index 0000000..a238d25 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/WarehouseOperatorConfiguration.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class WarehouseOperatorConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(w => w.Id); + + // Índice único: Un empleado solo puede tener un perfil de almacenista + builder.HasIndex(w => w.EmployeeId).IsUnique(); + + // Relación 1:1 con Employee + builder.HasOne(w => w.Employee) + .WithOne(e => e.WarehouseOperator) + .HasForeignKey(w => w.EmployeeId) + .OnDelete(DeleteBehavior.Cascade); + + // Relación con Location (ubicación asignada) + builder.HasOne(w => w.AssignedLocation) + .WithMany(l => l.AssignedWarehouseOperators) + .HasForeignKey(w => w.AssignedLocationId) + .OnDelete(DeleteBehavior.Restrict); + + // Relación con WarehouseZone (opcional) + builder.HasOne(w => w.PrimaryZone) + .WithMany(z => z.AssignedOperators) + .HasForeignKey(w => w.PrimaryZoneId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/WarehouseZoneConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/WarehouseZoneConfiguration.cs new file mode 100644 index 0000000..325d828 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/WarehouseZoneConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class WarehouseZoneConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(z => z.Id); + + builder.Property(z => z.Code) + .IsRequired() + .HasMaxLength(20); + + builder.Property(z => z.Name) + .IsRequired() + .HasMaxLength(100); + + // Índice único: código de zona único por ubicación + builder.HasIndex(z => new { z.LocationId, z.Code }).IsUnique(); + + // Relación con Location + builder.HasOne(z => z.Location) + .WithMany(l => l.Zones) + .HasForeignKey(z => z.LocationId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213194319_AddEmployeeLayerV043.Designer.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213194319_AddEmployeeLayerV043.Designer.cs new file mode 100644 index 0000000..2f43125 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213194319_AddEmployeeLayerV043.Designer.cs @@ -0,0 +1,1792 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + [Migration("20251213194319_AddEmployeeLayerV043")] + partial class AddEmployeeLayerV043 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PreferredProductTypes") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TaxId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "CompanyName"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LicenseExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LicenseType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Curp") + .HasMaxLength(18) + .HasColumnType("character varying(18)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Department") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("EmergencyContact") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Nss") + .HasMaxLength(11) + .HasColumnType("character varying(11)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Rfc") + .HasMaxLength(13) + .HasColumnType("character varying(13)"); + + b.Property("ShiftId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ShiftId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Department"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DaysOfWeek") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "IsActive"); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientClientId") + .HasColumnType("uuid"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("RecipientClientId"); + + b.HasIndex("SenderId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HandledByDriverId") + .HasColumnType("uuid"); + + b.Property("HandledByWarehouseOperatorId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LoadedOntoTruckId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("NewCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PreviousCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("HandledByDriverId"); + + b.HasIndex("HandledByWarehouseOperatorId"); + + b.HasIndex("LoadedOntoTruckId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentOdometerKm") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineNumber") + .HasColumnType("text"); + + b.Property("InsuranceExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("InsurancePolicy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NextMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationNumber") + .HasColumnType("text"); + + b.Property("Vin") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("IsSuperAdmin") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedLocationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("PrimaryZoneId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssignedLocationId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("PrimaryZoneId"); + + b.ToTable("WarehouseOperators"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId", "Code") + .IsUnique(); + + b.ToTable("WarehouseZones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", null) + .WithMany("Drivers") + .HasForeignKey("TenantId"); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.HasOne("Parhelion.Domain.Entities.Shift", "Shift") + .WithMany("Employees") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Employee") + .HasForeignKey("Parhelion.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Shift"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Client", "RecipientClient") + .WithMany("ShipmentsAsRecipient") + .HasForeignKey("RecipientClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Client", "Sender") + .WithMany("ShipmentsAsSender") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("RecipientClient"); + + b.Navigation("Sender"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "HandledByDriver") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByDriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseOperator", "HandledByWarehouseOperator") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByWarehouseOperatorId"); + + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") + .WithMany("LoadedCheckpoints") + .HasForeignKey("LoadedOntoTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("HandledByDriver"); + + b.Navigation("HandledByWarehouseOperator"); + + b.Navigation("LoadedOntoTruck"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "AssignedLocation") + .WithMany("AssignedWarehouseOperators") + .HasForeignKey("AssignedLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("WarehouseOperator") + .HasForeignKey("Parhelion.Domain.Entities.WarehouseOperator", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "PrimaryZone") + .WithMany("AssignedOperators") + .HasForeignKey("PrimaryZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedLocation"); + + b.Navigation("Employee"); + + b.Navigation("PrimaryZone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Zones") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Navigation("ShipmentsAsRecipient"); + + b.Navigation("ShipmentsAsSender"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("HandledCheckpoints"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Navigation("Driver"); + + b.Navigation("WarehouseOperator"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("AssignedWarehouseOperators"); + + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("LoadedCheckpoints"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Navigation("HandledCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Navigation("AssignedOperators"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213194319_AddEmployeeLayerV043.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213194319_AddEmployeeLayerV043.cs new file mode 100644 index 0000000..9689467 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251213194319_AddEmployeeLayerV043.cs @@ -0,0 +1,464 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + /// + public partial class AddEmployeeLayerV043 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Drivers_Tenants_TenantId", + table: "Drivers"); + + migrationBuilder.DropForeignKey( + name: "FK_Drivers_Users_UserId", + table: "Drivers"); + + migrationBuilder.DropIndex( + name: "IX_Drivers_TenantId_Status", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "Curp", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "EmergencyContact", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "EmergencyPhone", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "FullName", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "HireDate", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "Nss", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "Phone", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "Rfc", + table: "Drivers"); + + migrationBuilder.RenameColumn( + name: "UserId", + table: "Drivers", + newName: "EmployeeId"); + + migrationBuilder.RenameIndex( + name: "IX_Drivers_UserId", + table: "Drivers", + newName: "IX_Drivers_EmployeeId"); + + migrationBuilder.AddColumn( + name: "IsSuperAdmin", + table: "Users", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "HandledByWarehouseOperatorId", + table: "ShipmentCheckpoints", + type: "uuid", + nullable: true); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "Drivers", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "LicenseType", + table: "Drivers", + type: "character varying(10)", + maxLength: 10, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "Shifts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + StartTime = table.Column(type: "time without time zone", nullable: false), + EndTime = table.Column(type: "time without time zone", nullable: false), + DaysOfWeek = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Shifts", x => x.Id); + table.ForeignKey( + name: "FK_Shifts_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "WarehouseZones", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + LocationId = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Type = table.Column(type: "integer", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WarehouseZones", x => x.Id); + table.ForeignKey( + name: "FK_WarehouseZones_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Employees", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Rfc = table.Column(type: "character varying(13)", maxLength: 13, nullable: true), + Nss = table.Column(type: "character varying(11)", maxLength: 11, nullable: true), + Curp = table.Column(type: "character varying(18)", maxLength: 18, nullable: true), + EmergencyContact = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + EmergencyPhone = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), + HireDate = table.Column(type: "timestamp with time zone", nullable: true), + ShiftId = table.Column(type: "uuid", nullable: true), + Department = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Employees", x => x.Id); + table.ForeignKey( + name: "FK_Employees_Shifts_ShiftId", + column: x => x.ShiftId, + principalTable: "Shifts", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Employees_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Employees_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "WarehouseOperators", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + EmployeeId = table.Column(type: "uuid", nullable: false), + AssignedLocationId = table.Column(type: "uuid", nullable: false), + PrimaryZoneId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WarehouseOperators", x => x.Id); + table.ForeignKey( + name: "FK_WarehouseOperators_Employees_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_WarehouseOperators_Locations_AssignedLocationId", + column: x => x.AssignedLocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_WarehouseOperators_WarehouseZones_PrimaryZoneId", + column: x => x.PrimaryZoneId, + principalTable: "WarehouseZones", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentCheckpoints_HandledByWarehouseOperatorId", + table: "ShipmentCheckpoints", + column: "HandledByWarehouseOperatorId"); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_Status", + table: "Drivers", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_TenantId", + table: "Drivers", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Employees_ShiftId", + table: "Employees", + column: "ShiftId"); + + migrationBuilder.CreateIndex( + name: "IX_Employees_TenantId_Department", + table: "Employees", + columns: new[] { "TenantId", "Department" }); + + migrationBuilder.CreateIndex( + name: "IX_Employees_UserId", + table: "Employees", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shifts_TenantId_IsActive", + table: "Shifts", + columns: new[] { "TenantId", "IsActive" }); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseOperators_AssignedLocationId", + table: "WarehouseOperators", + column: "AssignedLocationId"); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseOperators_EmployeeId", + table: "WarehouseOperators", + column: "EmployeeId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseOperators_PrimaryZoneId", + table: "WarehouseOperators", + column: "PrimaryZoneId"); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseZones_LocationId_Code", + table: "WarehouseZones", + columns: new[] { "LocationId", "Code" }, + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Drivers_Employees_EmployeeId", + table: "Drivers", + column: "EmployeeId", + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Drivers_Tenants_TenantId", + table: "Drivers", + column: "TenantId", + principalTable: "Tenants", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ShipmentCheckpoints_WarehouseOperators_HandledByWarehouseOp~", + table: "ShipmentCheckpoints", + column: "HandledByWarehouseOperatorId", + principalTable: "WarehouseOperators", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Drivers_Employees_EmployeeId", + table: "Drivers"); + + migrationBuilder.DropForeignKey( + name: "FK_Drivers_Tenants_TenantId", + table: "Drivers"); + + migrationBuilder.DropForeignKey( + name: "FK_ShipmentCheckpoints_WarehouseOperators_HandledByWarehouseOp~", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropTable( + name: "WarehouseOperators"); + + migrationBuilder.DropTable( + name: "Employees"); + + migrationBuilder.DropTable( + name: "WarehouseZones"); + + migrationBuilder.DropTable( + name: "Shifts"); + + migrationBuilder.DropIndex( + name: "IX_ShipmentCheckpoints_HandledByWarehouseOperatorId", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropIndex( + name: "IX_Drivers_Status", + table: "Drivers"); + + migrationBuilder.DropIndex( + name: "IX_Drivers_TenantId", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "IsSuperAdmin", + table: "Users"); + + migrationBuilder.DropColumn( + name: "HandledByWarehouseOperatorId", + table: "ShipmentCheckpoints"); + + migrationBuilder.RenameColumn( + name: "EmployeeId", + table: "Drivers", + newName: "UserId"); + + migrationBuilder.RenameIndex( + name: "IX_Drivers_EmployeeId", + table: "Drivers", + newName: "IX_Drivers_UserId"); + + migrationBuilder.AlterColumn( + name: "TenantId", + table: "Drivers", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LicenseType", + table: "Drivers", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(10)", + oldMaxLength: 10, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Curp", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "EmergencyContact", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "EmergencyPhone", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "FullName", + table: "Drivers", + type: "character varying(200)", + maxLength: 200, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "HireDate", + table: "Drivers", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "Nss", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Phone", + table: "Drivers", + type: "character varying(20)", + maxLength: 20, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Rfc", + table: "Drivers", + type: "text", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Drivers_TenantId_Status", + table: "Drivers", + columns: new[] { "TenantId", "Status" }); + + migrationBuilder.AddForeignKey( + name: "FK_Drivers_Tenants_TenantId", + table: "Drivers", + column: "TenantId", + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Drivers_Users_UserId", + table: "Drivers", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs index 81df1f3..3c54208 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs @@ -120,9 +120,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("Curp") - .HasColumnType("text"); - b.Property("CurrentTruckId") .HasColumnType("uuid"); @@ -132,19 +129,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); - b.Property("EmergencyContact") - .HasColumnType("text"); - - b.Property("EmergencyPhone") - .HasColumnType("text"); - - b.Property("FullName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("HireDate") - .HasColumnType("timestamp with time zone"); + b.Property("EmployeeId") + .HasColumnType("uuid"); b.Property("IsDeleted") .HasColumnType("boolean"); @@ -158,10 +144,71 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)"); b.Property("LicenseType") - .HasColumnType("text"); + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Curp") + .HasMaxLength(18) + .HasColumnType("character varying(18)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Department") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("EmergencyContact") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); b.Property("Nss") - .HasColumnType("text"); + .HasMaxLength(11) + .HasColumnType("character varying(11)"); b.Property("Phone") .IsRequired() @@ -169,10 +216,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(20)"); b.Property("Rfc") - .HasColumnType("text"); + .HasMaxLength(13) + .HasColumnType("character varying(13)"); - b.Property("Status") - .HasColumnType("integer"); + b.Property("ShiftId") + .HasColumnType("uuid"); b.Property("TenantId") .HasColumnType("uuid"); @@ -185,16 +233,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("CurrentTruckId"); - - b.HasIndex("DefaultTruckId"); + b.HasIndex("ShiftId"); b.HasIndex("UserId") .IsUnique(); - b.HasIndex("TenantId", "Status"); + b.HasIndex("TenantId", "Department"); - b.ToTable("Drivers"); + b.ToTable("Employees"); }); modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => @@ -544,6 +590,53 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RouteSteps"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DaysOfWeek") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "IsActive"); + + b.ToTable("Shifts"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => { b.Property("Id") @@ -713,6 +806,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("HandledByDriverId") .HasColumnType("uuid"); + b.Property("HandledByWarehouseOperatorId") + .HasColumnType("uuid"); + b.Property("IsDeleted") .HasColumnType("boolean"); @@ -752,6 +848,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("HandledByDriverId"); + b.HasIndex("HandledByWarehouseOperatorId"); + b.HasIndex("LoadedOntoTruckId"); b.HasIndex("LocationId"); @@ -1049,6 +1147,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDemoUser") .HasColumnType("boolean"); + b.Property("IsSuperAdmin") + .HasColumnType("boolean"); + b.Property("LastLogin") .HasColumnType("timestamp with time zone"); @@ -1081,6 +1182,90 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedLocationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("PrimaryZoneId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssignedLocationId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("PrimaryZoneId"); + + b.ToTable("WarehouseOperators"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId", "Code") + .IsUnique(); + + b.ToTable("WarehouseZones"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => { b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") @@ -1104,21 +1289,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("DefaultTruckId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", null) .WithMany("Drivers") + .HasForeignKey("TenantId"); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.HasOne("Parhelion.Domain.Entities.Shift", "Shift") + .WithMany("Employees") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() .HasForeignKey("TenantId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.HasOne("Parhelion.Domain.Entities.User", "User") - .WithOne("Driver") - .HasForeignKey("Parhelion.Domain.Entities.Driver", "UserId") + .WithOne("Employee") + .HasForeignKey("Parhelion.Domain.Entities.Employee", "UserId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.Navigation("CurrentTruck"); - - b.Navigation("DefaultTruck"); + b.Navigation("Shift"); b.Navigation("Tenant"); @@ -1246,6 +1453,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("RouteBlueprint"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => { b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") @@ -1321,6 +1539,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("HandledByDriverId") .OnDelete(DeleteBehavior.SetNull); + b.HasOne("Parhelion.Domain.Entities.WarehouseOperator", "HandledByWarehouseOperator") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByWarehouseOperatorId"); + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") .WithMany("LoadedCheckpoints") .HasForeignKey("LoadedOntoTruckId") @@ -1341,6 +1563,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("HandledByDriver"); + b.Navigation("HandledByWarehouseOperator"); + b.Navigation("LoadedOntoTruck"); b.Navigation("Location"); @@ -1400,6 +1624,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Tenant"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "AssignedLocation") + .WithMany("AssignedWarehouseOperators") + .HasForeignKey("AssignedLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("WarehouseOperator") + .HasForeignKey("Parhelion.Domain.Entities.WarehouseOperator", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "PrimaryZone") + .WithMany("AssignedOperators") + .HasForeignKey("PrimaryZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedLocation"); + + b.Navigation("Employee"); + + b.Navigation("PrimaryZone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Zones") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => { b.Navigation("ShipmentsAsRecipient"); @@ -1416,8 +1677,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Shipments"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Navigation("Driver"); + + b.Navigation("WarehouseOperator"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => { + b.Navigation("AssignedWarehouseOperators"); + b.Navigation("Checkpoints"); b.Navigation("DestinationShipments"); @@ -1429,6 +1699,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("OutgoingLinks"); b.Navigation("RouteSteps"); + + b.Navigation("Zones"); }); modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => @@ -1443,6 +1715,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Steps"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Navigation("Employees"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => { b.Navigation("Documents"); @@ -1492,10 +1769,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("CreatedFleetLogs"); - b.Navigation("Driver"); + b.Navigation("Employee"); b.Navigation("RefreshTokens"); }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Navigation("HandledCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Navigation("AssignedOperators"); + }); #pragma warning restore 612, 618 } } diff --git a/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs b/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs index 41acaf7..a8fe347 100644 --- a/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs +++ b/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs @@ -45,6 +45,14 @@ public ParhelionDbContext(DbContextOptions options, Guid? te public DbSet Trucks => Set(); public DbSet FleetLogs => Set(); + // Empleados y Turnos (v0.4.3) + public DbSet Employees => Set(); + public DbSet Shifts => Set(); + + // Almacén (v0.4.3) + public DbSet WarehouseZones => Set(); + public DbSet WarehouseOperators => Set(); + // Red Logística public DbSet Locations => Set(); public DbSet NetworkLinks => Set(); diff --git a/backend/src/Parhelion.Infrastructure/Data/SeedData.cs b/backend/src/Parhelion.Infrastructure/Data/SeedData.cs index e128287..8892b52 100644 --- a/backend/src/Parhelion.Infrastructure/Data/SeedData.cs +++ b/backend/src/Parhelion.Infrastructure/Data/SeedData.cs @@ -15,6 +15,7 @@ public static class SeedData public static readonly Guid DriverRoleId = Guid.Parse("22222222-2222-2222-2222-222222222222"); public static readonly Guid DemoUserRoleId = Guid.Parse("33333333-3333-3333-3333-333333333333"); public static readonly Guid WarehouseRoleId = Guid.Parse("44444444-4444-4444-4444-444444444444"); + public static readonly Guid SystemAdminRoleId = Guid.Parse("55555555-5555-5555-5555-555555555555"); /// /// Inicializa la base de datos con seed data. @@ -57,6 +58,12 @@ private static async Task SeedRolesAsync(ParhelionDbContext context) Id = DemoUserRoleId, Name = "DemoUser", Description = "Usuario de demostración temporal (24-48h)" + }, + new Role + { + Id = SystemAdminRoleId, + Name = "SystemAdmin", + Description = "Super Admin - Gestiona tenants y administradores (v0.4.3)" } }; diff --git a/backend/tests/Parhelion.Tests/EmployeeLayerIntegrationTests.cs b/backend/tests/Parhelion.Tests/EmployeeLayerIntegrationTests.cs new file mode 100644 index 0000000..9f0e037 --- /dev/null +++ b/backend/tests/Parhelion.Tests/EmployeeLayerIntegrationTests.cs @@ -0,0 +1,482 @@ +using Microsoft.EntityFrameworkCore; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.Tests; + +/// +/// Tests de integración E2E para el Employee Layer (v0.4.3). +/// Verifica el flujo completo: Tenant → User → Employee → Driver/WarehouseOperator. +/// +public class EmployeeLayerIntegrationTests : IDisposable +{ + private readonly ParhelionDbContext _context; + private readonly Guid _tenantId = Guid.NewGuid(); + + public EmployeeLayerIntegrationTests() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + _context = new ParhelionDbContext(options, _tenantId); + + // Seed de roles + SeedRoles(); + } + + private void SeedRoles() + { + _context.Roles.AddRange( + new Role { Id = SeedData.AdminRoleId, Name = "Admin" }, + new Role { Id = SeedData.DriverRoleId, Name = "Driver" }, + new Role { Id = SeedData.WarehouseRoleId, Name = "Warehouse" }, + new Role { Id = SeedData.DemoUserRoleId, Name = "DemoUser" }, + new Role { Id = SeedData.SystemAdminRoleId, Name = "SystemAdmin" } + ); + _context.SaveChanges(); + } + + public void Dispose() + { + _context.Database.EnsureDeleted(); + _context.Dispose(); + } + + // ========== TEST 1: Crear Tenant ========== + + [Fact] + public async Task CanCreateTenant() + { + // Arrange + var tenant = new Tenant + { + Id = _tenantId, + CompanyName = "Test Company", + ContactEmail = "admin@test.parhelion.com", + FleetSize = 10, + DriverCount = 5, + IsActive = true + }; + + // Act + _context.Tenants.Add(tenant); + await _context.SaveChangesAsync(); + + // Assert + var savedTenant = await _context.Tenants.FindAsync(_tenantId); + Assert.NotNull(savedTenant); + Assert.Equal("Test Company", savedTenant.CompanyName); + } + + // ========== TEST 2: Crear User con IsSuperAdmin ========== + + [Fact] + public async Task CanCreateSuperAdminUser() + { + // Arrange - Crear tenant primero + var tenant = new Tenant + { + Id = _tenantId, + CompanyName = "Test Company", + ContactEmail = "admin@test.parhelion.com", + IsActive = true + }; + _context.Tenants.Add(tenant); + await _context.SaveChangesAsync(); + + // Act - Crear super admin + var superAdmin = new User + { + Id = Guid.NewGuid(), + TenantId = _tenantId, // Para test, en producción sería null + Email = "superadmin@parhelion.com", + PasswordHash = "hashed_password", + FullName = "Super Admin", + RoleId = SeedData.SystemAdminRoleId, + IsSuperAdmin = true, + IsActive = true + }; + + _context.Users.Add(superAdmin); + await _context.SaveChangesAsync(); + + // Assert + var savedUser = await _context.Users.FirstOrDefaultAsync(u => u.IsSuperAdmin); + Assert.NotNull(savedUser); + Assert.True(savedUser.IsSuperAdmin); + Assert.Equal("superadmin@parhelion.com", savedUser.Email); + } + + // ========== TEST 3: Flujo completo User → Employee → Driver ========== + + [Fact] + public async Task CanCreateCompleteDriverFlow() + { + // Arrange - Crear tenant + var tenant = new Tenant + { + Id = _tenantId, + CompanyName = "Transportes Norte", + ContactEmail = "admin@norte.parhelion.com", + FleetSize = 5, + IsActive = true + }; + _context.Tenants.Add(tenant); + await _context.SaveChangesAsync(); + + // Act 1 - Crear User (cuenta de acceso) + var userId = Guid.NewGuid(); + var user = new User + { + Id = userId, + TenantId = _tenantId, + Email = "carlos@norte.parhelion.com", + PasswordHash = "hashed", + FullName = "Carlos Pérez", + RoleId = SeedData.DriverRoleId, + IsActive = true + }; + _context.Users.Add(user); + await _context.SaveChangesAsync(); + + // Act 2 - Crear Employee (datos laborales) + var employeeId = Guid.NewGuid(); + var employee = new Employee + { + Id = employeeId, + TenantId = _tenantId, + UserId = userId, + Phone = "8181234567", + Rfc = "PERC901231ABC", + Nss = "12345678901", + Curp = "PERC901231HNLRRL09", + EmergencyContact = "María Pérez", + EmergencyPhone = "8187654321", + HireDate = DateTime.UtcNow.AddMonths(-6), + Department = "Field" + }; + _context.Employees.Add(employee); + await _context.SaveChangesAsync(); + + // Act 3 - Crear Driver (extensión con licencia) + var driver = new Driver + { + Id = Guid.NewGuid(), + EmployeeId = employeeId, + LicenseNumber = "NL-2024-123456", + LicenseType = "E", + LicenseExpiration = DateTime.UtcNow.AddYears(2), + Status = DriverStatus.Available + }; + _context.Drivers.Add(driver); + await _context.SaveChangesAsync(); + + // Assert - Verificar navegación completa + var savedDriver = await _context.Drivers + .Include(d => d.Employee) + .ThenInclude(e => e.User) + .FirstOrDefaultAsync(); + + Assert.NotNull(savedDriver); + Assert.NotNull(savedDriver.Employee); + Assert.NotNull(savedDriver.Employee.User); + + // Verificar datos + Assert.Equal("NL-2024-123456", savedDriver.LicenseNumber); + Assert.Equal("PERC901231ABC", savedDriver.Employee.Rfc); + Assert.Equal("carlos@norte.parhelion.com", savedDriver.Employee.User.Email); + } + + // ========== TEST 4: Flujo completo User → Employee → WarehouseOperator ========== + + [Fact] + public async Task CanCreateCompleteWarehouseOperatorFlow() + { + // Arrange - Crear tenant y ubicación + var tenant = new Tenant + { + Id = _tenantId, + CompanyName = "Transportes Norte", + ContactEmail = "admin@norte.parhelion.com", + IsActive = true + }; + _context.Tenants.Add(tenant); + + var locationId = Guid.NewGuid(); + var location = new Location + { + Id = locationId, + TenantId = _tenantId, + Code = "MTY", + Name = "CEDIS Monterrey", + Type = LocationType.Warehouse, + FullAddress = "Av. Industrial 123", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true + }; + _context.Locations.Add(location); + + // Crear zona de bodega + var zoneId = Guid.NewGuid(); + var zone = new WarehouseZone + { + Id = zoneId, + LocationId = locationId, + Code = "A1", + Name = "Zona de Recepción", + Type = WarehouseZoneType.Receiving, + IsActive = true + }; + _context.WarehouseZones.Add(zone); + await _context.SaveChangesAsync(); + + // Act 1 - Crear User + var userId = Guid.NewGuid(); + var user = new User + { + Id = userId, + TenantId = _tenantId, + Email = "maria@norte.parhelion.com", + PasswordHash = "hashed", + FullName = "María García", + RoleId = SeedData.WarehouseRoleId, + IsActive = true + }; + _context.Users.Add(user); + await _context.SaveChangesAsync(); + + // Act 2 - Crear Employee + var employeeId = Guid.NewGuid(); + var employee = new Employee + { + Id = employeeId, + TenantId = _tenantId, + UserId = userId, + Phone = "8189876543", + Rfc = "GARM850515XYZ", + Department = "Operations" + }; + _context.Employees.Add(employee); + await _context.SaveChangesAsync(); + + // Act 3 - Crear WarehouseOperator + var warehouseOperator = new WarehouseOperator + { + Id = Guid.NewGuid(), + EmployeeId = employeeId, + AssignedLocationId = locationId, + PrimaryZoneId = zoneId + }; + _context.WarehouseOperators.Add(warehouseOperator); + await _context.SaveChangesAsync(); + + // Assert + var savedOperator = await _context.WarehouseOperators + .Include(w => w.Employee) + .ThenInclude(e => e.User) + .Include(w => w.AssignedLocation) + .Include(w => w.PrimaryZone) + .FirstOrDefaultAsync(); + + Assert.NotNull(savedOperator); + Assert.NotNull(savedOperator.Employee); + Assert.NotNull(savedOperator.AssignedLocation); + Assert.NotNull(savedOperator.PrimaryZone); + + Assert.Equal("maria@norte.parhelion.com", savedOperator.Employee.User.Email); + Assert.Equal("MTY", savedOperator.AssignedLocation.Code); + Assert.Equal("A1", savedOperator.PrimaryZone.Code); + } + + // ========== TEST 5: Crear Shift y asignar a Employee ========== + + [Fact] + public async Task CanCreateShiftAndAssignToEmployee() + { + // Arrange + var tenant = new Tenant + { + Id = _tenantId, + CompanyName = "Test Company", + ContactEmail = "admin@test.parhelion.com", + IsActive = true + }; + _context.Tenants.Add(tenant); + + // Crear turno + var shiftId = Guid.NewGuid(); + var shift = new Shift + { + Id = shiftId, + TenantId = _tenantId, + Name = "Turno Matutino", + StartTime = new TimeOnly(6, 0), + EndTime = new TimeOnly(14, 0), + DaysOfWeek = "Mon,Tue,Wed,Thu,Fri", + IsActive = true + }; + _context.Shifts.Add(shift); + await _context.SaveChangesAsync(); + + // Crear User y Employee con turno + var userId = Guid.NewGuid(); + var user = new User + { + Id = userId, + TenantId = _tenantId, + Email = "test@test.parhelion.com", + PasswordHash = "hashed", + FullName = "Test User", + RoleId = SeedData.AdminRoleId, + IsActive = true + }; + _context.Users.Add(user); + + var employee = new Employee + { + Id = Guid.NewGuid(), + TenantId = _tenantId, + UserId = userId, + Phone = "1234567890", + ShiftId = shiftId, + Department = "Admin" + }; + _context.Employees.Add(employee); + await _context.SaveChangesAsync(); + + // Assert + var savedEmployee = await _context.Employees + .Include(e => e.Shift) + .FirstOrDefaultAsync(); + + Assert.NotNull(savedEmployee); + Assert.NotNull(savedEmployee.Shift); + Assert.Equal("Turno Matutino", savedEmployee.Shift.Name); + Assert.Equal(new TimeOnly(6, 0), savedEmployee.Shift.StartTime); + } + + // ========== TEST 6: Verificar nuevos permisos existen ========== + + [Fact] + public void NewPermissionsExist() + { + // Assert - Verificar que los nuevos permisos están definidos + Assert.True(Enum.IsDefined(typeof(Permission), Permission.EmployeesRead)); + Assert.True(Enum.IsDefined(typeof(Permission), Permission.ShiftsRead)); + Assert.True(Enum.IsDefined(typeof(Permission), Permission.WarehouseZonesRead)); + Assert.True(Enum.IsDefined(typeof(Permission), Permission.WarehouseOperatorsRead)); + Assert.True(Enum.IsDefined(typeof(Permission), Permission.TenantsRead)); + Assert.True(Enum.IsDefined(typeof(Permission), Permission.TenantsCreate)); + } + + // ========== TEST 7: ShipmentCheckpoint con WarehouseOperator ========== + + [Fact] + public async Task CanCreateCheckpointWithWarehouseOperator() + { + // Arrange - Setup completo + var tenant = new Tenant + { + Id = _tenantId, + CompanyName = "Test", + ContactEmail = "test@test.com", + IsActive = true + }; + _context.Tenants.Add(tenant); + + var locationId = Guid.NewGuid(); + var location = new Location + { + Id = locationId, + TenantId = _tenantId, + Code = "WH1", + Name = "Warehouse 1", + Type = LocationType.Warehouse, + FullAddress = "Test Address", + IsActive = true + }; + _context.Locations.Add(location); + + // Crear shipment + var shipmentId = Guid.NewGuid(); + var shipment = new Shipment + { + Id = shipmentId, + TenantId = _tenantId, + TrackingNumber = "PAR-TEST01", + QrCodeData = "QR-TEST01", + OriginLocationId = locationId, + DestinationLocationId = locationId, + RecipientName = "Test Recipient", + TotalWeightKg = 100, + TotalVolumeM3 = 1, + Status = ShipmentStatus.PendingApproval, + Priority = ShipmentPriority.Normal + }; + _context.Shipments.Add(shipment); + + // Crear User/Employee/WarehouseOperator + var userId = Guid.NewGuid(); + var user = new User + { + Id = userId, + TenantId = _tenantId, + Email = "wh@test.com", + PasswordHash = "hash", + FullName = "WH User", + RoleId = SeedData.WarehouseRoleId, + IsActive = true + }; + _context.Users.Add(user); + + var employeeId = Guid.NewGuid(); + var employee = new Employee + { + Id = employeeId, + TenantId = _tenantId, + UserId = userId, + Phone = "123", + Department = "Operations" + }; + _context.Employees.Add(employee); + + var warehouseOpId = Guid.NewGuid(); + var warehouseOp = new WarehouseOperator + { + Id = warehouseOpId, + EmployeeId = employeeId, + AssignedLocationId = locationId + }; + _context.WarehouseOperators.Add(warehouseOp); + await _context.SaveChangesAsync(); + + // Act - Crear checkpoint con referencia a WarehouseOperator + var checkpoint = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = shipmentId, + LocationId = locationId, + StatusCode = CheckpointStatus.ArrivedHub, + Timestamp = DateTime.UtcNow, + CreatedByUserId = userId, + HandledByWarehouseOperatorId = warehouseOpId, + ActionType = "Received" + }; + _context.ShipmentCheckpoints.Add(checkpoint); + await _context.SaveChangesAsync(); + + // Assert + var savedCheckpoint = await _context.ShipmentCheckpoints + .Include(c => c.HandledByWarehouseOperator) + .ThenInclude(w => w!.Employee) + .FirstOrDefaultAsync(); + + Assert.NotNull(savedCheckpoint); + Assert.NotNull(savedCheckpoint.HandledByWarehouseOperatorId); + Assert.Equal(warehouseOpId, savedCheckpoint.HandledByWarehouseOperatorId); + } +} diff --git a/backend/tests/Parhelion.Tests/FullSystemE2ETest.cs b/backend/tests/Parhelion.Tests/FullSystemE2ETest.cs new file mode 100644 index 0000000..310b2b4 --- /dev/null +++ b/backend/tests/Parhelion.Tests/FullSystemE2ETest.cs @@ -0,0 +1,942 @@ +using Microsoft.EntityFrameworkCore; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.Tests; + +/// +/// 🧪 SUPER TEST E2E - Flujo Completo del Sistema +/// +/// Recorre TODAS las tablas del sistema en un flujo realista: +/// SuperAdmin → Tenant → Admin → Employees → Trucks → Drivers → Warehouse → +/// Locations → Zones → Routes → Clients → Shipments → Items → Checkpoints → Documents → Delivery +/// +/// Este test valida que toda la base de datos está correctamente integrada. +/// +public class FullSystemE2ETest : IDisposable +{ + private readonly ParhelionDbContext _context; + private readonly Guid _testTenantId = Guid.NewGuid(); + + // IDs que se crean durante el test + private Guid _superAdminUserId; + private Guid _adminUserId; + private Guid _adminEmployeeId; + private Guid _driverUserId; + private Guid _driverEmployeeId; + private Guid _driverId; + private Guid _warehouseUserId; + private Guid _warehouseEmployeeId; + private Guid _warehouseOperatorId; + private Guid _truckId; + private Guid _originLocationId; + private Guid _hubLocationId; + private Guid _destLocationId; + private Guid _zoneId; + private Guid _shiftId; + private Guid _routeId; + private Guid _senderId; + private Guid _recipientId; + private Guid _shipmentId; + private Guid _shipmentItemId; + private Guid _networkLinkId; + + public FullSystemE2ETest() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"FullE2E_{Guid.NewGuid()}") + .Options; + + _context = new ParhelionDbContext(options, _testTenantId); + SeedSystemRoles(); + } + + private void SeedSystemRoles() + { + _context.Roles.AddRange( + new Role { Id = SeedData.AdminRoleId, Name = "Admin", Description = "Gerente de Tráfico" }, + new Role { Id = SeedData.DriverRoleId, Name = "Driver", Description = "Chofer" }, + new Role { Id = SeedData.WarehouseRoleId, Name = "Warehouse", Description = "Almacenista" }, + new Role { Id = SeedData.DemoUserRoleId, Name = "DemoUser", Description = "Demo" }, + new Role { Id = SeedData.SystemAdminRoleId, Name = "SystemAdmin", Description = "Super Admin" } + ); + _context.SaveChanges(); + } + + public void Dispose() + { + _context.Database.EnsureDeleted(); + _context.Dispose(); + } + + /// + /// 🔥 TEST PRINCIPAL: Flujo completo desde SuperAdmin hasta entrega final + /// + [Fact] + public async Task FullSystemFlow_SuperAdminToDelivery_AllTablesWork() + { + // ============================================================ + // FASE 1: SUPER ADMIN CREA TENANT Y ADMIN + // ============================================================ + await Step1_SuperAdminCreatesTenantAndAdmin(); + + // ============================================================ + // FASE 2: ADMIN CONFIGURA LA EMPRESA + // ============================================================ + await Step2_AdminConfiguresShifts(); + await Step3_AdminCreatesLocationsAndZones(); + await Step4_AdminCreatesNetworkAndRoutes(); + await Step5_AdminCreatesTrucks(); + await Step6_AdminCreatesDriverEmployee(); + await Step7_AdminCreatesWarehouseOperator(); + await Step8_AdminCreatesClients(); + + // ============================================================ + // FASE 3: OPERACIONES - CREACIÓN DE ENVÍO + // ============================================================ + await Step9_CreateShipmentWithItems(); + + // ============================================================ + // FASE 4: TRAZABILIDAD - CHECKPOINTS + // ============================================================ + await Step10_WarehouseLoadsShipment(); + await Step11_DriverPicksUpShipment(); + await Step12_ShipmentArrivesAtHub(); + await Step13_ShipmentOutForDelivery(); + await Step14_ShipmentDelivered(); + + // ============================================================ + // FASE 5: DOCUMENTACIÓN + // ============================================================ + await Step15_GenerateDocuments(); + + // ============================================================ + // FASE 6: VERIFICACIÓN FINAL + // ============================================================ + await VerifyAllTablesHaveData(); + } + + // ==================== FASE 1: SUPER ADMIN ==================== + + private async Task Step1_SuperAdminCreatesTenantAndAdmin() + { + // 1.1 Super Admin se registra (sin tenant) + _superAdminUserId = Guid.NewGuid(); + var superAdmin = new User + { + Id = _superAdminUserId, + TenantId = _testTenantId, // En test, requerido por InMemory + Email = "superadmin@parhelion.com", + PasswordHash = "hashed_super_secure", + FullName = "Sistema Parhelion", + RoleId = SeedData.SystemAdminRoleId, + IsSuperAdmin = true, + IsActive = true + }; + _context.Users.Add(superAdmin); + + // 1.2 Crear Tenant (empresa) + var tenant = new Tenant + { + Id = _testTenantId, + CompanyName = "Transportes Norte S.A. de C.V.", + ContactEmail = "admin@tnorte.parhelion.com", + FleetSize = 10, + DriverCount = 5, + IsActive = true + }; + _context.Tenants.Add(tenant); + await _context.SaveChangesAsync(); + + // 1.3 Super Admin crea Admin de la empresa + _adminUserId = Guid.NewGuid(); + var adminUser = new User + { + Id = _adminUserId, + TenantId = _testTenantId, + Email = "juan.perez@tnorte.parhelion.com", + PasswordHash = "hashed_admin", + FullName = "Juan Pérez García", + RoleId = SeedData.AdminRoleId, + IsActive = true + }; + _context.Users.Add(adminUser); + await _context.SaveChangesAsync(); + + // 1.4 Crear perfil de Employee para el Admin + _adminEmployeeId = Guid.NewGuid(); + var adminEmployee = new Employee + { + Id = _adminEmployeeId, + TenantId = _testTenantId, + UserId = _adminUserId, + Phone = "8181234567", + Rfc = "PEGJ850101ABC", + Nss = "12345678901", + Curp = "PEGJ850101HNLRRL09", + EmergencyContact = "María García", + EmergencyPhone = "8187654321", + HireDate = DateTime.UtcNow.AddYears(-5), + Department = "Admin" + }; + _context.Employees.Add(adminEmployee); + await _context.SaveChangesAsync(); + + // Verificación + Assert.NotNull(await _context.Tenants.FindAsync(_testTenantId)); + Assert.NotNull(await _context.Users.FindAsync(_superAdminUserId)); + Assert.NotNull(await _context.Users.FindAsync(_adminUserId)); + Assert.NotNull(await _context.Employees.FindAsync(_adminEmployeeId)); + } + + // ==================== FASE 2: ADMIN CONFIGURA ==================== + + private async Task Step2_AdminConfiguresShifts() + { + _shiftId = Guid.NewGuid(); + var shifts = new[] + { + new Shift + { + Id = _shiftId, + TenantId = _testTenantId, + Name = "Turno Matutino", + StartTime = new TimeOnly(6, 0), + EndTime = new TimeOnly(14, 0), + DaysOfWeek = "Mon,Tue,Wed,Thu,Fri", + IsActive = true + }, + new Shift + { + Id = Guid.NewGuid(), + TenantId = _testTenantId, + Name = "Turno Vespertino", + StartTime = new TimeOnly(14, 0), + EndTime = new TimeOnly(22, 0), + DaysOfWeek = "Mon,Tue,Wed,Thu,Fri", + IsActive = true + } + }; + _context.Shifts.AddRange(shifts); + await _context.SaveChangesAsync(); + + Assert.Equal(2, await _context.Shifts.CountAsync()); + } + + private async Task Step3_AdminCreatesLocationsAndZones() + { + // 3.1 Ubicaciones + _originLocationId = Guid.NewGuid(); + _hubLocationId = Guid.NewGuid(); + _destLocationId = Guid.NewGuid(); + + var locations = new[] + { + new Location + { + Id = _originLocationId, + TenantId = _testTenantId, + Code = "MTY", + Name = "CEDIS Monterrey", + Type = LocationType.Warehouse, + FullAddress = "Av. Eugenio Garza Sada 2501, Monterrey, NL", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true + }, + new Location + { + Id = _hubLocationId, + TenantId = _testTenantId, + Code = "SLP", + Name = "Hub San Luis Potosí", + Type = LocationType.CrossDock, + FullAddress = "Carretera 57 Km 15, SLP", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true + }, + new Location + { + Id = _destLocationId, + TenantId = _testTenantId, + Code = "CDMX", + Name = "Punto de Entrega CDMX", + Type = LocationType.Store, + FullAddress = "Calzada de Tlalpan 1234, CDMX", + CanReceive = true, + CanDispatch = false, + IsInternal = false, + IsActive = true + } + }; + _context.Locations.AddRange(locations); + await _context.SaveChangesAsync(); + + // 3.2 Zonas de bodega + _zoneId = Guid.NewGuid(); + var zones = new[] + { + new WarehouseZone + { + Id = _zoneId, + LocationId = _originLocationId, + Code = "A1", + Name = "Zona de Recepción", + Type = WarehouseZoneType.Receiving, + IsActive = true + }, + new WarehouseZone + { + Id = Guid.NewGuid(), + LocationId = _originLocationId, + Code = "B1", + Name = "Almacenamiento General", + Type = WarehouseZoneType.Storage, + IsActive = true + }, + new WarehouseZone + { + Id = Guid.NewGuid(), + LocationId = _originLocationId, + Code = "C1", + Name = "Andén de Salida", + Type = WarehouseZoneType.Shipping, + IsActive = true + }, + new WarehouseZone + { + Id = Guid.NewGuid(), + LocationId = _originLocationId, + Code = "COLD-1", + Name = "Cuarto Frío", + Type = WarehouseZoneType.ColdChain, + IsActive = true + } + }; + _context.WarehouseZones.AddRange(zones); + await _context.SaveChangesAsync(); + + Assert.Equal(3, await _context.Locations.CountAsync()); + Assert.Equal(4, await _context.WarehouseZones.CountAsync()); + } + + private async Task Step4_AdminCreatesNetworkAndRoutes() + { + // 4.1 Network Links (conexiones entre ubicaciones) + _networkLinkId = Guid.NewGuid(); + var links = new[] + { + new NetworkLink + { + Id = _networkLinkId, + TenantId = _testTenantId, + OriginLocationId = _originLocationId, + DestinationLocationId = _hubLocationId, + LinkType = NetworkLinkType.LineHaul, + TransitTime = TimeSpan.FromHours(5), + IsBidirectional = true, + IsActive = true + }, + new NetworkLink + { + Id = Guid.NewGuid(), + TenantId = _testTenantId, + OriginLocationId = _hubLocationId, + DestinationLocationId = _destLocationId, + LinkType = NetworkLinkType.LastMile, + TransitTime = TimeSpan.FromHours(4), + IsBidirectional = false, + IsActive = true + } + }; + _context.NetworkLinks.AddRange(links); + await _context.SaveChangesAsync(); + + // 4.2 Route Blueprint + _routeId = Guid.NewGuid(); + var route = new RouteBlueprint + { + Id = _routeId, + TenantId = _testTenantId, + Name = "Ruta Norte-Centro", + Description = "MTY → SLP → CDMX", + TotalSteps = 3, + TotalTransitTime = TimeSpan.FromHours(9), + IsActive = true + }; + _context.RouteBlueprints.Add(route); + await _context.SaveChangesAsync(); + + // 4.3 Route Steps + var steps = new[] + { + new RouteStep + { + Id = Guid.NewGuid(), + RouteBlueprintId = _routeId, + LocationId = _originLocationId, + StepOrder = 1, + StandardTransitTime = TimeSpan.Zero, + StepType = RouteStepType.Origin + }, + new RouteStep + { + Id = Guid.NewGuid(), + RouteBlueprintId = _routeId, + LocationId = _hubLocationId, + StepOrder = 2, + StandardTransitTime = TimeSpan.FromHours(5), + StepType = RouteStepType.Intermediate + }, + new RouteStep + { + Id = Guid.NewGuid(), + RouteBlueprintId = _routeId, + LocationId = _destLocationId, + StepOrder = 3, + StandardTransitTime = TimeSpan.FromHours(4), + StepType = RouteStepType.Destination + } + }; + _context.RouteSteps.AddRange(steps); + await _context.SaveChangesAsync(); + + Assert.Equal(2, await _context.NetworkLinks.CountAsync()); + Assert.Equal(1, await _context.RouteBlueprints.CountAsync()); + Assert.Equal(3, await _context.RouteSteps.CountAsync()); + } + + private async Task Step5_AdminCreatesTrucks() + { + _truckId = Guid.NewGuid(); + var trucks = new[] + { + new Truck + { + Id = _truckId, + TenantId = _testTenantId, + Plate = "NL-001-X", + Model = "Kenworth T680", + Type = TruckType.DryBox, + MaxCapacityKg = 25000, + MaxVolumeM3 = 80, + Vin = "1XKYD49X8NJ123456", + Year = 2023, + InsurancePolicy = "POL-2024-001", + InsuranceExpiration = DateTime.UtcNow.AddYears(1), + IsActive = true + }, + new Truck + { + Id = Guid.NewGuid(), + TenantId = _testTenantId, + Plate = "NL-002-R", + Model = "Freightliner Cascadia", + Type = TruckType.Refrigerated, + MaxCapacityKg = 20000, + MaxVolumeM3 = 60, + IsActive = true + } + }; + _context.Trucks.AddRange(trucks); + await _context.SaveChangesAsync(); + + Assert.Equal(2, await _context.Trucks.CountAsync()); + } + + private async Task Step6_AdminCreatesDriverEmployee() + { + // 6.1 User para el chofer + _driverUserId = Guid.NewGuid(); + var driverUser = new User + { + Id = _driverUserId, + TenantId = _testTenantId, + Email = "carlos.driver@tnorte.parhelion.com", + PasswordHash = "hashed_driver", + FullName = "Carlos Rodríguez", + RoleId = SeedData.DriverRoleId, + IsActive = true + }; + _context.Users.Add(driverUser); + await _context.SaveChangesAsync(); + + // 6.2 Employee (datos laborales) + _driverEmployeeId = Guid.NewGuid(); + var driverEmployee = new Employee + { + Id = _driverEmployeeId, + TenantId = _testTenantId, + UserId = _driverUserId, + Phone = "8189876543", + Rfc = "RODC900215XYZ", + Nss = "98765432101", + Curp = "RODC900215HNLRRL05", + EmergencyContact = "Ana Rodríguez", + EmergencyPhone = "8181112233", + HireDate = DateTime.UtcNow.AddMonths(-18), + ShiftId = _shiftId, + Department = "Field" + }; + _context.Employees.Add(driverEmployee); + await _context.SaveChangesAsync(); + + // 6.3 Driver (extensión con licencia) + _driverId = Guid.NewGuid(); + var driver = new Driver + { + Id = _driverId, + EmployeeId = _driverEmployeeId, + LicenseNumber = "NL-2024-567890", + LicenseType = "E", + LicenseExpiration = DateTime.UtcNow.AddYears(3), + DefaultTruckId = _truckId, + CurrentTruckId = _truckId, + Status = DriverStatus.Available + }; + _context.Drivers.Add(driver); + await _context.SaveChangesAsync(); + + // 6.4 Fleet Log (asignación inicial) + var fleetLog = new FleetLog + { + Id = Guid.NewGuid(), + TenantId = _testTenantId, + DriverId = _driverId, + OldTruckId = null, + NewTruckId = _truckId, + Reason = FleetLogReason.Reassignment, + Timestamp = DateTime.UtcNow, + CreatedByUserId = _adminUserId + }; + _context.FleetLogs.Add(fleetLog); + await _context.SaveChangesAsync(); + + Assert.NotNull(await _context.Drivers.FindAsync(_driverId)); + Assert.Equal(1, await _context.FleetLogs.CountAsync()); + } + + private async Task Step7_AdminCreatesWarehouseOperator() + { + // 7.1 User para almacenista + _warehouseUserId = Guid.NewGuid(); + var whUser = new User + { + Id = _warehouseUserId, + TenantId = _testTenantId, + Email = "maria.wh@tnorte.parhelion.com", + PasswordHash = "hashed_wh", + FullName = "María López", + RoleId = SeedData.WarehouseRoleId, + IsActive = true + }; + _context.Users.Add(whUser); + await _context.SaveChangesAsync(); + + // 7.2 Employee + _warehouseEmployeeId = Guid.NewGuid(); + var whEmployee = new Employee + { + Id = _warehouseEmployeeId, + TenantId = _testTenantId, + UserId = _warehouseUserId, + Phone = "8185554433", + Rfc = "LOPM880512ABC", + ShiftId = _shiftId, + Department = "Operations" + }; + _context.Employees.Add(whEmployee); + await _context.SaveChangesAsync(); + + // 7.3 WarehouseOperator + _warehouseOperatorId = Guid.NewGuid(); + var whOperator = new WarehouseOperator + { + Id = _warehouseOperatorId, + EmployeeId = _warehouseEmployeeId, + AssignedLocationId = _originLocationId, + PrimaryZoneId = _zoneId + }; + _context.WarehouseOperators.Add(whOperator); + await _context.SaveChangesAsync(); + + Assert.NotNull(await _context.WarehouseOperators.FindAsync(_warehouseOperatorId)); + } + + private async Task Step8_AdminCreatesClients() + { + // Remitente (quien envía) + _senderId = Guid.NewGuid(); + var sender = new Client + { + Id = _senderId, + TenantId = _testTenantId, + CompanyName = "Fábrica de Electrónicos SA", + ContactName = "Roberto Sánchez", + Email = "roberto@fabricaelectronicos.mx", + Phone = "5512345678", + TaxId = "FEL901231XYZ", + LegalName = "Fábrica de Electrónicos S.A. de C.V.", + BillingAddress = "Parque Industrial Norte 123, Monterrey", + ShippingAddress = "Parque Industrial Norte 123, Monterrey", + Priority = ClientPriority.High, + IsActive = true + }; + _context.Clients.Add(sender); + + // Destinatario (quien recibe) + _recipientId = Guid.NewGuid(); + var recipient = new Client + { + Id = _recipientId, + TenantId = _testTenantId, + CompanyName = "Tienda Electro CDMX", + ContactName = "Laura Martínez", + Email = "laura@electrocdmx.mx", + Phone = "5598765432", + ShippingAddress = "Calzada de Tlalpan 1234, CDMX", + Priority = ClientPriority.Normal, + IsActive = true + }; + _context.Clients.Add(recipient); + await _context.SaveChangesAsync(); + + Assert.Equal(2, await _context.Clients.CountAsync()); + } + + // ==================== FASE 3: OPERACIONES ==================== + + private async Task Step9_CreateShipmentWithItems() + { + // 9.1 Crear envío + _shipmentId = Guid.NewGuid(); + var shipment = new Shipment + { + Id = _shipmentId, + TenantId = _testTenantId, + TrackingNumber = "PAR-E2E001", + QrCodeData = "QR-PAR-E2E001", + OriginLocationId = _originLocationId, + DestinationLocationId = _destLocationId, + SenderId = _senderId, + RecipientClientId = _recipientId, + RecipientName = "Laura Martínez", + RecipientPhone = "5598765432", + TotalWeightKg = 150, + TotalVolumeM3 = 2.5m, + DeclaredValue = 50000, + SatMerchandiseCode = "84111506", + DeliveryInstructions = "Entregar en horario de oficina", + AssignedRouteId = _routeId, + CurrentStepOrder = 1, + Priority = ShipmentPriority.Normal, + Status = ShipmentStatus.PendingApproval, + ScheduledDeparture = DateTime.UtcNow.AddHours(2) + }; + _context.Shipments.Add(shipment); + await _context.SaveChangesAsync(); + + // 9.2 Agregar items al envío + _shipmentItemId = Guid.NewGuid(); + var items = new[] + { + new ShipmentItem + { + Id = _shipmentItemId, + ShipmentId = _shipmentId, + Sku = "ELEC-TV-55", + Description = "Televisor LED 55 pulgadas", + PackagingType = PackagingType.Box, + Quantity = 5, + WeightKg = 25, + WidthCm = 130, + HeightCm = 80, + LengthCm = 20, + DeclaredValue = 8000, + IsFragile = true, + IsHazardous = false, + RequiresRefrigeration = false + }, + new ShipmentItem + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + Sku = "ELEC-LAPTOP-15", + Description = "Laptop Empresarial 15.6\"", + PackagingType = PackagingType.Box, + Quantity = 10, + WeightKg = 2.5m, + WidthCm = 40, + HeightCm = 30, + LengthCm = 10, + DeclaredValue = 2500, + IsFragile = true + } + }; + _context.ShipmentItems.AddRange(items); + await _context.SaveChangesAsync(); + + Assert.NotNull(await _context.Shipments.FindAsync(_shipmentId)); + Assert.Equal(2, await _context.ShipmentItems.CountAsync()); + } + + // ==================== FASE 4: TRAZABILIDAD ==================== + + private async Task Step10_WarehouseLoadsShipment() + { + // Aprobar envío + var shipment = await _context.Shipments.FindAsync(_shipmentId); + shipment!.Status = ShipmentStatus.Approved; + await _context.SaveChangesAsync(); + + // Checkpoint: Almacenista carga el paquete + var checkpoint = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + LocationId = _originLocationId, + StatusCode = CheckpointStatus.Loaded, + Remarks = "Carga completada por almacenista", + Timestamp = DateTime.UtcNow, + CreatedByUserId = _warehouseUserId, + HandledByWarehouseOperatorId = _warehouseOperatorId, + LoadedOntoTruckId = _truckId, + ActionType = "Loaded", + NewCustodian = "María López" + }; + _context.ShipmentCheckpoints.Add(checkpoint); + + // Actualizar estado + shipment.Status = ShipmentStatus.Loaded; + shipment.TruckId = _truckId; + await _context.SaveChangesAsync(); + + Assert.Equal(ShipmentStatus.Loaded, shipment.Status); + } + + private async Task Step11_DriverPicksUpShipment() + { + // Checkpoint: Chofer escanea QR y toma custodia + var checkpoint = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + LocationId = _originLocationId, + StatusCode = CheckpointStatus.QrScanned, + Remarks = "Custodia transferida a chofer", + Timestamp = DateTime.UtcNow.AddMinutes(30), + CreatedByUserId = _driverUserId, + HandledByDriverId = _driverId, + LoadedOntoTruckId = _truckId, + ActionType = "CustodyTransfer", + PreviousCustodian = "María López", + NewCustodian = "Carlos Rodríguez" + }; + _context.ShipmentCheckpoints.Add(checkpoint); + + // Actualizar envío + var shipment = await _context.Shipments.FindAsync(_shipmentId); + shipment!.Status = ShipmentStatus.InTransit; + shipment.DriverId = _driverId; + shipment.WasQrScanned = true; + shipment.AssignedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + Assert.Equal(ShipmentStatus.InTransit, shipment.Status); + } + + private async Task Step12_ShipmentArrivesAtHub() + { + var checkpoint = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + LocationId = _hubLocationId, + StatusCode = CheckpointStatus.ArrivedHub, + Remarks = "Llegada a Hub SLP", + Timestamp = DateTime.UtcNow.AddHours(5), + CreatedByUserId = _driverUserId, + HandledByDriverId = _driverId, + ActionType = "ArrivedHub" + }; + _context.ShipmentCheckpoints.Add(checkpoint); + + var shipment = await _context.Shipments.FindAsync(_shipmentId); + shipment!.Status = ShipmentStatus.AtHub; + shipment.CurrentStepOrder = 2; + await _context.SaveChangesAsync(); + + Assert.Equal(ShipmentStatus.AtHub, shipment.Status); + } + + private async Task Step13_ShipmentOutForDelivery() + { + var checkpoint = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + LocationId = _hubLocationId, + StatusCode = CheckpointStatus.DepartedHub, + Remarks = "Salida hacia CDMX", + Timestamp = DateTime.UtcNow.AddHours(6), + CreatedByUserId = _driverUserId, + HandledByDriverId = _driverId, + ActionType = "DepartedHub" + }; + _context.ShipmentCheckpoints.Add(checkpoint); + + // Cerca del destino + var outForDelivery = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + LocationId = _destLocationId, + StatusCode = CheckpointStatus.OutForDelivery, + Remarks = "En camino a punto de entrega", + Timestamp = DateTime.UtcNow.AddHours(9), + CreatedByUserId = _driverUserId, + HandledByDriverId = _driverId, + ActionType = "OutForDelivery" + }; + _context.ShipmentCheckpoints.Add(outForDelivery); + + var shipment = await _context.Shipments.FindAsync(_shipmentId); + shipment!.Status = ShipmentStatus.OutForDelivery; + shipment.CurrentStepOrder = 3; + await _context.SaveChangesAsync(); + } + + private async Task Step14_ShipmentDelivered() + { + var checkpoint = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + LocationId = _destLocationId, + StatusCode = CheckpointStatus.Delivered, + Remarks = "Entrega exitosa. Firma: Laura Martínez", + Timestamp = DateTime.UtcNow.AddHours(10), + CreatedByUserId = _driverUserId, + HandledByDriverId = _driverId, + ActionType = "Delivered", + NewCustodian = "Laura Martínez (Destinatario)" + }; + _context.ShipmentCheckpoints.Add(checkpoint); + + var shipment = await _context.Shipments.FindAsync(_shipmentId); + shipment!.Status = ShipmentStatus.Delivered; + shipment.DeliveredAt = DateTime.UtcNow.AddHours(10); + shipment.RecipientSignatureUrl = "/signatures/PAR-E2E001.png"; + await _context.SaveChangesAsync(); + + Assert.Equal(ShipmentStatus.Delivered, shipment.Status); + Assert.NotNull(shipment.DeliveredAt); + } + + // ==================== FASE 5: DOCUMENTOS ==================== + + private async Task Step15_GenerateDocuments() + { + var documents = new[] + { + new ShipmentDocument + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + DocumentType = DocumentType.ServiceOrder, + FileUrl = "/documents/PAR-E2E001/service_order.pdf", + GeneratedBy = "System", + GeneratedAt = DateTime.UtcNow.AddHours(-2) + }, + new ShipmentDocument + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + DocumentType = DocumentType.Waybill, + FileUrl = "/documents/PAR-E2E001/carta_porte.pdf", + GeneratedBy = "System", + GeneratedAt = DateTime.UtcNow.AddHours(-2) + }, + new ShipmentDocument + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + DocumentType = DocumentType.Manifest, + FileUrl = "/documents/PAR-E2E001/manifest.pdf", + GeneratedBy = "System", + GeneratedAt = DateTime.UtcNow.AddHours(-1) + }, + new ShipmentDocument + { + Id = Guid.NewGuid(), + ShipmentId = _shipmentId, + DocumentType = DocumentType.POD, + FileUrl = "/documents/PAR-E2E001/pod.pdf", + GeneratedBy = "System", + GeneratedAt = DateTime.UtcNow + } + }; + _context.ShipmentDocuments.AddRange(documents); + await _context.SaveChangesAsync(); + + Assert.Equal(4, await _context.ShipmentDocuments.CountAsync()); + } + + // ==================== VERIFICACIÓN FINAL ==================== + + private async Task VerifyAllTablesHaveData() + { + // Verificar TODAS las tablas tienen datos + var results = new Dictionary + { + ["Tenants"] = await _context.Tenants.CountAsync(), + ["Users"] = await _context.Users.CountAsync(), + ["Roles"] = await _context.Roles.CountAsync(), + ["Employees"] = await _context.Employees.CountAsync(), + ["Shifts"] = await _context.Shifts.CountAsync(), + ["Drivers"] = await _context.Drivers.CountAsync(), + ["WarehouseOperators"] = await _context.WarehouseOperators.CountAsync(), + ["Trucks"] = await _context.Trucks.CountAsync(), + ["FleetLogs"] = await _context.FleetLogs.CountAsync(), + ["Locations"] = await _context.Locations.CountAsync(), + ["WarehouseZones"] = await _context.WarehouseZones.CountAsync(), + ["NetworkLinks"] = await _context.NetworkLinks.CountAsync(), + ["RouteBlueprints"] = await _context.RouteBlueprints.CountAsync(), + ["RouteSteps"] = await _context.RouteSteps.CountAsync(), + ["Clients"] = await _context.Clients.CountAsync(), + ["Shipments"] = await _context.Shipments.CountAsync(), + ["ShipmentItems"] = await _context.ShipmentItems.CountAsync(), + ["ShipmentCheckpoints"] = await _context.ShipmentCheckpoints.CountAsync(), + ["ShipmentDocuments"] = await _context.ShipmentDocuments.CountAsync() + }; + + // Verificar que TODAS las tablas tienen al menos 1 registro + foreach (var (table, count) in results) + { + Assert.True(count > 0, $"La tabla {table} está vacía"); + } + + // Verificar conteos específicos + Assert.Equal(4, results["Users"]); // SuperAdmin, Admin, Driver, Warehouse + Assert.Equal(3, results["Employees"]); // Admin, Driver, Warehouse + Assert.Equal(6, results["ShipmentCheckpoints"]); // Loaded, QR, ArrivedHub, Departed, OutForDelivery, Delivered + Assert.Equal(4, results["ShipmentDocuments"]); // ServiceOrder, Waybill, Manifest, POD + + // Verificar estado final del envío + var finalShipment = await _context.Shipments + .Include(s => s.Items) + .Include(s => s.History) + .Include(s => s.Documents) + .FirstOrDefaultAsync(s => s.Id == _shipmentId); + + Assert.NotNull(finalShipment); + Assert.Equal(ShipmentStatus.Delivered, finalShipment.Status); + Assert.Equal(2, finalShipment.Items.Count); + Assert.Equal(6, finalShipment.History.Count); + Assert.Equal(4, finalShipment.Documents.Count); + } +} diff --git a/backend/tests/Parhelion.Tests/GlobalUsings.cs b/backend/tests/Parhelion.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/backend/tests/Parhelion.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/backend/tests/Parhelion.Tests/Parhelion.Tests.csproj b/backend/tests/Parhelion.Tests/Parhelion.Tests.csproj new file mode 100644 index 0000000..310b4cf --- /dev/null +++ b/backend/tests/Parhelion.Tests/Parhelion.Tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/database-schema.md b/database-schema.md index f847f56..83eb58e 100644 --- a/database-schema.md +++ b/database-schema.md @@ -1,12 +1,14 @@ # PARHELION-LOGISTICS | Modelo de Base de Datos -**Versión:** 2.3 (Final - Scope Freeze) -**Fecha:** Diciembre 2025 -**Motor:** PostgreSQL + Entity Framework Core (Code First) +**Versión:** 2.4 (v0.4.3 - Employee Layer) +**Fecha:** Diciembre 2025 +**Motor:** PostgreSQL + Entity Framework Core (Code First) **Estado:** Diseño Cerrado - Listo para Implementación > **Nota Técnica:** Esta plataforma unifica WMS (Warehouse Management System) y TMS (Transportation Management System). El módulo de almacén gestiona inventario estático y carga, mientras que el núcleo TMS maneja logística de media milla: gestión de flotas tipificadas, redes Hub & Spoke y trazabilidad de envíos en movimiento. +> **v0.4.3:** Agrega Employee Layer (centraliza datos legales), Shift (turnos), WarehouseZone (zonas de bodega), WarehouseOperator (extensión de empleado para almacenistas), y SuperAdmin (IsSuperAdmin en User). + --- ## 1. Diagrama Entidad-Relación (ER) - Vista General @@ -58,6 +60,18 @@ erDiagram DRIVER ||--o{ SHIPMENT_CHECKPOINT : "maneja paquetes" TRUCK ||--o{ SHIPMENT_CHECKPOINT : "carga paquetes" + %% ========== EMPLOYEE LAYER (v0.4.3) ========== + TENANT ||--o{ EMPLOYEE : "emplea" + TENANT ||--o{ SHIFT : "define turnos" + USER ||--o| EMPLOYEE : "tiene perfil de empleado" + EMPLOYEE ||--o| DRIVER : "extensión chofer" + EMPLOYEE ||--o| WAREHOUSE_OPERATOR : "extensión almacenista" + EMPLOYEE }o--o| SHIFT : "tiene turno" + LOCATION ||--o{ WAREHOUSE_ZONE : "tiene zonas" + LOCATION ||--o{ WAREHOUSE_OPERATOR : "asigna operadores" + WAREHOUSE_ZONE ||--o{ WAREHOUSE_OPERATOR : "asigna a zona" + WAREHOUSE_OPERATOR ||--o{ SHIPMENT_CHECKPOINT : "maneja paquetes" + %% ========== ENTIDADES CORE ========== TENANT { uuid id PK @@ -65,8 +79,14 @@ erDiagram string contact_email int fleet_size int driver_count - datetime created_at boolean is_active + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } USER { @@ -77,36 +97,100 @@ erDiagram string full_name uuid role_id FK boolean is_demo_user - datetime last_login - datetime created_at + boolean uses_argon2 "True si usa Argon2id" + boolean is_super_admin "v0.4.3 - SuperAdmin del sistema" + datetime last_login "nullable" boolean is_active + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } ROLE { uuid id PK string name UK string description + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } DRIVER { uuid id PK - uuid tenant_id FK - uuid user_id FK - string full_name - string phone + uuid employee_id FK "v0.4.3 - Referencia a Employee" string license_number + string license_type "nullable - A|B|C|D|E" + datetime license_expiration "nullable" uuid default_truck_id FK "nullable - asignación fija" uuid current_truck_id FK "nullable - camión actual" string status "Available|OnRoute|Inactive" + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + } + + %% ========== EMPLOYEE LAYER ENTITIES (v0.4.3) ========== + EMPLOYEE { + uuid id PK + uuid tenant_id FK + uuid user_id FK UK "1:1 con User" + string phone string rfc "nullable - RFC fiscal" string nss "nullable - Número de Seguro Social" string curp "nullable - CURP" - string license_type "nullable - A|B|C|D|E" - datetime license_expiration "nullable" string emergency_contact "nullable" string emergency_phone "nullable" - datetime hire_date "nullable - Fecha de contratación" + datetime hire_date "nullable" + uuid shift_id FK "nullable" + string department "nullable - Admin|Operations|Field" + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + } + + SHIFT { + uuid id PK + uuid tenant_id FK + string name "Matutino|Vespertino|Nocturno" + time start_time + time end_time + string days_of_week "Mon,Tue,Wed,Thu,Fri" + boolean is_active + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + } + + WAREHOUSE_ZONE { + uuid id PK + uuid location_id FK + string code UK "A1, B2, COLD-1" + string name + string type "Receiving|Storage|Staging|Shipping|ColdChain|Hazmat" + boolean is_active + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + } + + WAREHOUSE_OPERATOR { + uuid id PK + uuid employee_id FK UK "1:1 con Employee" + uuid assigned_location_id FK + uuid primary_zone_id FK "nullable" datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } TRUCK { @@ -120,16 +204,19 @@ erDiagram boolean is_active string vin "nullable - VIN" string engine_number "nullable" - int year "nullable - Año del vehículo" + int year "nullable" string color "nullable" - string insurance_policy "nullable - Número de póliza" + string insurance_policy "nullable" datetime insurance_expiration "nullable" - string verification_number "nullable - Verificación vehicular" + string verification_number "nullable" datetime verification_expiration "nullable" datetime last_maintenance_date "nullable" datetime next_maintenance_date "nullable" decimal current_odometer_km "nullable" datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } %% ========== ENTIDADES ENTERPRISE ========== @@ -145,6 +232,9 @@ erDiagram boolean is_internal "Propio o externo" boolean is_active datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } SHIPMENT { @@ -177,6 +267,9 @@ erDiagram datetime assigned_at "nullable" datetime delivered_at "nullable" datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } SHIPMENT_ITEM { @@ -197,6 +290,9 @@ erDiagram boolean requires_refrigeration string stacking_instructions "nullable - Ej: No apilar" datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } SHIPMENT_CHECKPOINT { @@ -244,8 +340,9 @@ erDiagram string billing_address "nullable" string shipping_address "Dirección de envío" string preferred_product_types "nullable - Tipos de productos" - string priority "Normal|Low|High|Urgent" + string priority "Default heredado a sus envíos - Normal|Low|High|Urgent" boolean is_active + string notes "nullable - Notas internas" } REFRESH_TOKEN { @@ -270,6 +367,9 @@ erDiagram time total_transit_time "Suma de tiempos de tránsito" boolean is_active datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } ROUTE_STEP { @@ -280,6 +380,9 @@ erDiagram time standard_transit_time "Tiempo desde parada anterior" string step_type "Origin|Intermediate|Destination" datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } NETWORK_LINK { @@ -292,6 +395,9 @@ erDiagram boolean is_bidirectional "Si aplica en ambas direcciones" boolean is_active datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" } ``` @@ -604,15 +710,38 @@ stateDiagram-v2 --- -## 5. Roles del Sistema (Seed Data) +## 5. Roles del Sistema y Permisos + +### 5.1 Roles (Seed Data) ```sql INSERT INTO roles (id, name, description) VALUES ('11111111-1111-1111-1111-111111111111', 'Admin', 'Gerente de Tráfico - Acceso total'), ('22222222-2222-2222-2222-222222222222', 'Driver', 'Chofer - Solo ve sus envíos'), - ('33333333-3333-3333-3333-333333333333', 'DemoUser', 'Usuario de demostración temporal'); + ('33333333-3333-3333-3333-333333333333', 'DemoUser', 'Usuario de demostración temporal'), + ('44444444-4444-4444-4444-444444444444', 'Warehouse', 'Almacenista - Gestión de carga'); ``` +### 5.2 Permisos por Rol (Inmutables en Código) + +> **Nota:** Los permisos están definidos en `RolePermissions.cs` y NO pueden modificarse en runtime. + +| Recurso | Admin | Driver | Warehouse | DemoUser | +| --------------- | :-----------: | :-----: | :-------: | :------: | +| **Usuarios** | CRUD | - | - | R | +| **Camiones** | CRUD | - | R | R | +| **Choferes** | CRUD | - | R | R | +| **Clientes** | CRUD | - | - | R | +| **Envíos** | CRUD + Assign | ReadOwn | Read | R | +| **Items** | CRU | - | RU | R | +| **Checkpoints** | CR | C | C | R | +| **Rutas** | CRUD | R | R | R | +| **Ubicaciones** | CRUD | R | R | R | +| **Documentos** | CR | ReadOwn | - | R | +| **Fleet Logs** | CR | - | - | R | + +**Leyenda:** C=Create, R=Read, U=Update, D=Delete + --- ## 6. Índices Recomendados @@ -853,6 +982,10 @@ public enum PackagingType { Pallet, Box, Drum, Piece } ### Entidad ShipmentCheckpoint ```csharp +/// +/// Evento de trazabilidad del envío. +/// INMUTABLE: Los checkpoints no se modifican, solo se agregan nuevos. +/// public class ShipmentCheckpoint { public Guid Id { get; set; } @@ -863,15 +996,44 @@ public class ShipmentCheckpoint public DateTime Timestamp { get; set; } public Guid CreatedByUserId { get; set; } + // ========== TRAZABILIDAD DE CARGUEROS (v0.4.2) ========== + + /// Chofer que manejó el paquete en este checkpoint + public Guid? HandledByDriverId { get; set; } + + /// Camión donde se cargó el paquete + public Guid? LoadedOntoTruckId { get; set; } + + /// Tipo de acción: Loaded, Unloaded, Transferred, Delivered, etc. + public string? ActionType { get; set; } + + /// Nombre del custodio anterior (quien entregó) + public string? PreviousCustodian { get; set; } + + /// Nombre del nuevo custodio (quien recibió) + public string? NewCustodian { get; set; } + // Navigation Properties public Shipment Shipment { get; set; } = null!; public Location? Location { get; set; } public User CreatedBy { get; set; } = null!; + public Driver? HandledByDriver { get; set; } + public Truck? LoadedOntoTruck { get; set; } } public enum CheckpointStatus { Loaded, QrScanned, ArrivedHub, DepartedHub, OutForDelivery, DeliveryAttempt, Delivered, Exception } ``` +**Campos de Trazabilidad de Cargueros:** + +| Campo | Tipo | Propósito | +| ------------------- | ----------------- | -------------------------------------------------------- | +| `HandledByDriverId` | UUID (nullable) | Chofer que procesó el paquete en este evento | +| `LoadedOntoTruckId` | UUID (nullable) | Camión donde se cargó/descargó el paquete | +| `ActionType` | string (nullable) | Tipo de acción realizada (Loaded, Unloaded, Transferred) | +| `PreviousCustodian` | string (nullable) | Nombre del custodio que entregó | +| `NewCustodian` | string (nullable) | Nombre del custodio que recibió | + ### Entidad ShipmentDocument ```csharp @@ -1234,6 +1396,9 @@ public class FleetLog ## 14. Entidad Driver Actualizada (C#) ```csharp +/// +/// Chofer de la flotilla con datos legales mexicanos. +/// public class Driver { public Guid Id { get; set; } @@ -1247,6 +1412,32 @@ public class Driver public DriverStatus Status { get; set; } public DateTime CreatedAt { get; set; } + // ========== DATOS LEGALES (v0.4.2) ========== + + /// RFC del chofer para facturación + public string? Rfc { get; set; } + + /// Número de Seguro Social (IMSS) + public string? Nss { get; set; } + + /// Clave Única de Registro de Población + public string? Curp { get; set; } + + /// Tipo de licencia: A, B, C, D, E + public string? LicenseType { get; set; } + + /// Fecha de vencimiento de la licencia + public DateTime? LicenseExpiration { get; set; } + + /// Nombre del contacto de emergencia + public string? EmergencyContact { get; set; } + + /// Teléfono del contacto de emergencia + public string? EmergencyPhone { get; set; } + + /// Fecha de contratación + public DateTime? HireDate { get; set; } + // Navigation Properties public Tenant Tenant { get; set; } = null!; public User User { get; set; } = null!; @@ -1254,6 +1445,7 @@ public class Driver public Truck? CurrentTruck { get; set; } public ICollection Shipments { get; set; } = new List(); public ICollection FleetHistory { get; set; } = new List(); + public ICollection HandledCheckpoints { get; set; } = new List(); } ``` From cad2bac1dfe7332e6aa966e22e96a04abf64fe92 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sat, 13 Dec 2025 20:14:27 +0000 Subject: [PATCH 12/34] fix(docs): Actualizar version a v0.4.3 y mejorar CI con PostgreSQL - README.md: Actualizado estado a v0.4.3 Employee Layer - ci.yml: Agregado PostgreSQL service para validar migraciones - ci.yml: Agregado paso para aplicar migraciones a DB real - ci.yml: Agregado verificacion de schema con psql - ci.yml: Removidos emojis del summary --- .github/workflows/ci.yml | 47 +++++++++++++++++++++++++++++++++++----- README.md | 2 +- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 790cada..30d0d82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ # =================================== # PARHELION CI - Build & Test Pipeline # Se ejecuta en cada push/PR a develop y main +# v0.4.3: Agregado PostgreSQL service para tests de integracion # =================================== name: CI Pipeline @@ -12,14 +13,34 @@ on: branches: [develop, main] jobs: - # ===== BACKEND (.NET 8) ===== + # ===== BACKEND (.NET 8) + PostgreSQL Tests ===== backend: - name: Backend Build & Test + name: Backend Build & Integration Tests runs-on: ubuntu-latest defaults: run: working-directory: ./backend + # PostgreSQL service para tests de integracion reales + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: parhelion_test + POSTGRES_PASSWORD: test_password_ci + POSTGRES_DB: parhelion_test_db + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + # Connection string para tests con PostgreSQL real + ConnectionStrings__DefaultConnection: "Host=localhost;Port=5432;Database=parhelion_test_db;Username=parhelion_test;Password=test_password_ci" + steps: - uses: actions/checkout@v4 @@ -34,8 +55,8 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore - # E2E Integration Tests - Validates entire system flow - - name: Run Integration Tests + # Tests de integracion con InMemory DB (rapidos, no requieren DB real) + - name: Run Integration Tests (InMemory) run: dotnet test --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" - name: Upload Test Results @@ -45,6 +66,20 @@ jobs: name: test-results path: backend/tests/**/test-results.trx + # Validar que las migraciones se aplican correctamente a PostgreSQL + - name: Install EF Core Tools + run: dotnet tool install --global dotnet-ef + + - name: Apply Migrations to PostgreSQL + run: dotnet ef database update --project src/Parhelion.Infrastructure --startup-project src/Parhelion.API + env: + ASPNETCORE_ENVIRONMENT: Testing + + - name: Verify Database Schema + run: | + echo "Verificando que todas las tablas existen en PostgreSQL..." + PGPASSWORD=test_password_ci psql -h localhost -U parhelion_test -d parhelion_test_db -c "\dt" | grep -E "(Tenants|Users|Employees|Shifts|Drivers|Trucks|Locations|Shipments)" && echo "Schema validation passed" + # ===== FRONTEND ADMIN (Angular) ===== frontend-admin: name: Admin Build & Lint @@ -143,11 +178,11 @@ jobs: # ===== RESUMEN FINAL ===== summary: - name: ✅ All Checks Passed + name: All Checks Passed runs-on: ubuntu-latest needs: [docker] if: success() steps: - name: Success - run: echo "🎉 Todos los builds y tests pasaron correctamente!" + run: echo "Todos los builds, tests y validacion de DB pasaron correctamente" diff --git a/README.md b/README.md index fe18b5d..a3dd995 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. -> **Estado:** Development Preview v0.4.2 - Database Layer Implementado ✅ +> **Estado:** Development Preview v0.4.3 - Employee Layer Implementado --- From 9d0d8886f07d1870d02a18eae73db1475ae4d1ee Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sat, 13 Dec 2025 20:17:20 +0000 Subject: [PATCH 13/34] fix(ci): Especificar version 8.0.11 de dotnet-ef para compatibilidad con .NET 8 El error 'System.Runtime, Version=10.0.0.0' ocurria porque dotnet-ef se instalaba con la version mas reciente (10.x) que no es compatible con proyectos .NET 8. Ahora se instala version 8.0.11 explicitamente. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30d0d82..173ee01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,7 @@ jobs: # Validar que las migraciones se aplican correctamente a PostgreSQL - name: Install EF Core Tools - run: dotnet tool install --global dotnet-ef + run: dotnet tool install --global dotnet-ef --version 8.0.11 - name: Apply Migrations to PostgreSQL run: dotnet ef database update --project src/Parhelion.Infrastructure --startup-project src/Parhelion.API From b6f9336e836069b2b4190cf873bbc982590970d1 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sat, 13 Dec 2025 20:23:23 +0000 Subject: [PATCH 14/34] fix(docs): Corregir sintaxis Mermaid en database-schema.md Mermaid no soporta FK UK juntos con comentario. Simplificado a FK con UK incluido en el comentario para evitar parse error. --- database-schema.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database-schema.md b/database-schema.md index 83eb58e..8d75c57 100644 --- a/database-schema.md +++ b/database-schema.md @@ -139,7 +139,7 @@ erDiagram EMPLOYEE { uuid id PK uuid tenant_id FK - uuid user_id FK UK "1:1 con User" + uuid user_id FK "UK - 1:1 con User" string phone string rfc "nullable - RFC fiscal" string nss "nullable - Número de Seguro Social" @@ -172,7 +172,7 @@ erDiagram WAREHOUSE_ZONE { uuid id PK uuid location_id FK - string code UK "A1, B2, COLD-1" + string code "UK - A1, B2, COLD-1" string name string type "Receiving|Storage|Staging|Shipping|ColdChain|Hazmat" boolean is_active @@ -184,7 +184,7 @@ erDiagram WAREHOUSE_OPERATOR { uuid id PK - uuid employee_id FK UK "1:1 con Employee" + uuid employee_id FK "UK - 1:1 con Employee" uuid assigned_location_id FK uuid primary_zone_id FK "nullable" datetime created_at From 04a4e6cdbee4f8f0e1f33e6809d9b571727208df Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sat, 13 Dec 2025 20:27:25 +0000 Subject: [PATCH 15/34] fix(docs): remove duplicate fields in Mermaid ER diagram causing render errors Corrige errores de renderizado en diagramas Mermaid eliminando campos duplicados en entidades TENANT y ROLE del diagrama ER. Fixed Mermaid ER diagram rendering errors by removing duplicate field definitions in TENANT and ROLE entities that were causing 'translate(undefined, NaN)' transform errors. --- database-schema.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/database-schema.md b/database-schema.md index 8d75c57..e474261 100644 --- a/database-schema.md +++ b/database-schema.md @@ -84,9 +84,6 @@ erDiagram datetime updated_at "nullable" boolean is_deleted datetime deleted_at "nullable" - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" } USER { @@ -115,9 +112,6 @@ erDiagram datetime updated_at "nullable" boolean is_deleted datetime deleted_at "nullable" - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" } DRIVER { From 409ea410982de446e7f96038dffecb0982f14d6f Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sun, 14 Dec 2025 15:45:21 +0000 Subject: [PATCH 16/34] feat(wms): Mejoras de esquema WMS v0.4.4 - Inventario, Catalogo, Auditoria WMS schema enhancements v0.4.4 - Inventory, Catalog, Audit Nuevas entidades / New entities: - CatalogItem: Catalogo maestro de productos con SKU unico por tenant - InventoryStock: Stock cuantificado por zona con lote y caducidad - InventoryTransaction: Kardex de movimientos internos (9 tipos) Nuevos servicios / New services: - ICurrentUserService: Interface para usuario actual - CurrentUserService: Implementacion con HttpContext - AuditSaveChangesInterceptor: Automatiza CreatedByUserId/LastModifiedByUserId Modificaciones / Modifications: - BaseEntity: +RowVersion (xmin), +CreatedByUserId, +LastModifiedByUserId - Location: +Latitude, +Longitude (precision 9,6) - ShipmentCheckpoint: +Latitude, +Longitude - ShipmentItem: +ProductId FK a CatalogItem - WarehouseZone: +Navigation properties para inventario Documentacion / Documentation: - CHANGELOG.md: Seccion completa v0.4.4 - database-schema.md: Version 2.5 con nuevas entidades - ci.yml: Validacion de tablas de inventario Migracion: WmsEnhancement044 (23 tablas PostgreSQL) Tests: 8/8 pasando --- .github/workflows/ci.yml | 6 +- CHANGELOG.md | 79 + backend/src/Parhelion.API/Program.cs | 18 +- .../Services/ICurrentUserService.cs | 17 + .../src/Parhelion.Domain/Common/BaseEntity.cs | 26 +- .../Parhelion.Domain/Entities/CatalogItem.cs | 41 + .../src/Parhelion.Domain/Entities/FleetLog.cs | 7 +- .../Entities/InventoryStock.cs | 40 + .../Entities/InventoryTransaction.cs | 46 + .../src/Parhelion.Domain/Entities/Location.cs | 6 + .../Entities/ShipmentCheckpoint.cs | 15 +- .../Parhelion.Domain/Entities/ShipmentItem.cs | 9 + .../Entities/WarehouseZone.cs | 5 + .../Enums/InventoryTransactionType.cs | 34 + .../CatalogItemConfiguration.cs | 46 + .../InventoryStockConfiguration.cs | 59 + .../InventoryTransactionConfiguration.cs | 61 + .../Configurations/LocationConfiguration.cs | 4 + .../ShipmentCheckpointConfiguration.cs | 4 + .../ShipmentItemConfiguration.cs | 8 +- .../AuditSaveChangesInterceptor.cs | 70 + ...251214153448_WmsEnhancement044.Designer.cs | 2403 +++++++++++++++++ .../20251214153448_WmsEnhancement044.cs | 938 +++++++ .../ParhelionDbContextModelSnapshot.cs | 611 +++++ .../Data/ParhelionDbContext.cs | 6 + .../Services/CurrentUserService.cs | 41 + database-schema.md | 86 +- 27 files changed, 4668 insertions(+), 18 deletions(-) create mode 100644 backend/src/Parhelion.Application/Services/ICurrentUserService.cs create mode 100644 backend/src/Parhelion.Domain/Entities/CatalogItem.cs create mode 100644 backend/src/Parhelion.Domain/Entities/InventoryStock.cs create mode 100644 backend/src/Parhelion.Domain/Entities/InventoryTransaction.cs create mode 100644 backend/src/Parhelion.Domain/Enums/InventoryTransactionType.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/CatalogItemConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/InventoryStockConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/InventoryTransactionConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Interceptors/AuditSaveChangesInterceptor.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251214153448_WmsEnhancement044.Designer.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251214153448_WmsEnhancement044.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/CurrentUserService.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 173ee01..a5eb10b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ # =================================== # PARHELION CI - Build & Test Pipeline # Se ejecuta en cada push/PR a develop y main -# v0.4.3: Agregado PostgreSQL service para tests de integracion +# v0.4.4: Agregado verificacion de tablas de inventario (CatalogItems, InventoryStocks, InventoryTransactions) # =================================== name: CI Pipeline @@ -77,8 +77,8 @@ jobs: - name: Verify Database Schema run: | - echo "Verificando que todas las tablas existen en PostgreSQL..." - PGPASSWORD=test_password_ci psql -h localhost -U parhelion_test -d parhelion_test_db -c "\dt" | grep -E "(Tenants|Users|Employees|Shifts|Drivers|Trucks|Locations|Shipments)" && echo "Schema validation passed" + echo "Verificando que todas las tablas existen en PostgreSQL (23 tablas esperadas)..." + PGPASSWORD=test_password_ci psql -h localhost -U parhelion_test -d parhelion_test_db -c "\dt" | grep -E "(Tenants|Users|Employees|Shifts|Drivers|Trucks|Locations|Shipments|CatalogItems|InventoryStocks|InventoryTransactions)" && echo "Schema validation passed (v0.4.4)" # ===== FRONTEND ADMIN (Angular) ===== frontend-admin: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7f924..dcae14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,85 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.4.4] - 2025-12-14 + +### Agregado + +- **Catalogo Maestro de Productos (`CatalogItem`)**: + + - SKU unico por tenant con indice unico + - Dimensiones por defecto (peso, ancho, alto, largo) + - Flags: RequiresRefrigeration, IsHazardous, IsFragile + - Unidad de medida base (Pza, Kg, Lt, Caja) + +- **Inventario Cuantificado (`InventoryStock`)**: + + - Stock por zona de bodega con FK a `WarehouseZone` + - Numero de lote (`BatchNumber`) y fecha de caducidad + - Cantidad reservada y disponible + - Costo unitario para valuacion + - Indice unico compuesto: `(ZoneId, ProductId, BatchNumber)` + - Indice filtrado para productos proximos a caducar + +- **Kardex de Movimientos (`InventoryTransaction`)**: + + - Bitacora de todos los movimientos internos + - Tipos: Receipt, PutAway, InternalMove, Picking, Packing, Dispatch, Adjustment, Scrap, Return + - FK a zonas origen/destino, usuario ejecutor, envio relacionado + - Indices para consultas por producto y fecha + +- **Automatizacion de Auditoria (`AuditSaveChangesInterceptor`)**: + + - Llena automaticamente `CreatedByUserId` en inserts + - Llena automaticamente `LastModifiedByUserId` en updates + - Maneja `DeletedAt` en soft deletes + - Servicios: `ICurrentUserService`, `CurrentUserService` + +- **Campos de Auditoria en `BaseEntity`**: + + - `CreatedByUserId` (Guid?) - Usuario que creo el registro + - `LastModifiedByUserId` (Guid?) - Ultimo usuario que modifico + - `RowVersion` (uint) - Token de concurrencia optimista + +- **Geolocalizacion**: + + - `Latitude` y `Longitude` (decimal, precision 9,6) en `Location` + - `Latitude` y `Longitude` en `ShipmentCheckpoint` + +- **FK `ProductId` en `ShipmentItem`**: + - Referencia opcional a `CatalogItem` + - Campos descriptivos se mantienen para override + +### Modificado + +- `BaseEntity`: +RowVersion, +CreatedByUserId, +LastModifiedByUserId +- `ShipmentItem`: +ProductId FK a CatalogItem +- `WarehouseZone`: +InventoryStocks, +OriginTransactions, +DestinationTransactions +- `Location`: +Latitude, +Longitude +- `ShipmentCheckpoint`: +Latitude, +Longitude, CreatedByUserId marcado con `new` +- `FleetLog`: CreatedByUserId marcado con `new` +- `ParhelionDbContext`: +CatalogItems, +InventoryStocks, +InventoryTransactions DbSets +- `Program.cs`: +AuditSaveChangesInterceptor, version 0.4.4 + +### Configuraciones EF Core + +- `CatalogItemConfiguration`: SKU unico por tenant, precision de decimales +- `InventoryStockConfiguration`: Concurrencia xmin, indices filtrados +- `InventoryTransactionConfiguration`: Relaciones con zonas, usuario, envio + +### Migracion + +- `WmsEnhancement044` aplicada a PostgreSQL +- 3 nuevas tablas: CatalogItems, InventoryStocks, InventoryTransactions +- Total: 23 tablas en base de datos + +### Tests + +- 8 tests de integracion pasando +- Compatibilidad verificada con esquema anterior + +--- + ## [0.4.3] - 2025-12-13 ### Agregado diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index b7d0d22..daa5afc 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -3,8 +3,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Parhelion.Application.Auth; +using Parhelion.Application.Services; using Parhelion.Infrastructure.Auth; using Parhelion.Infrastructure.Data; +using Parhelion.Infrastructure.Data.Interceptors; +using Parhelion.Infrastructure.Services; var builder = WebApplication.CreateBuilder(args); @@ -13,17 +16,26 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// ========== INFRASTRUCTURE SERVICES ========== +builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // ========== DATABASE ========== // Usar connection string de variables de entorno o appsettings var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); -builder.Services.AddDbContext(options => +builder.Services.AddDbContext((sp, options) => +{ + var auditInterceptor = sp.GetRequiredService(); options.UseNpgsql(connectionString, npgsqlOptions => { npgsqlOptions.MigrationsAssembly("Parhelion.Infrastructure"); npgsqlOptions.EnableRetryOnFailure(3); - })); + }) + .AddInterceptors(auditInterceptor); +}); // ========== AUTH SERVICES ========== builder.Services.AddScoped(); @@ -106,7 +118,7 @@ status = "healthy", service = "Parhelion API", timestamp = DateTime.UtcNow, - version = "0.4.2", + version = "0.4.4", database = "PostgreSQL" }) .WithName("HealthCheck") diff --git a/backend/src/Parhelion.Application/Services/ICurrentUserService.cs b/backend/src/Parhelion.Application/Services/ICurrentUserService.cs new file mode 100644 index 0000000..05a8eb5 --- /dev/null +++ b/backend/src/Parhelion.Application/Services/ICurrentUserService.cs @@ -0,0 +1,17 @@ +namespace Parhelion.Application.Services; + +/// +/// Servicio para obtener información del usuario actual desde el contexto HTTP. +/// Se usa para auditoría automática de CreatedByUserId/LastModifiedByUserId. +/// +public interface ICurrentUserService +{ + /// ID del usuario autenticado (null si anónimo) + Guid? UserId { get; } + + /// ID del tenant del usuario actual + Guid? TenantId { get; } + + /// Indica si hay un usuario autenticado + bool IsAuthenticated { get; } +} diff --git a/backend/src/Parhelion.Domain/Common/BaseEntity.cs b/backend/src/Parhelion.Domain/Common/BaseEntity.cs index b31f3fe..815cf58 100644 --- a/backend/src/Parhelion.Domain/Common/BaseEntity.cs +++ b/backend/src/Parhelion.Domain/Common/BaseEntity.cs @@ -1,8 +1,10 @@ +using System.ComponentModel.DataAnnotations; + namespace Parhelion.Domain.Common; /// /// Entidad base para todas las entidades del sistema. -/// Incluye Soft Delete y Audit Trail automático. +/// Incluye Soft Delete, Audit Trail automático, y Concurrencia Optimista. /// public abstract class BaseEntity { @@ -15,14 +17,34 @@ public abstract class BaseEntity /// public bool IsDeleted { get; set; } public DateTime? DeletedAt { get; set; } + + /// + /// Concurrency token mapeado a PostgreSQL xmin. + /// NO insertar/modificar manualmente - el DB lo maneja. + /// + [Timestamp] + public uint RowVersion { get; set; } + + /// + /// Usuario que creó el registro. + /// Se llena automáticamente via AuditSaveChangesInterceptor. + /// + public Guid? CreatedByUserId { get; set; } + + /// + /// Último usuario que modificó el registro. + /// Se llena automáticamente via AuditSaveChangesInterceptor. + /// + public Guid? LastModifiedByUserId { get; set; } } /// /// Entidad base para entidades que pertenecen a un tenant específico. -/// Hereda de BaseEntity para incluir Soft Delete y Audit Trail. +/// Hereda de BaseEntity para incluir Soft Delete, Audit Trail, y Concurrencia. /// Todas las queries automáticamente filtran por TenantId via Query Filters. /// public abstract class TenantEntity : BaseEntity { public Guid TenantId { get; set; } } + diff --git a/backend/src/Parhelion.Domain/Entities/CatalogItem.cs b/backend/src/Parhelion.Domain/Entities/CatalogItem.cs new file mode 100644 index 0000000..bd8bd0e --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/CatalogItem.cs @@ -0,0 +1,41 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Catálogo maestro de productos/SKUs. +/// Normaliza datos fijos que se repiten en ShipmentItems. +/// +public class CatalogItem : TenantEntity +{ + public string Sku { get; set; } = null!; + public string Name { get; set; } = null!; + public string? Description { get; set; } + + /// Unidad de medida base: Pza, Kg, Lt, Caja + public string BaseUom { get; set; } = "Pza"; + + // ========== DIMENSIONES POR DEFECTO ========== + + public decimal DefaultWeightKg { get; set; } + public decimal DefaultWidthCm { get; set; } + public decimal DefaultHeightCm { get; set; } + public decimal DefaultLengthCm { get; set; } + + /// Volumen calculado en metros cúbicos + public decimal DefaultVolumeM3 => (DefaultWidthCm * DefaultHeightCm * DefaultLengthCm) / 1_000_000m; + + // ========== FLAGS DE MANEJO ESPECIAL ========== + + public bool RequiresRefrigeration { get; set; } + public bool IsHazardous { get; set; } + public bool IsFragile { get; set; } + + public bool IsActive { get; set; } = true; + + // ========== NAVIGATION PROPERTIES ========== + + public Tenant Tenant { get; set; } = null!; + public ICollection ShipmentItems { get; set; } = new List(); + public ICollection InventoryStocks { get; set; } = new List(); +} diff --git a/backend/src/Parhelion.Domain/Entities/FleetLog.cs b/backend/src/Parhelion.Domain/Entities/FleetLog.cs index 4169cd7..a7e4f02 100644 --- a/backend/src/Parhelion.Domain/Entities/FleetLog.cs +++ b/backend/src/Parhelion.Domain/Entities/FleetLog.cs @@ -20,7 +20,12 @@ public class FleetLog : TenantEntity public Guid NewTruckId { get; set; } public FleetLogReason Reason { get; set; } public DateTime Timestamp { get; set; } - public Guid CreatedByUserId { get; set; } + + /// + /// Usuario que registró el cambio (siempre requerido para FleetLog). + /// Sobrescribe el campo nullable de BaseEntity. + /// + public new Guid CreatedByUserId { get; set; } // Navigation Properties public Tenant Tenant { get; set; } = null!; diff --git a/backend/src/Parhelion.Domain/Entities/InventoryStock.cs b/backend/src/Parhelion.Domain/Entities/InventoryStock.cs new file mode 100644 index 0000000..b7f8214 --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/InventoryStock.cs @@ -0,0 +1,40 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// Inventario físico cuantificado por zona y lote. +/// Representa el saldo actual de un producto en una ubicación específica. +/// +public class InventoryStock : TenantEntity +{ + public Guid ZoneId { get; set; } + public Guid ProductId { get; set; } + + /// Cantidad total en esta ubicación + public decimal Quantity { get; set; } + + /// Cantidad reservada para envíos pendientes + public decimal QuantityReserved { get; set; } + + /// Cantidad disponible = Quantity - QuantityReserved + public decimal QuantityAvailable => Quantity - QuantityReserved; + + /// Número de lote (vital para trazabilidad) + public string? BatchNumber { get; set; } + + /// Fecha de caducidad (vital para perecederos) + public DateTime? ExpiryDate { get; set; } + + /// Última fecha de conteo físico + public DateTime? LastCountDate { get; set; } + + /// Costo unitario para valuación de inventario + public decimal? UnitCost { get; set; } + + // ========== NAVIGATION PROPERTIES ========== + + public Tenant Tenant { get; set; } = null!; + public WarehouseZone Zone { get; set; } = null!; + public CatalogItem Product { get; set; } = null!; +} diff --git a/backend/src/Parhelion.Domain/Entities/InventoryTransaction.cs b/backend/src/Parhelion.Domain/Entities/InventoryTransaction.cs new file mode 100644 index 0000000..a660bee --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/InventoryTransaction.cs @@ -0,0 +1,46 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Bitácora de movimientos de inventario (Kardex). +/// Registra TODOS los movimientos: entradas, salidas, movimientos internos, ajustes. +/// INMUTABLE: Las transacciones no se modifican, solo se agregan. +/// +public class InventoryTransaction : TenantEntity +{ + public Guid ProductId { get; set; } + + /// Zona de origen (null si es entrada externa) + public Guid? OriginZoneId { get; set; } + + /// Zona de destino (null si es salida) + public Guid? DestinationZoneId { get; set; } + + public decimal Quantity { get; set; } + + public InventoryTransactionType TransactionType { get; set; } + + /// Usuario que ejecutó el movimiento + public Guid PerformedByUserId { get; set; } + + /// Envío relacionado (si aplica) + public Guid? ShipmentId { get; set; } + + /// Lote afectado + public string? BatchNumber { get; set; } + + public string? Remarks { get; set; } + + public DateTime Timestamp { get; set; } + + // ========== NAVIGATION PROPERTIES ========== + + public Tenant Tenant { get; set; } = null!; + public CatalogItem Product { get; set; } = null!; + public WarehouseZone? OriginZone { get; set; } + public WarehouseZone? DestinationZone { get; set; } + public User PerformedBy { get; set; } = null!; + public Shipment? Shipment { get; set; } +} diff --git a/backend/src/Parhelion.Domain/Entities/Location.cs b/backend/src/Parhelion.Domain/Entities/Location.cs index 5a667bd..a687873 100644 --- a/backend/src/Parhelion.Domain/Entities/Location.cs +++ b/backend/src/Parhelion.Domain/Entities/Location.cs @@ -14,6 +14,12 @@ public class Location : TenantEntity public LocationType Type { get; set; } public string FullAddress { get; set; } = null!; + /// Latitud para geolocalización + public decimal? Latitude { get; set; } + + /// Longitud para geolocalización + public decimal? Longitude { get; set; } + /// /// Flag: Puede recibir mercancía. /// diff --git a/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs b/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs index b7a662a..c5f43cd 100644 --- a/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs +++ b/backend/src/Parhelion.Domain/Entities/ShipmentCheckpoint.cs @@ -14,7 +14,12 @@ public class ShipmentCheckpoint : BaseEntity public CheckpointStatus StatusCode { get; set; } public string? Remarks { get; set; } public DateTime Timestamp { get; set; } - public Guid CreatedByUserId { get; set; } + + /// + /// Usuario que registró el checkpoint (siempre requerido). + /// Sobrescribe el campo nullable de BaseEntity. + /// + public new Guid CreatedByUserId { get; set; } // ========== TRAZABILIDAD DE CARGUEROS ========== @@ -35,6 +40,14 @@ public class ShipmentCheckpoint : BaseEntity /// Almacenista que manejó el paquete en este checkpoint public Guid? HandledByWarehouseOperatorId { get; set; } + + // ========== GEOLOCALIZACIÓN ========== + + /// Latitud donde se registró el checkpoint + public decimal? Latitude { get; set; } + + /// Longitud donde se registró el checkpoint + public decimal? Longitude { get; set; } // Navigation Properties public Shipment Shipment { get; set; } = null!; diff --git a/backend/src/Parhelion.Domain/Entities/ShipmentItem.cs b/backend/src/Parhelion.Domain/Entities/ShipmentItem.cs index 61d23db..5035c02 100644 --- a/backend/src/Parhelion.Domain/Entities/ShipmentItem.cs +++ b/backend/src/Parhelion.Domain/Entities/ShipmentItem.cs @@ -6,10 +6,18 @@ namespace Parhelion.Domain.Entities; /// /// Partida individual dentro de un envío (SKU, dimensiones, peso). /// Incluye cálculo de peso volumétrico para cotizaciones. +/// ProductId es opcional para compatibilidad con envíos sin catálogo. /// public class ShipmentItem : BaseEntity { public Guid ShipmentId { get; set; } + + /// + /// FK a CatalogItem (nullable para compatibilidad con envíos sin catálogo). + /// Si se especifica, los campos de dimensiones pueden ser override del catálogo. + /// + public Guid? ProductId { get; set; } + public string? Sku { get; set; } public string Description { get; set; } = null!; public PackagingType PackagingType { get; set; } @@ -42,4 +50,5 @@ public class ShipmentItem : BaseEntity // Navigation Properties public Shipment Shipment { get; set; } = null!; + public CatalogItem? Product { get; set; } } diff --git a/backend/src/Parhelion.Domain/Entities/WarehouseZone.cs b/backend/src/Parhelion.Domain/Entities/WarehouseZone.cs index ee27093..ac261f0 100644 --- a/backend/src/Parhelion.Domain/Entities/WarehouseZone.cs +++ b/backend/src/Parhelion.Domain/Entities/WarehouseZone.cs @@ -27,4 +27,9 @@ public class WarehouseZone : BaseEntity public Location Location { get; set; } = null!; public ICollection AssignedOperators { get; set; } = new List(); + + // Inventario y movimientos + public ICollection InventoryStocks { get; set; } = new List(); + public ICollection OriginTransactions { get; set; } = new List(); + public ICollection DestinationTransactions { get; set; } = new List(); } diff --git a/backend/src/Parhelion.Domain/Enums/InventoryTransactionType.cs b/backend/src/Parhelion.Domain/Enums/InventoryTransactionType.cs new file mode 100644 index 0000000..2cb8259 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/InventoryTransactionType.cs @@ -0,0 +1,34 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipos de transacciones de inventario para el Kardex. +/// +public enum InventoryTransactionType +{ + /// Entrada de mercancía externa (de proveedor) + Receipt = 0, + + /// Almacenamiento (de recepción a ubicación) + PutAway = 1, + + /// Movimiento interno entre zonas + InternalMove = 2, + + /// Surtido para envío (picking) + Picking = 3, + + /// Empaque para despacho + Packing = 4, + + /// Salida del almacén + Dispatch = 5, + + /// Ajuste de inventario (+/-) + Adjustment = 6, + + /// Baja por daño/caducidad + Scrap = 7, + + /// Devolución de cliente + Return = 8 +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/CatalogItemConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/CatalogItemConfiguration.cs new file mode 100644 index 0000000..a9b9352 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/CatalogItemConfiguration.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class CatalogItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(c => c.Id); + + builder.Property(c => c.Sku) + .HasMaxLength(50) + .IsRequired(); + + builder.Property(c => c.Name) + .HasMaxLength(200) + .IsRequired(); + + builder.Property(c => c.Description) + .HasMaxLength(1000); + + builder.Property(c => c.BaseUom) + .HasMaxLength(20); + + // Dimensiones por defecto + builder.Property(c => c.DefaultWeightKg).HasPrecision(10, 3); + builder.Property(c => c.DefaultWidthCm).HasPrecision(10, 2); + builder.Property(c => c.DefaultHeightCm).HasPrecision(10, 2); + builder.Property(c => c.DefaultLengthCm).HasPrecision(10, 2); + + // Ignorar propiedad calculada + builder.Ignore(c => c.DefaultVolumeM3); + + // Índice único: SKU por tenant + builder.HasIndex(c => new { c.TenantId, c.Sku }) + .IsUnique(); + + // Relaciones + builder.HasOne(c => c.Tenant) + .WithMany() + .HasForeignKey(c => c.TenantId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/InventoryStockConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/InventoryStockConfiguration.cs new file mode 100644 index 0000000..b3b7cd2 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/InventoryStockConfiguration.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class InventoryStockConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(i => i.Id); + + builder.Property(i => i.Quantity).HasPrecision(18, 4); + builder.Property(i => i.QuantityReserved).HasPrecision(18, 4); + builder.Property(i => i.UnitCost).HasPrecision(18, 4); + builder.Property(i => i.BatchNumber).HasMaxLength(100); + + // Ignorar propiedad calculada + builder.Ignore(i => i.QuantityAvailable); + + // PostgreSQL xmin concurrency token + // Usa la columna del sistema xmin - NO crea columna física + builder.Property(i => i.RowVersion) + .HasColumnName("xmin") + .HasColumnType("xid") + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); + + // ========== ÍNDICES ========== + + // Índice único: Producto + Zona + Lote + builder.HasIndex(i => new { i.ZoneId, i.ProductId, i.BatchNumber }) + .IsUnique(); + + // Índice para búsqueda de stock por producto + builder.HasIndex(i => new { i.TenantId, i.ProductId }); + + // Índice para productos próximos a caducar + builder.HasIndex(i => new { i.TenantId, i.ExpiryDate }) + .HasFilter("\"ExpiryDate\" IS NOT NULL"); + + // ========== RELACIONES ========== + + builder.HasOne(i => i.Tenant) + .WithMany() + .HasForeignKey(i => i.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(i => i.Zone) + .WithMany(z => z.InventoryStocks) + .HasForeignKey(i => i.ZoneId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(i => i.Product) + .WithMany(p => p.InventoryStocks) + .HasForeignKey(i => i.ProductId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/InventoryTransactionConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/InventoryTransactionConfiguration.cs new file mode 100644 index 0000000..869deb7 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/InventoryTransactionConfiguration.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +public class InventoryTransactionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(t => t.Id); + + builder.Property(t => t.Quantity).HasPrecision(18, 4); + builder.Property(t => t.BatchNumber).HasMaxLength(100); + builder.Property(t => t.Remarks).HasMaxLength(500); + + // ========== ÍNDICES ========== + + // Kardex por producto + builder.HasIndex(t => new { t.TenantId, t.ProductId, t.Timestamp }); + + // Historial temporal + builder.HasIndex(t => new { t.TenantId, t.Timestamp }); + + // Transacciones por envío + builder.HasIndex(t => t.ShipmentId) + .HasFilter("\"ShipmentId\" IS NOT NULL"); + + // ========== RELACIONES ========== + + builder.HasOne(t => t.Tenant) + .WithMany() + .HasForeignKey(t => t.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(t => t.Product) + .WithMany() + .HasForeignKey(t => t.ProductId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(t => t.OriginZone) + .WithMany(z => z.OriginTransactions) + .HasForeignKey(t => t.OriginZoneId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(t => t.DestinationZone) + .WithMany(z => z.DestinationTransactions) + .HasForeignKey(t => t.DestinationZoneId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(t => t.PerformedBy) + .WithMany() + .HasForeignKey(t => t.PerformedByUserId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(t => t.Shipment) + .WithMany() + .HasForeignKey(t => t.ShipmentId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/LocationConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/LocationConfiguration.cs index ec6c834..e2a7ed5 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Configurations/LocationConfiguration.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/LocationConfiguration.cs @@ -21,6 +21,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(l => l.FullAddress) .IsRequired() .HasMaxLength(500); + + // Geolocalización + builder.Property(l => l.Latitude).HasPrecision(9, 6); + builder.Property(l => l.Longitude).HasPrecision(9, 6); // Código único por tenant (estilo aeropuerto: MTY, GDL, MM) builder.HasIndex(l => new { l.TenantId, l.Code }).IsUnique(); diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentCheckpointConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentCheckpointConfiguration.cs index 269b506..d96de42 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentCheckpointConfiguration.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentCheckpointConfiguration.cs @@ -22,6 +22,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(sc => sc.NewCustodian) .HasMaxLength(200); + + // Geolocalización + builder.Property(sc => sc.Latitude).HasPrecision(9, 6); + builder.Property(sc => sc.Longitude).HasPrecision(9, 6); // Índices para trazabilidad builder.HasIndex(sc => sc.ShipmentId); diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentItemConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentItemConfiguration.cs index ceb3e55..cd434b9 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentItemConfiguration.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ShipmentItemConfiguration.cs @@ -39,10 +39,16 @@ public void Configure(EntityTypeBuilder builder) builder.Ignore(si => si.VolumeM3); builder.Ignore(si => si.VolumetricWeightKg); - // Relación + // Relación con Shipment builder.HasOne(si => si.Shipment) .WithMany(s => s.Items) .HasForeignKey(si => si.ShipmentId) .OnDelete(DeleteBehavior.Cascade); + + // Relación con CatalogItem (opcional para compatibilidad) + builder.HasOne(si => si.Product) + .WithMany(p => p.ShipmentItems) + .HasForeignKey(si => si.ProductId) + .OnDelete(DeleteBehavior.SetNull); } } diff --git a/backend/src/Parhelion.Infrastructure/Data/Interceptors/AuditSaveChangesInterceptor.cs b/backend/src/Parhelion.Infrastructure/Data/Interceptors/AuditSaveChangesInterceptor.cs new file mode 100644 index 0000000..a721c61 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Interceptors/AuditSaveChangesInterceptor.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Parhelion.Application.Services; +using Parhelion.Domain.Common; + +namespace Parhelion.Infrastructure.Data.Interceptors; + +/// +/// Interceptor que automáticamente llena campos de auditoría antes de guardar. +/// - CreatedAt, CreatedByUserId en inserts +/// - UpdatedAt, LastModifiedByUserId en updates +/// - DeletedAt en soft deletes +/// +public class AuditSaveChangesInterceptor : SaveChangesInterceptor +{ + private readonly ICurrentUserService _currentUserService; + + public AuditSaveChangesInterceptor(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + + public override InterceptionResult SavingChanges( + DbContextEventData eventData, + InterceptionResult result) + { + UpdateAuditFields(eventData.Context); + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + UpdateAuditFields(eventData.Context); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private void UpdateAuditFields(DbContext? context) + { + if (context == null) return; + + var now = DateTime.UtcNow; + var userId = _currentUserService.UserId; + + foreach (var entry in context.ChangeTracker.Entries()) + { + switch (entry.State) + { + case EntityState.Added: + entry.Entity.CreatedAt = now; + entry.Entity.CreatedByUserId = userId; + entry.Entity.IsDeleted = false; + break; + + case EntityState.Modified: + entry.Entity.UpdatedAt = now; + entry.Entity.LastModifiedByUserId = userId; + + // Soft delete timestamp + if (entry.Entity.IsDeleted && entry.Entity.DeletedAt == null) + { + entry.Entity.DeletedAt = now; + } + break; + } + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251214153448_WmsEnhancement044.Designer.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251214153448_WmsEnhancement044.Designer.cs new file mode 100644 index 0000000..862f060 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251214153448_WmsEnhancement044.Designer.cs @@ -0,0 +1,2403 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + [Migration("20251214153448_WmsEnhancement044")] + partial class WmsEnhancement044 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaseUom") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DefaultHeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultLengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultWeightKg") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("DefaultWidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Sku") + .IsUnique(); + + b.ToTable("CatalogItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PreferredProductTypes") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TaxId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "CompanyName"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LicenseExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LicenseType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Curp") + .HasMaxLength(18) + .HasColumnType("character varying(18)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Department") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("EmergencyContact") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Nss") + .HasMaxLength(11) + .HasColumnType("character varying(11)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Rfc") + .HasMaxLength(13) + .HasColumnType("character varying(13)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShiftId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ShiftId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Department"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastCountDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QuantityReserved") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZoneId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("TenantId", "ExpiryDate") + .HasFilter("\"ExpiryDate\" IS NOT NULL"); + + b.HasIndex("TenantId", "ProductId"); + + b.HasIndex("ZoneId", "ProductId", "BatchNumber") + .IsUnique(); + + b.ToTable("InventoryStocks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationZoneId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginZoneId") + .HasColumnType("uuid"); + + b.Property("PerformedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Remarks") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationZoneId"); + + b.HasIndex("OriginZoneId"); + + b.HasIndex("PerformedByUserId"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId") + .HasFilter("\"ShipmentId\" IS NOT NULL"); + + b.HasIndex("TenantId", "Timestamp"); + + b.HasIndex("TenantId", "ProductId", "Timestamp"); + + b.ToTable("InventoryTransactions"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DaysOfWeek") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "IsActive"); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientClientId") + .HasColumnType("uuid"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("RecipientClientId"); + + b.HasIndex("SenderId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HandledByDriverId") + .HasColumnType("uuid"); + + b.Property("HandledByWarehouseOperatorId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("LoadedOntoTruckId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("NewCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PreviousCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("HandledByDriverId"); + + b.HasIndex("HandledByWarehouseOperatorId"); + + b.HasIndex("LoadedOntoTruckId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentOdometerKm") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineNumber") + .HasColumnType("text"); + + b.Property("InsuranceExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("InsurancePolicy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NextMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationNumber") + .HasColumnType("text"); + + b.Property("Vin") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("IsSuperAdmin") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedLocationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PrimaryZoneId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssignedLocationId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("PrimaryZoneId"); + + b.ToTable("WarehouseOperators"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId", "Code") + .IsUnique(); + + b.ToTable("WarehouseZones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", null) + .WithMany("Drivers") + .HasForeignKey("TenantId"); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.HasOne("Parhelion.Domain.Entities.Shift", "Shift") + .WithMany("Employees") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Employee") + .HasForeignKey("Parhelion.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Shift"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("InventoryStocks") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "Zone") + .WithMany("InventoryStocks") + .HasForeignKey("ZoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Tenant"); + + b.Navigation("Zone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "DestinationZone") + .WithMany("DestinationTransactions") + .HasForeignKey("DestinationZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "OriginZone") + .WithMany("OriginTransactions") + .HasForeignKey("OriginZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.User", "PerformedBy") + .WithMany() + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany() + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationZone"); + + b.Navigation("OriginZone"); + + b.Navigation("PerformedBy"); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Client", "RecipientClient") + .WithMany("ShipmentsAsRecipient") + .HasForeignKey("RecipientClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Client", "Sender") + .WithMany("ShipmentsAsSender") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("RecipientClient"); + + b.Navigation("Sender"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "HandledByDriver") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByDriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseOperator", "HandledByWarehouseOperator") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByWarehouseOperatorId"); + + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") + .WithMany("LoadedCheckpoints") + .HasForeignKey("LoadedOntoTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("HandledByDriver"); + + b.Navigation("HandledByWarehouseOperator"); + + b.Navigation("LoadedOntoTruck"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("ShipmentItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "AssignedLocation") + .WithMany("AssignedWarehouseOperators") + .HasForeignKey("AssignedLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("WarehouseOperator") + .HasForeignKey("Parhelion.Domain.Entities.WarehouseOperator", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "PrimaryZone") + .WithMany("AssignedOperators") + .HasForeignKey("PrimaryZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedLocation"); + + b.Navigation("Employee"); + + b.Navigation("PrimaryZone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Zones") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Navigation("InventoryStocks"); + + b.Navigation("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Navigation("ShipmentsAsRecipient"); + + b.Navigation("ShipmentsAsSender"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("HandledCheckpoints"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Navigation("Driver"); + + b.Navigation("WarehouseOperator"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("AssignedWarehouseOperators"); + + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("LoadedCheckpoints"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Navigation("HandledCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Navigation("AssignedOperators"); + + b.Navigation("DestinationTransactions"); + + b.Navigation("InventoryStocks"); + + b.Navigation("OriginTransactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251214153448_WmsEnhancement044.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251214153448_WmsEnhancement044.cs new file mode 100644 index 0000000..38107fc --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251214153448_WmsEnhancement044.cs @@ -0,0 +1,938 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + /// + public partial class WmsEnhancement044 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "WarehouseZones", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "WarehouseZones", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "WarehouseZones", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "WarehouseOperators", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "WarehouseOperators", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "WarehouseOperators", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Users", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Users", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Users", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Trucks", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Trucks", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Trucks", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Tenants", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Tenants", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Tenants", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Shipments", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Shipments", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Shipments", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "ShipmentItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "ShipmentItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "ProductId", + table: "ShipmentItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "ShipmentItems", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "ShipmentDocuments", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "ShipmentDocuments", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "ShipmentDocuments", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "ShipmentCheckpoints", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Latitude", + table: "ShipmentCheckpoints", + type: "numeric(9,6)", + precision: 9, + scale: 6, + nullable: true); + + migrationBuilder.AddColumn( + name: "Longitude", + table: "ShipmentCheckpoints", + type: "numeric(9,6)", + precision: 9, + scale: 6, + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "ShipmentCheckpoints", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Shifts", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Shifts", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Shifts", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "RouteSteps", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "RouteSteps", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "RouteSteps", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "RouteBlueprints", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "RouteBlueprints", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "RouteBlueprints", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Roles", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Roles", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Roles", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "RefreshTokens", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "RefreshTokens", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "RefreshTokens", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "NetworkLinks", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "NetworkLinks", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "NetworkLinks", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Locations", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Locations", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Latitude", + table: "Locations", + type: "numeric(9,6)", + precision: 9, + scale: 6, + nullable: true); + + migrationBuilder.AddColumn( + name: "Longitude", + table: "Locations", + type: "numeric(9,6)", + precision: 9, + scale: 6, + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Locations", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "FleetLogs", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "FleetLogs", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Employees", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Employees", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Employees", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Drivers", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Drivers", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Drivers", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedByUserId", + table: "Clients", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedByUserId", + table: "Clients", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "xmin", + table: "Clients", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.CreateTable( + name: "CatalogItems", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Sku = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + BaseUom = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + DefaultWeightKg = table.Column(type: "numeric(10,3)", precision: 10, scale: 3, nullable: false), + DefaultWidthCm = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + DefaultHeightCm = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + DefaultLengthCm = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + RequiresRefrigeration = table.Column(type: "boolean", nullable: false), + IsHazardous = table.Column(type: "boolean", nullable: false), + IsFragile = table.Column(type: "boolean", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + xmin = table.Column(type: "xid", rowVersion: true, nullable: false), + CreatedByUserId = table.Column(type: "uuid", nullable: true), + LastModifiedByUserId = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CatalogItems", x => x.Id); + table.ForeignKey( + name: "FK_CatalogItems_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "InventoryStocks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ZoneId = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + QuantityReserved = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + BatchNumber = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ExpiryDate = table.Column(type: "timestamp with time zone", nullable: true), + LastCountDate = table.Column(type: "timestamp with time zone", nullable: true), + UnitCost = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + xmin = table.Column(type: "xid", rowVersion: true, nullable: false), + CreatedByUserId = table.Column(type: "uuid", nullable: true), + LastModifiedByUserId = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InventoryStocks", x => x.Id); + table.ForeignKey( + name: "FK_InventoryStocks_CatalogItems_ProductId", + column: x => x.ProductId, + principalTable: "CatalogItems", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_InventoryStocks_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_InventoryStocks_WarehouseZones_ZoneId", + column: x => x.ZoneId, + principalTable: "WarehouseZones", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "InventoryTransactions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + OriginZoneId = table.Column(type: "uuid", nullable: true), + DestinationZoneId = table.Column(type: "uuid", nullable: true), + Quantity = table.Column(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false), + TransactionType = table.Column(type: "integer", nullable: false), + PerformedByUserId = table.Column(type: "uuid", nullable: false), + ShipmentId = table.Column(type: "uuid", nullable: true), + BatchNumber = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + Remarks = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + xmin = table.Column(type: "xid", rowVersion: true, nullable: false), + CreatedByUserId = table.Column(type: "uuid", nullable: true), + LastModifiedByUserId = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InventoryTransactions", x => x.Id); + table.ForeignKey( + name: "FK_InventoryTransactions_CatalogItems_ProductId", + column: x => x.ProductId, + principalTable: "CatalogItems", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_InventoryTransactions_Shipments_ShipmentId", + column: x => x.ShipmentId, + principalTable: "Shipments", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_InventoryTransactions_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_InventoryTransactions_Users_PerformedByUserId", + column: x => x.PerformedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_InventoryTransactions_WarehouseZones_DestinationZoneId", + column: x => x.DestinationZoneId, + principalTable: "WarehouseZones", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_InventoryTransactions_WarehouseZones_OriginZoneId", + column: x => x.OriginZoneId, + principalTable: "WarehouseZones", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_ShipmentItems_ProductId", + table: "ShipmentItems", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_CatalogItems_TenantId_Sku", + table: "CatalogItems", + columns: new[] { "TenantId", "Sku" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InventoryStocks_ProductId", + table: "InventoryStocks", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryStocks_TenantId_ExpiryDate", + table: "InventoryStocks", + columns: new[] { "TenantId", "ExpiryDate" }, + filter: "\"ExpiryDate\" IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryStocks_TenantId_ProductId", + table: "InventoryStocks", + columns: new[] { "TenantId", "ProductId" }); + + migrationBuilder.CreateIndex( + name: "IX_InventoryStocks_ZoneId_ProductId_BatchNumber", + table: "InventoryStocks", + columns: new[] { "ZoneId", "ProductId", "BatchNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InventoryTransactions_DestinationZoneId", + table: "InventoryTransactions", + column: "DestinationZoneId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryTransactions_OriginZoneId", + table: "InventoryTransactions", + column: "OriginZoneId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryTransactions_PerformedByUserId", + table: "InventoryTransactions", + column: "PerformedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryTransactions_ProductId", + table: "InventoryTransactions", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryTransactions_ShipmentId", + table: "InventoryTransactions", + column: "ShipmentId", + filter: "\"ShipmentId\" IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryTransactions_TenantId_ProductId_Timestamp", + table: "InventoryTransactions", + columns: new[] { "TenantId", "ProductId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_InventoryTransactions_TenantId_Timestamp", + table: "InventoryTransactions", + columns: new[] { "TenantId", "Timestamp" }); + + migrationBuilder.AddForeignKey( + name: "FK_ShipmentItems_CatalogItems_ProductId", + table: "ShipmentItems", + column: "ProductId", + principalTable: "CatalogItems", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ShipmentItems_CatalogItems_ProductId", + table: "ShipmentItems"); + + migrationBuilder.DropTable( + name: "InventoryStocks"); + + migrationBuilder.DropTable( + name: "InventoryTransactions"); + + migrationBuilder.DropTable( + name: "CatalogItems"); + + migrationBuilder.DropIndex( + name: "IX_ShipmentItems_ProductId", + table: "ShipmentItems"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "WarehouseZones"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "WarehouseZones"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "WarehouseZones"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "WarehouseOperators"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "WarehouseOperators"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "WarehouseOperators"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Users"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Tenants"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Tenants"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Tenants"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Shipments"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Shipments"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Shipments"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "ShipmentItems"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "ShipmentItems"); + + migrationBuilder.DropColumn( + name: "ProductId", + table: "ShipmentItems"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "ShipmentItems"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "Latitude", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "Longitude", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "ShipmentCheckpoints"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Shifts"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Shifts"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Shifts"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "RouteSteps"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "RouteSteps"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "RouteSteps"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "RouteBlueprints"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "RouteBlueprints"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "RouteBlueprints"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Roles"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Roles"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Roles"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "RefreshTokens"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "RefreshTokens"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "RefreshTokens"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "NetworkLinks"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "NetworkLinks"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "NetworkLinks"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Locations"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Locations"); + + migrationBuilder.DropColumn( + name: "Latitude", + table: "Locations"); + + migrationBuilder.DropColumn( + name: "Longitude", + table: "Locations"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Locations"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "FleetLogs"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "FleetLogs"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Employees"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Employees"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Employees"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Drivers"); + + migrationBuilder.DropColumn( + name: "CreatedByUserId", + table: "Clients"); + + migrationBuilder.DropColumn( + name: "LastModifiedByUserId", + table: "Clients"); + + migrationBuilder.DropColumn( + name: "xmin", + table: "Clients"); + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs index 3c54208..7e86400 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs @@ -22,6 +22,94 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaseUom") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DefaultHeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultLengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultWeightKg") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("DefaultWidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Sku") + .IsUnique(); + + b.ToTable("CatalogItems"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => { b.Property("Id") @@ -45,6 +133,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -59,6 +150,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("LegalName") .HasMaxLength(300) .HasColumnType("character varying(300)"); @@ -81,6 +175,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(20) .HasColumnType("character varying(20)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("ShippingAddress") .IsRequired() .HasMaxLength(500) @@ -120,6 +220,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("CurrentTruckId") .HasColumnType("uuid"); @@ -135,6 +238,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("LicenseExpiration") .HasColumnType("timestamp with time zone"); @@ -147,6 +253,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(10) .HasColumnType("character varying(10)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("Status") .HasColumnType("integer"); @@ -181,6 +293,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("Curp") .HasMaxLength(18) .HasColumnType("character varying(18)"); @@ -206,6 +321,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("Nss") .HasMaxLength(11) .HasColumnType("character varying(11)"); @@ -219,6 +337,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(13) .HasColumnType("character varying(13)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("ShiftId") .HasColumnType("uuid"); @@ -264,6 +388,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("NewTruckId") .HasColumnType("uuid"); @@ -273,6 +400,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Reason") .HasColumnType("integer"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("TenantId") .HasColumnType("uuid"); @@ -297,6 +430,168 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("FleetLogs"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastCountDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QuantityReserved") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZoneId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("TenantId", "ExpiryDate") + .HasFilter("\"ExpiryDate\" IS NOT NULL"); + + b.HasIndex("TenantId", "ProductId"); + + b.HasIndex("ZoneId", "ProductId", "BatchNumber") + .IsUnique(); + + b.ToTable("InventoryStocks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationZoneId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginZoneId") + .HasColumnType("uuid"); + + b.Property("PerformedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Remarks") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationZoneId"); + + b.HasIndex("OriginZoneId"); + + b.HasIndex("PerformedByUserId"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId") + .HasFilter("\"ShipmentId\" IS NOT NULL"); + + b.HasIndex("TenantId", "Timestamp"); + + b.HasIndex("TenantId", "ProductId", "Timestamp"); + + b.ToTable("InventoryTransactions"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => { b.Property("Id") @@ -317,6 +612,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -334,11 +632,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsInternal") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + b.Property("Name") .IsRequired() .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("TenantId") .HasColumnType("uuid"); @@ -367,6 +682,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -382,12 +700,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("LinkType") .HasColumnType("integer"); b.Property("OriginLocationId") .HasColumnType("uuid"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("TenantId") .HasColumnType("uuid"); @@ -417,6 +744,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("CreatedFromIp") .HasMaxLength(45) .HasColumnType("character varying(45)"); @@ -435,6 +765,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasDefaultValue(false); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("RevokedAt") .HasColumnType("timestamp with time zone"); @@ -442,6 +775,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("TokenHash") .IsRequired() .HasMaxLength(256) @@ -477,6 +816,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -486,11 +828,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -511,6 +862,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -524,11 +878,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("TenantId") .HasColumnType("uuid"); @@ -557,18 +920,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("LocationId") .HasColumnType("uuid"); b.Property("RouteBlueprintId") .HasColumnType("uuid"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("StandardTransitTime") .HasColumnType("interval"); @@ -599,6 +974,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DaysOfWeek") .IsRequired() .HasMaxLength(50) @@ -616,11 +994,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("StartTime") .HasColumnType("time without time zone"); @@ -652,6 +1039,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("CurrentStepOrder") .HasColumnType("integer"); @@ -684,6 +1074,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("OriginLocationId") .HasColumnType("uuid"); @@ -717,6 +1110,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(500) .HasColumnType("character varying(500)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("SatMerchandiseCode") .HasMaxLength(20) .HasColumnType("character varying(20)"); @@ -812,12 +1211,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + b.Property("LoadedOntoTruckId") .HasColumnType("uuid"); b.Property("LocationId") .HasColumnType("uuid"); + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + b.Property("NewCustodian") .HasMaxLength(200) .HasColumnType("character varying(200)"); @@ -830,6 +1240,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(1000) .HasColumnType("character varying(1000)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("ShipmentId") .HasColumnType("uuid"); @@ -870,6 +1286,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -895,6 +1314,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("ShipmentId") .HasColumnType("uuid"); @@ -917,6 +1345,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeclaredValue") .HasPrecision(18, 2) .HasColumnType("numeric(18,2)"); @@ -942,6 +1373,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsHazardous") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("LengthCm") .HasPrecision(10, 2) .HasColumnType("numeric(10,2)"); @@ -949,12 +1383,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PackagingType") .HasColumnType("integer"); + b.Property("ProductId") + .HasColumnType("uuid"); + b.Property("Quantity") .HasColumnType("integer"); b.Property("RequiresRefrigeration") .HasColumnType("boolean"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("ShipmentId") .HasColumnType("uuid"); @@ -979,6 +1422,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("ProductId"); + b.HasIndex("ShipmentId"); b.ToTable("ShipmentItems"); @@ -1003,6 +1448,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -1018,6 +1466,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -1040,6 +1497,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("CurrentOdometerKm") .HasColumnType("numeric"); @@ -1064,6 +1524,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastMaintenanceDate") .HasColumnType("timestamp with time zone"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("MaxCapacityKg") .HasPrecision(10, 2) .HasColumnType("numeric(10,2)"); @@ -1085,6 +1548,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(20) .HasColumnType("character varying(20)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("TenantId") .HasColumnType("uuid"); @@ -1125,6 +1594,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -1153,6 +1625,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastLogin") .HasColumnType("timestamp with time zone"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("PasswordHash") .IsRequired() .HasMaxLength(500) @@ -1161,6 +1636,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RoleId") .HasColumnType("uuid"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("TenantId") .HasColumnType("uuid"); @@ -1194,6 +1675,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -1203,9 +1687,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("PrimaryZoneId") .HasColumnType("uuid"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -1235,6 +1728,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); @@ -1244,6 +1740,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + b.Property("LocationId") .HasColumnType("uuid"); @@ -1252,6 +1751,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.Property("Type") .HasColumnType("integer"); @@ -1266,6 +1771,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("WarehouseZones"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => { b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") @@ -1374,6 +1890,81 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Tenant"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("InventoryStocks") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "Zone") + .WithMany("InventoryStocks") + .HasForeignKey("ZoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Tenant"); + + b.Navigation("Zone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "DestinationZone") + .WithMany("DestinationTransactions") + .HasForeignKey("DestinationZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "OriginZone") + .WithMany("OriginTransactions") + .HasForeignKey("OriginZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.User", "PerformedBy") + .WithMany() + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany() + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationZone"); + + b.Navigation("OriginZone"); + + b.Navigation("PerformedBy"); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + + b.Navigation("Tenant"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => { b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") @@ -1585,12 +2176,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("ShipmentItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") .WithMany("Items") .HasForeignKey("ShipmentId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Product"); + b.Navigation("Shipment"); }); @@ -1661,6 +2259,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Location"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Navigation("InventoryStocks"); + + b.Navigation("ShipmentItems"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => { b.Navigation("ShipmentsAsRecipient"); @@ -1782,6 +2387,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => { b.Navigation("AssignedOperators"); + + b.Navigation("DestinationTransactions"); + + b.Navigation("InventoryStocks"); + + b.Navigation("OriginTransactions"); }); #pragma warning restore 612, 618 } diff --git a/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs b/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs index a8fe347..3d92fcb 100644 --- a/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs +++ b/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs @@ -64,6 +64,12 @@ public ParhelionDbContext(DbContextOptions options, Guid? te public DbSet ShipmentItems => Set(); public DbSet ShipmentCheckpoints => Set(); public DbSet ShipmentDocuments => Set(); + + // Inventario y Catálogo (v0.4.4) + public DbSet CatalogItems => Set(); + public DbSet InventoryStocks => Set(); + public DbSet InventoryTransactions => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/backend/src/Parhelion.Infrastructure/Services/CurrentUserService.cs b/backend/src/Parhelion.Infrastructure/Services/CurrentUserService.cs new file mode 100644 index 0000000..0d4984e --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/CurrentUserService.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Parhelion.Application.Services; +using System.Security.Claims; + +namespace Parhelion.Infrastructure.Services; + +/// +/// Implementación de ICurrentUserService que lee claims del HttpContext. +/// +public class CurrentUserService : ICurrentUserService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public CurrentUserService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public Guid? UserId + { + get + { + var userId = _httpContextAccessor.HttpContext?.User + .FindFirstValue(ClaimTypes.NameIdentifier); + return Guid.TryParse(userId, out var id) ? id : null; + } + } + + public Guid? TenantId + { + get + { + var tenantId = _httpContextAccessor.HttpContext?.User + .FindFirstValue("tenant_id"); + return Guid.TryParse(tenantId, out var id) ? id : null; + } + } + + public bool IsAuthenticated => + _httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false; +} diff --git a/database-schema.md b/database-schema.md index e474261..4dbad79 100644 --- a/database-schema.md +++ b/database-schema.md @@ -1,13 +1,13 @@ # PARHELION-LOGISTICS | Modelo de Base de Datos -**Versión:** 2.4 (v0.4.3 - Employee Layer) +**Version:** 2.5 (v0.4.4 - WMS Enhancement) **Fecha:** Diciembre 2025 **Motor:** PostgreSQL + Entity Framework Core (Code First) -**Estado:** Diseño Cerrado - Listo para Implementación +**Estado:** Diseno Cerrado - Listo para Implementacion -> **Nota Técnica:** Esta plataforma unifica WMS (Warehouse Management System) y TMS (Transportation Management System). El módulo de almacén gestiona inventario estático y carga, mientras que el núcleo TMS maneja logística de media milla: gestión de flotas tipificadas, redes Hub & Spoke y trazabilidad de envíos en movimiento. +> **Nota Tecnica:** Esta plataforma unifica WMS (Warehouse Management System) y TMS (Transportation Management System). El modulo de almacen gestiona inventario estatico y carga, mientras que el nucleo TMS maneja logistica de media milla: gestion de flotas tipificadas, redes Hub & Spoke y trazabilidad de envios en movimiento. -> **v0.4.3:** Agrega Employee Layer (centraliza datos legales), Shift (turnos), WarehouseZone (zonas de bodega), WarehouseOperator (extensión de empleado para almacenistas), y SuperAdmin (IsSuperAdmin en User). +> **v0.4.4:** Agrega CatalogItem (catalogo maestro de productos), InventoryStock (inventario cuantificado por zona y lote), InventoryTransaction (Kardex de movimientos), campos de auditoria (CreatedByUserId, LastModifiedByUserId), geolocalizacion (Latitude/Longitude), y optimistic locking (RowVersion). --- @@ -72,6 +72,14 @@ erDiagram WAREHOUSE_ZONE ||--o{ WAREHOUSE_OPERATOR : "asigna a zona" WAREHOUSE_OPERATOR ||--o{ SHIPMENT_CHECKPOINT : "maneja paquetes" + %% ========== INVENTARIO Y CATALOGO (v0.4.4) ========== + TENANT ||--o{ CATALOG_ITEM : "tiene productos" + CATALOG_ITEM ||--o{ SHIPMENT_ITEM : "referencia" + CATALOG_ITEM ||--o{ INVENTORY_STOCK : "existe en" + WAREHOUSE_ZONE ||--o{ INVENTORY_STOCK : "contiene" + WAREHOUSE_ZONE ||--o{ INVENTORY_TRANSACTION : "origen/destino" + USER ||--o{ INVENTORY_TRANSACTION : "ejecuta" + %% ========== ENTIDADES CORE ========== TENANT { uuid id PK @@ -187,6 +195,68 @@ erDiagram datetime deleted_at "nullable" } + %% ========== INVENTARIO Y CATALOGO (v0.4.4) ========== + CATALOG_ITEM { + uuid id PK + uuid tenant_id FK + string sku UK "SKU unico por tenant" + string name + string description "nullable" + string base_uom "Pza|Kg|Lt|Caja" + decimal default_weight_kg + decimal default_width_cm + decimal default_height_cm + decimal default_length_cm + boolean requires_refrigeration + boolean is_hazardous + boolean is_fragile + boolean is_active + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + uuid created_by_user_id FK "nullable v0.4.4" + uuid last_modified_by_user_id FK "nullable v0.4.4" + } + + INVENTORY_STOCK { + uuid id PK + uuid tenant_id FK + uuid zone_id FK "WarehouseZone" + uuid product_id FK "CatalogItem" + decimal quantity + decimal quantity_reserved + string batch_number "nullable" + datetime expiry_date "nullable" + datetime last_count_date "nullable" + decimal unit_cost "nullable" + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + uuid created_by_user_id FK "nullable v0.4.4" + uuid last_modified_by_user_id FK "nullable v0.4.4" + } + + INVENTORY_TRANSACTION { + uuid id PK + uuid tenant_id FK + uuid product_id FK "CatalogItem" + uuid origin_zone_id FK "nullable" + uuid destination_zone_id FK "nullable" + decimal quantity + string transaction_type "Receipt|PutAway|InternalMove|Picking|Packing|Dispatch|Adjustment|Scrap|Return" + uuid performed_by_user_id FK + uuid shipment_id FK "nullable" + string batch_number "nullable" + string remarks "nullable" + datetime timestamp + datetime created_at + datetime updated_at "nullable" + boolean is_deleted + datetime deleted_at "nullable" + } + TRUCK { uuid id PK uuid tenant_id FK @@ -217,12 +287,14 @@ erDiagram LOCATION { uuid id PK uuid tenant_id FK - string code UK "Ej: MTY, GDL, MM - Código corto único" + string code UK "Ej: MTY, GDL, MM - Codigo corto unico" string name "Ej: CEDIS Norte" string type "RegionalHub|CrossDock|Warehouse|Store|SupplierPlant" string full_address - boolean can_receive "Puede recibir mercancía" - boolean can_dispatch "Puede despachar mercancía" + decimal latitude "nullable v0.4.4 - Geolocalizacion" + decimal longitude "nullable v0.4.4 - Geolocalizacion" + boolean can_receive "Puede recibir mercancia" + boolean can_dispatch "Puede despachar mercancia" boolean is_internal "Propio o externo" boolean is_active datetime created_at From 9c889a93833a88539dd3154e3484c5fa65226e1f Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sun, 14 Dec 2025 15:59:38 +0000 Subject: [PATCH 17/34] docs: Actualizar Seccion 9 de database-schema.md con mapeos C# v0.4.4 Update Section 9 of database-schema.md with C# mappings v0.4.4 Agregado / Added: - BaseEntity: Clase base con RowVersion, CreatedByUserId, LastModifiedByUserId - CatalogItem: Catalogo maestro de productos - InventoryStock: Inventario cuantificado por zona - InventoryTransaction: Kardex de movimientos - InventoryTransactionType: Enum con 9 tipos de transaccion Actualizado / Updated: - Location: +Latitude, +Longitude, +Zones collection - ShipmentItem: +ProductId, +Product navigation - Tenant: +CatalogItems collection --- database-schema.md | 156 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 8 deletions(-) diff --git a/database-schema.md b/database-schema.md index 4dbad79..179fe8f 100644 --- a/database-schema.md +++ b/database-schema.md @@ -902,22 +902,51 @@ flowchart LR ## 9. Mapeo a C# (Entity Framework) -### Entidad Location +### Clase Base (BaseEntity) ```csharp -public class Location +/// +/// Clase base para todas las entidades del sistema. +/// Incluye Soft Delete, Audit Trail, y Concurrencia Optimista. +/// +public abstract class BaseEntity { public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletedAt { get; set; } + + // v0.4.4 - Auditoria y Concurrencia + public Guid? CreatedByUserId { get; set; } + public Guid? LastModifiedByUserId { get; set; } + public uint RowVersion { get; set; } // Mapeado a xmin en PostgreSQL +} + +public abstract class TenantEntity : BaseEntity +{ public Guid TenantId { get; set; } - public string Code { get; set; } = null!; // Código corto único (MTY, GDL, MM) +} +``` + +### Entidad Location + +```csharp +public class Location : TenantEntity +{ + public string Code { get; set; } = null!; // Codigo corto unico (MTY, GDL, MM) public string Name { get; set; } = null!; public LocationType Type { get; set; } public string FullAddress { get; set; } = null!; + + // v0.4.4 - Geolocalizacion + public decimal? Latitude { get; set; } + public decimal? Longitude { get; set; } + public bool CanReceive { get; set; } public bool CanDispatch { get; set; } public bool IsInternal { get; set; } public bool IsActive { get; set; } - public DateTime CreatedAt { get; set; } // Navigation Properties public Tenant Tenant { get; set; } = null!; @@ -927,6 +956,7 @@ public class Location public ICollection RouteSteps { get; set; } = new List(); public ICollection OutgoingLinks { get; set; } = new List(); public ICollection IncomingLinks { get; set; } = new List(); + public ICollection Zones { get; set; } = new List(); } public enum LocationType { RegionalHub, CrossDock, Warehouse, Store, SupplierPlant } @@ -1018,10 +1048,13 @@ public enum ShipmentPriority { Normal, Urgent, Express } ### Entidad ShipmentItem ```csharp -public class ShipmentItem +public class ShipmentItem : BaseEntity { - public Guid Id { get; set; } public Guid ShipmentId { get; set; } + + // v0.4.4 - Enlace opcional a catalogo + public Guid? ProductId { get; set; } + public string? Sku { get; set; } public string Description { get; set; } = null!; public PackagingType PackagingType { get; set; } @@ -1030,16 +1063,16 @@ public class ShipmentItem public decimal WidthCm { get; set; } public decimal HeightCm { get; set; } public decimal LengthCm { get; set; } - public decimal VolumeM3 => (WidthCm * HeightCm * LengthCm) / 1_000_000; + public decimal VolumeM3 => (WidthCm * HeightCm * LengthCm) / 1_000_000m; public decimal DeclaredValue { get; set; } public bool IsFragile { get; set; } public bool IsHazardous { get; set; } public bool RequiresRefrigeration { get; set; } public string? StackingInstructions { get; set; } - public DateTime CreatedAt { get; set; } // Navigation Properties public Shipment Shipment { get; set; } = null!; + public CatalogItem? Product { get; set; } // v0.4.4 } public enum PackagingType { Pallet, Box, Drum, Piece } @@ -1205,6 +1238,113 @@ public class Tenant public ICollection Drivers { get; set; } = new List(); public ICollection Locations { get; set; } = new List(); public ICollection Shipments { get; set; } = new List(); + public ICollection CatalogItems { get; set; } = new List(); // v0.4.4 +} +``` + +### Entidad CatalogItem (v0.4.4 - Nueva) + +```csharp +/// +/// Catalogo maestro de productos/SKUs. +/// Normaliza datos que se repiten en ShipmentItems. +/// +public class CatalogItem : TenantEntity +{ + public string Sku { get; set; } = null!; + public string Name { get; set; } = null!; + public string? Description { get; set; } + public string BaseUom { get; set; } = "Pza"; // Unidad de medida: Pza, Kg, Lt, Caja + + // Dimensiones Default + public decimal DefaultWeightKg { get; set; } + public decimal DefaultWidthCm { get; set; } + public decimal DefaultHeightCm { get; set; } + public decimal DefaultLengthCm { get; set; } + + // Flags de manejo especial + public bool RequiresRefrigeration { get; set; } + public bool IsHazardous { get; set; } + public bool IsFragile { get; set; } + public bool IsActive { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public ICollection ShipmentItems { get; set; } = new List(); + public ICollection InventoryStocks { get; set; } = new List(); +} +``` + +### Entidad InventoryStock (v0.4.4 - Nueva) + +```csharp +/// +/// Inventario fisico cuantificado por zona y lote. +/// Representa el saldo actual de un producto en una ubicacion. +/// +public class InventoryStock : TenantEntity +{ + public Guid ZoneId { get; set; } + public Guid ProductId { get; set; } + + public decimal Quantity { get; set; } + public decimal QuantityReserved { get; set; } + public decimal QuantityAvailable => Quantity - QuantityReserved; + + public string? BatchNumber { get; set; } + public DateTime? ExpiryDate { get; set; } + public DateTime? LastCountDate { get; set; } + public decimal? UnitCost { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public WarehouseZone Zone { get; set; } = null!; + public CatalogItem Product { get; set; } = null!; +} +``` + +### Entidad InventoryTransaction (v0.4.4 - Nueva) + +```csharp +/// +/// Bitacora de movimientos de inventario (Kardex). +/// INMUTABLE: Las transacciones no se modifican, solo se agregan. +/// +public class InventoryTransaction : TenantEntity +{ + public Guid ProductId { get; set; } + public Guid? OriginZoneId { get; set; } + public Guid? DestinationZoneId { get; set; } + + public decimal Quantity { get; set; } + public InventoryTransactionType TransactionType { get; set; } + + public Guid PerformedByUserId { get; set; } + public Guid? ShipmentId { get; set; } + public string? BatchNumber { get; set; } + public string? Remarks { get; set; } + public DateTime Timestamp { get; set; } + + // Navigation Properties + public Tenant Tenant { get; set; } = null!; + public CatalogItem Product { get; set; } = null!; + public WarehouseZone? OriginZone { get; set; } + public WarehouseZone? DestinationZone { get; set; } + public User PerformedBy { get; set; } = null!; + public Shipment? Shipment { get; set; } +} + +public enum InventoryTransactionType +{ + Receipt, // Entrada de mercancia externa + PutAway, // Almacenamiento + InternalMove, // Movimiento entre zonas + Picking, // Surtido para envio + Packing, // Empaque + Dispatch, // Salida del almacen + Adjustment, // Ajuste (+/-) + Scrap, // Baja por dano/caducidad + Return // Devolucion } ``` From af3099b0a89638c23d09089b0a756e5b428d7b7d Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sun, 14 Dec 2025 16:04:43 +0000 Subject: [PATCH 18/34] fix(docs): Corregir errores de renderizado Mermaid en database-schema.md Fix Mermaid rendering errors in database-schema.md Problema: El diagrama ER era demasiado grande (450+ lineas) para renderizar en GitHub. El error 'Could not find a suitable point for the given distance' ocurria por exceso de nodos. Solucion: - Simplificar diagrama ER a solo relaciones (70 lineas) - Remover bloques de atributos de entidad del diagrama - Agregar tabla resumen de 23 entidades por modulo - Los atributos detallados permanecen en Secciones 2-9 Reduccion: ~370 lineas removidas del diagrama principal --- database-schema.md | 401 ++------------------------------------------- 1 file changed, 16 insertions(+), 385 deletions(-) diff --git a/database-schema.md b/database-schema.md index 179fe8f..4b33e8f 100644 --- a/database-schema.md +++ b/database-schema.md @@ -77,399 +77,30 @@ erDiagram CATALOG_ITEM ||--o{ SHIPMENT_ITEM : "referencia" CATALOG_ITEM ||--o{ INVENTORY_STOCK : "existe en" WAREHOUSE_ZONE ||--o{ INVENTORY_STOCK : "contiene" - WAREHOUSE_ZONE ||--o{ INVENTORY_TRANSACTION : "origen/destino" + WAREHOUSE_ZONE ||--o{ INVENTORY_TRANSACTION : "origen-destino" USER ||--o{ INVENTORY_TRANSACTION : "ejecuta" +``` - %% ========== ENTIDADES CORE ========== - TENANT { - uuid id PK - string company_name - string contact_email - int fleet_size - int driver_count - boolean is_active - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - USER { - uuid id PK - uuid tenant_id FK - string email UK - string password_hash - string full_name - uuid role_id FK - boolean is_demo_user - boolean uses_argon2 "True si usa Argon2id" - boolean is_super_admin "v0.4.3 - SuperAdmin del sistema" - datetime last_login "nullable" - boolean is_active - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - ROLE { - uuid id PK - string name UK - string description - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - DRIVER { - uuid id PK - uuid employee_id FK "v0.4.3 - Referencia a Employee" - string license_number - string license_type "nullable - A|B|C|D|E" - datetime license_expiration "nullable" - uuid default_truck_id FK "nullable - asignación fija" - uuid current_truck_id FK "nullable - camión actual" - string status "Available|OnRoute|Inactive" - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - %% ========== EMPLOYEE LAYER ENTITIES (v0.4.3) ========== - EMPLOYEE { - uuid id PK - uuid tenant_id FK - uuid user_id FK "UK - 1:1 con User" - string phone - string rfc "nullable - RFC fiscal" - string nss "nullable - Número de Seguro Social" - string curp "nullable - CURP" - string emergency_contact "nullable" - string emergency_phone "nullable" - datetime hire_date "nullable" - uuid shift_id FK "nullable" - string department "nullable - Admin|Operations|Field" - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - SHIFT { - uuid id PK - uuid tenant_id FK - string name "Matutino|Vespertino|Nocturno" - time start_time - time end_time - string days_of_week "Mon,Tue,Wed,Thu,Fri" - boolean is_active - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - WAREHOUSE_ZONE { - uuid id PK - uuid location_id FK - string code "UK - A1, B2, COLD-1" - string name - string type "Receiving|Storage|Staging|Shipping|ColdChain|Hazmat" - boolean is_active - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - WAREHOUSE_OPERATOR { - uuid id PK - uuid employee_id FK "UK - 1:1 con Employee" - uuid assigned_location_id FK - uuid primary_zone_id FK "nullable" - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - %% ========== INVENTARIO Y CATALOGO (v0.4.4) ========== - CATALOG_ITEM { - uuid id PK - uuid tenant_id FK - string sku UK "SKU unico por tenant" - string name - string description "nullable" - string base_uom "Pza|Kg|Lt|Caja" - decimal default_weight_kg - decimal default_width_cm - decimal default_height_cm - decimal default_length_cm - boolean requires_refrigeration - boolean is_hazardous - boolean is_fragile - boolean is_active - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - uuid created_by_user_id FK "nullable v0.4.4" - uuid last_modified_by_user_id FK "nullable v0.4.4" - } - - INVENTORY_STOCK { - uuid id PK - uuid tenant_id FK - uuid zone_id FK "WarehouseZone" - uuid product_id FK "CatalogItem" - decimal quantity - decimal quantity_reserved - string batch_number "nullable" - datetime expiry_date "nullable" - datetime last_count_date "nullable" - decimal unit_cost "nullable" - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - uuid created_by_user_id FK "nullable v0.4.4" - uuid last_modified_by_user_id FK "nullable v0.4.4" - } - - INVENTORY_TRANSACTION { - uuid id PK - uuid tenant_id FK - uuid product_id FK "CatalogItem" - uuid origin_zone_id FK "nullable" - uuid destination_zone_id FK "nullable" - decimal quantity - string transaction_type "Receipt|PutAway|InternalMove|Picking|Packing|Dispatch|Adjustment|Scrap|Return" - uuid performed_by_user_id FK - uuid shipment_id FK "nullable" - string batch_number "nullable" - string remarks "nullable" - datetime timestamp - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - TRUCK { - uuid id PK - uuid tenant_id FK - string plate UK - string model - string type "DryBox|Refrigerated|HazmatTank|Flatbed|Armored" - decimal max_capacity_kg - decimal max_volume_m3 - boolean is_active - string vin "nullable - VIN" - string engine_number "nullable" - int year "nullable" - string color "nullable" - string insurance_policy "nullable" - datetime insurance_expiration "nullable" - string verification_number "nullable" - datetime verification_expiration "nullable" - datetime last_maintenance_date "nullable" - datetime next_maintenance_date "nullable" - decimal current_odometer_km "nullable" - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - %% ========== ENTIDADES ENTERPRISE ========== - LOCATION { - uuid id PK - uuid tenant_id FK - string code UK "Ej: MTY, GDL, MM - Codigo corto unico" - string name "Ej: CEDIS Norte" - string type "RegionalHub|CrossDock|Warehouse|Store|SupplierPlant" - string full_address - decimal latitude "nullable v0.4.4 - Geolocalizacion" - decimal longitude "nullable v0.4.4 - Geolocalizacion" - boolean can_receive "Puede recibir mercancia" - boolean can_dispatch "Puede despachar mercancia" - boolean is_internal "Propio o externo" - boolean is_active - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - SHIPMENT { - uuid id PK - uuid tenant_id FK - string tracking_number UK "PAR-XXXXXX" - string qr_code_data "String único para generar QR" - uuid origin_location_id FK - uuid destination_location_id FK - uuid assigned_route_id FK "nullable - Ruta predefinida asignada" - int current_step_order "nullable - Paso actual en la ruta" - string recipient_name - string recipient_phone - decimal total_weight_kg - decimal total_volume_m3 - decimal declared_value - string sat_merchandise_code "nullable - Código SAT para Carta Porte" - string delivery_instructions "nullable - Instrucciones para Hoja de Ruta" - string recipient_signature_url "nullable - URL firma digital POD" - string priority "Normal|Urgent|Express" - string status "PendingApproval|Approved|Loaded|InTransit|AtHub|OutForDelivery|Delivered|Exception" - uuid truck_id FK "nullable" - uuid driver_id FK "nullable" - boolean was_qr_scanned "True si se usó cámara" - boolean is_delayed "True si hay retraso (avería, tráfico)" - datetime scheduled_departure "nullable - Fecha/hora salida planeada" - datetime pickup_window_start "nullable - Ventana de recolección" - datetime pickup_window_end "nullable" - datetime estimated_arrival "Calculada: Salida + Suma tiempos ruta" - datetime assigned_at "nullable" - datetime delivered_at "nullable" - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - SHIPMENT_ITEM { - uuid id PK - uuid shipment_id FK - string sku "nullable" - string description - string packaging_type "Pallet|Box|Drum|Piece" - int quantity - decimal weight_kg - decimal width_cm - decimal height_cm - decimal length_cm - decimal volume_m3 "Calculado" - decimal declared_value "Valor monetario para seguro" - boolean is_fragile - boolean is_hazardous - boolean requires_refrigeration - string stacking_instructions "nullable - Ej: No apilar" - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } - - SHIPMENT_CHECKPOINT { - uuid id PK - uuid shipment_id FK - uuid location_id FK "nullable" - string status_code "Loaded|ArrivedHub|DepartedHub|OutForDelivery|DeliveryAttempt|Delivered|Exception" - string remarks "Comentarios del operador" - datetime timestamp - uuid created_by_user_id FK - } - - FLEET_LOG { - uuid id PK - uuid tenant_id FK - uuid driver_id FK - uuid old_truck_id FK "nullable - si venía sin camión" - uuid new_truck_id FK - string reason "ShiftChange|Breakdown|Reassignment" - datetime timestamp - uuid created_by_user_id FK - } - - SHIPMENT_DOCUMENT { - uuid id PK - uuid shipment_id FK - string document_type "ServiceOrder|Waybill|Manifest|TripSheet|POD" - string file_url "URL al PDF o imagen" - string generated_by "System|User" - datetime generated_at - datetime expires_at "nullable - para documentos temporales" - } - - %% ========== CLIENTES (v0.4.2) ========== - CLIENT { - uuid id PK - uuid tenant_id FK - string company_name "Nombre de la empresa" - string trade_name "nullable - Nombre comercial" - string contact_name "Contacto principal" - string email - string phone - string tax_id "nullable - RFC" - string legal_name "nullable - Razón Social" - string billing_address "nullable" - string shipping_address "Dirección de envío" - string preferred_product_types "nullable - Tipos de productos" - string priority "Default heredado a sus envíos - Normal|Low|High|Urgent" - boolean is_active - string notes "nullable - Notas internas" - } - - REFRESH_TOKEN { - uuid id PK - uuid user_id FK - string token_hash "Hash del token (nunca texto plano)" - datetime expires_at - boolean is_revoked - datetime revoked_at "nullable" - string revoked_reason "nullable" - string created_from_ip "nullable" - string user_agent "nullable" - } +> **Nota:** Los atributos detallados de cada entidad se documentan en las Secciones 2-9. - %% ========== ENRUTAMIENTO (HUB & SPOKE) ========== - ROUTE_BLUEPRINT { - uuid id PK - uuid tenant_id FK - string name "Ej: Ruta Mty-Saltillo-Torreón" - string description "nullable" - int total_steps "Número de paradas" - time total_transit_time "Suma de tiempos de tránsito" - boolean is_active - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } +--- - ROUTE_STEP { - uuid id PK - uuid route_blueprint_id FK - uuid location_id FK "La sede (Hub, Almacén, etc.)" - int step_order "1, 2, 3... Orden de la parada" - time standard_transit_time "Tiempo desde parada anterior" - string step_type "Origin|Intermediate|Destination" - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } +## 1.1 Entidades del Sistema (23 tablas) - NETWORK_LINK { - uuid id PK - uuid tenant_id FK - uuid origin_location_id FK - uuid destination_location_id FK - string link_type "FirstMile|LineHaul|LastMile" - time transit_time "Tiempo estándar del tramo" - boolean is_bidirectional "Si aplica en ambas direcciones" - boolean is_active - datetime created_at - datetime updated_at "nullable" - boolean is_deleted - datetime deleted_at "nullable" - } -``` +| Modulo | Entidades | +| ------------- | ------------------------------------------------------------ | +| **Core** | Tenant, User, Role, RefreshToken | +| **Employee** | Employee, Shift | +| **Fleet** | Driver, Truck, FleetLog | +| **Warehouse** | Location, WarehouseZone, WarehouseOperator | +| **Inventory** | CatalogItem, InventoryStock, InventoryTransaction | +| **Shipment** | Shipment, ShipmentItem, ShipmentCheckpoint, ShipmentDocument | +| **Routing** | RouteBlueprint, RouteStep, NetworkLink | +| **CRM** | Client | --- -## 2. Módulos del Sistema +## 2. Modulos del Sistema ### 2.1 Módulo Core (Multi-Tenant) From d3b79b685eded17c86f654516ab75e3575496ec4 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Mon, 15 Dec 2025 02:13:25 +0000 Subject: [PATCH 19/34] v0.5.0: API Skeleton - Endpoints base para todas las entidades feat(api): Implementacion de 22 endpoints base organizados en 5 capas - Core Layer: Tenants, Users, Roles, Employees, Clients - Warehouse Layer: Locations, WarehouseZones, WarehouseOperators, InventoryStocks, InventoryTransactions - Fleet Layer: Trucks, Drivers, Shifts, FleetLogs - Shipment Layer: Shipments, ShipmentItems, ShipmentCheckpoints, ShipmentDocuments, CatalogItems - Network Layer: NetworkLinks, RouteBlueprints, RouteSteps feat(schema): Endpoint /api/Schema/metadata para herramientas de desarrollo docs: Nuevo archivo api-architecture.md con estructura de capas docs: Actualizacion de CHANGELOG.md y README.md con progreso v0.5.0 ci: Actualizacion para verificar 24 tablas en base de datos chore: Exclusion de herramientas de desarrollo local del repositorio --- v0.5.0: API Skeleton - Base endpoints for all entities feat(api): Implementation of 22 base endpoints organized in 5 layers feat(schema): Schema metadata endpoint for development tooling docs: New api-architecture.md with layer structure documentation docs: Updated CHANGELOG.md and README.md with v0.5.0 progress ci: Updated to verify 24 database tables chore: Excluded local development tools from repository --- .github/workflows/ci.yml | 6 +- .gitignore | 4 +- CHANGELOG.md | 33 +- README.md | 43 +-- api-architecture.md | 134 +++++++++ .../Controllers/CatalogItemsController.cs | 201 +++++++++++++ .../Controllers/ClientsController.cs | 172 +++++++++++ .../Controllers/DriversController.cs | 147 +++++++++ .../Controllers/EmployeesController.cs | 165 ++++++++++ .../Controllers/FleetLogsController.cs | 140 +++++++++ .../Controllers/InventoryStocksController.cs | 141 +++++++++ .../InventoryTransactionsController.cs | 140 +++++++++ .../Controllers/LocationsController.cs | 130 ++++++++ .../Controllers/NetworkLinksController.cs | 129 ++++++++ .../Controllers/RolesController.cs | 121 ++++++++ .../Controllers/RouteBlueprintsController.cs | 112 +++++++ .../Controllers/RouteStepsController.cs | 141 +++++++++ .../Controllers/SchemaController.cs | 283 ++++++++++++++++++ .../Controllers/ShiftsController.cs | 101 +++++++ .../ShipmentCheckpointsController.cs | 109 +++++++ .../ShipmentDocumentsController.cs | 108 +++++++ .../Controllers/ShipmentItemsController.cs | 136 +++++++++ .../Controllers/ShipmentsController.cs | 182 +++++++++++ .../Controllers/TenantsController.cs | 158 ++++++++++ .../Controllers/TrucksController.cs | 153 ++++++++++ .../Controllers/UsersController.cs | 169 +++++++++++ .../WarehouseOperatorsController.cs | 122 ++++++++ .../Controllers/WarehouseZonesController.cs | 113 +++++++ backend/src/Parhelion.API/Data/DataSeeder.cs | 90 ++++++ backend/src/Parhelion.API/Program.cs | 4 +- .../DTOs/Catalog/CatalogItemDtos.cs | 58 ++++ .../DTOs/Core/CoreDtos.cs | 175 +++++++++++ .../DTOs/Fleet/FleetDtos.cs | 150 ++++++++++ .../DTOs/Network/NetworkDtos.cs | 88 ++++++ .../DTOs/Shipment/ShipmentDtos.cs | 190 ++++++++++++ .../DTOs/Warehouse/WarehouseDtos.cs | 168 +++++++++++ docs/api-reference.md | 141 +++++++++ 37 files changed, 4628 insertions(+), 29 deletions(-) create mode 100644 api-architecture.md create mode 100644 backend/src/Parhelion.API/Controllers/CatalogItemsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/ClientsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/DriversController.cs create mode 100644 backend/src/Parhelion.API/Controllers/EmployeesController.cs create mode 100644 backend/src/Parhelion.API/Controllers/FleetLogsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/InventoryStocksController.cs create mode 100644 backend/src/Parhelion.API/Controllers/InventoryTransactionsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/LocationsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/NetworkLinksController.cs create mode 100644 backend/src/Parhelion.API/Controllers/RolesController.cs create mode 100644 backend/src/Parhelion.API/Controllers/RouteBlueprintsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/RouteStepsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/SchemaController.cs create mode 100644 backend/src/Parhelion.API/Controllers/ShiftsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/ShipmentCheckpointsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/ShipmentDocumentsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/ShipmentItemsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/ShipmentsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/TenantsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/TrucksController.cs create mode 100644 backend/src/Parhelion.API/Controllers/UsersController.cs create mode 100644 backend/src/Parhelion.API/Controllers/WarehouseOperatorsController.cs create mode 100644 backend/src/Parhelion.API/Controllers/WarehouseZonesController.cs create mode 100644 backend/src/Parhelion.API/Data/DataSeeder.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Catalog/CatalogItemDtos.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Core/CoreDtos.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Fleet/FleetDtos.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Network/NetworkDtos.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Shipment/ShipmentDtos.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Warehouse/WarehouseDtos.cs create mode 100644 docs/api-reference.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5eb10b..3a13ad9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ # =================================== # PARHELION CI - Build & Test Pipeline # Se ejecuta en cada push/PR a develop y main -# v0.4.4: Agregado verificacion de tablas de inventario (CatalogItems, InventoryStocks, InventoryTransactions) +# v0.5.0: API Skeleton endpoints, 24 tablas en base de datos # =================================== name: CI Pipeline @@ -77,8 +77,8 @@ jobs: - name: Verify Database Schema run: | - echo "Verificando que todas las tablas existen en PostgreSQL (23 tablas esperadas)..." - PGPASSWORD=test_password_ci psql -h localhost -U parhelion_test -d parhelion_test_db -c "\dt" | grep -E "(Tenants|Users|Employees|Shifts|Drivers|Trucks|Locations|Shipments|CatalogItems|InventoryStocks|InventoryTransactions)" && echo "Schema validation passed (v0.4.4)" + echo "Verificando que todas las tablas existen en PostgreSQL (24 tablas esperadas)..." + PGPASSWORD=test_password_ci psql -h localhost -U parhelion_test -d parhelion_test_db -c "\dt" | grep -E "(Tenants|Users|Employees|Shifts|Drivers|Trucks|Locations|Shipments|CatalogItems|InventoryStocks|InventoryTransactions)" && echo "Schema validation passed (v0.5.0)" # ===== FRONTEND ADMIN (Angular) ===== frontend-admin: diff --git a/.gitignore b/.gitignore index a094688..84d04d8 100644 --- a/.gitignore +++ b/.gitignore @@ -67,5 +67,7 @@ TestResults/ *.temp *.bak -# ===== Local Development Tools ===== +# ===== Local Development Tools (NEVER in production) ===== +# Control panel is a private development tool - never committed to repository control-panel/ +.agent/ diff --git a/CHANGELOG.md b/CHANGELOG.md index dcae14f..8c38484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,37 @@ Historial de cambios del proyecto Parhelion Logistics. --- -## [Unreleased] - En desarrollo +## [0.5.0] - 2025-12-15 -### Pendiente +### Agregado + +- **Endpoints API Skeleton (22 endpoints)**: + + - Core Layer: Tenants, Users, Roles, Employees, Clients + - Warehouse Layer: Locations, WarehouseZones, WarehouseOperators, InventoryStocks, InventoryTransactions + - Fleet Layer: Trucks, Drivers, Shifts, FleetLogs + - Shipment Layer: Shipments, ShipmentItems, ShipmentCheckpoints, ShipmentDocuments, CatalogItems + - Network Layer: NetworkLinks, RouteBlueprints, RouteSteps + +- **Schema Metadata Endpoint**: + + - `GET /api/Schema/metadata` - Retorna estructura de BD para herramientas + - `POST /api/Schema/refresh` - Invalida cache de metadata + +- **Documentacion**: + - Nuevo archivo `api-architecture.md` con estructura de capas y endpoints + - Documentacion de Swagger UI en `/swagger` + +### Modificado + +- Version del sistema actualizada a 0.5.0 +- CI/CD actualizado para verificar 24 tablas en base de datos + +### Notas Tecnicas -- PWA Service Workers para modo offline -- Endpoints CRUD para todas las entidades +- Endpoints responden con HTTP 200 (lista vacia) para GET autenticados +- Logica CRUD pendiente para v0.5.x +- Herramientas de desarrollo local excluidas del repositorio --- diff --git a/README.md b/README.md index a3dd995..d26cde0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. -> **Estado:** Development Preview v0.4.3 - Employee Layer Implementado +> **Estado:** Development Preview v0.5.0 - API Skeleton Implementado --- @@ -28,43 +28,45 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in ### Core -- [x] Documentación de requerimientos y esquema de base de datos -- [x] **Arquitectura Base:** Configuración de Clean Architecture y estructura de proyecto +- [x] Documentacion de requerimientos y esquema de base de datos +- [x] **Arquitectura Base:** Configuracion de Clean Architecture y estructura de proyecto - [x] **Multi-tenancy:** Query Filters globales por TenantId - [x] **Domain Layer:** 14 entidades + 11 enumeraciones - [x] **Infrastructure Layer:** EF Core + PostgreSQL + Migrations +- [x] **API Skeleton:** 22 endpoints base para todas las entidades +- [x] **Autenticacion:** JWT con roles SuperAdmin/Admin/Driver/Warehouse -### Gestión de Flotilla +### Gestion de Flotilla - [ ] **Camiones Tipificados:** DryBox, Refrigerado, HAZMAT, Plataforma, Blindado -- [ ] **Choferes:** Asignación fija (default_truck) y dinámica (current_truck) -- [ ] **Bitácora de Flotilla:** Historial de cambios de vehículo (FleetLog) +- [ ] **Choferes:** Asignacion fija (default_truck) y dinamica (current_truck) +- [ ] **Bitacora de Flotilla:** Historial de cambios de vehiculo (FleetLog) -### Red Logística (Hub & Spoke) +### Red Logistica (Hub and Spoke) - [ ] **Nodos de Red:** RegionalHub, CrossDock, Warehouse, Store, SupplierPlant -- [ ] **Códigos Aeroportuarios:** Identificadores únicos por ubicación (MTY, GDL, MM) +- [ ] **Codigos Aeroportuarios:** Identificadores unicos por ubicacion (MTY, GDL, MM) - [ ] **Enlaces de Red:** Conexiones FirstMile, LineHaul, LastMile -- [ ] **Rutas Predefinidas:** RouteBlueprint con paradas y tiempos de tránsito +- [ ] **Rutas Predefinidas:** RouteBlueprint con paradas y tiempos de transito -### Envíos y Trazabilidad +### Envios y Trazabilidad -- [ ] **Manifiesto de Carga:** Items con peso volumétrico y valor declarado -- [ ] **Restricciones de Compatibilidad:** Cadena de frío, HAZMAT, Alto valor -- [ ] **Checkpoints:** Bitácora de eventos (Loaded, QrScanned, ArrivedHub, Delivered) +- [ ] **Manifiesto de Carga:** Items con peso volumetrico y valor declarado +- [ ] **Restricciones de Compatibilidad:** Cadena de frio, HAZMAT, Alto valor +- [ ] **Checkpoints:** Bitacora de eventos (Loaded, QrScanned, ArrivedHub, Delivered) - [ ] **QR Handshake:** Transferencia de custodia digital mediante escaneo -### Documentación B2B +### Documentacion B2B -- [ ] **Orden de Servicio:** Petición inicial del cliente +- [ ] **Orden de Servicio:** Peticion inicial del cliente - [ ] **Carta Porte (Waybill):** Documento legal SAT para transporte - [ ] **Manifiesto de Carga:** Checklist de estiba para almacenista - [ ] **Hoja de Ruta:** Itinerario con ventanas de entrega - [ ] **POD (Proof of Delivery):** Firma digital del receptor -### Operación +### Operacion -- [ ] **Seguridad:** Autenticación JWT con roles (Admin/Chofer/Almacenista) +- [x] **Seguridad:** Autenticacion JWT con roles (Admin/Chofer/Almacenista) - [ ] **Dashboard:** KPIs operativos en tiempo real - [ ] **Modo Demo:** Acceso para reclutadores sin registro previo @@ -196,12 +198,13 @@ src/ --- -## Documentación +## Documentacion -| Documento | Descripción | +| Documento | Descripcion | | :----------------------------------------------- | :-------------------------------------------- | -| [Requerimientos (MVP)](./requirments.md) | Especificación funcional completa del sistema | +| [Requerimientos (MVP)](./requirments.md) | Especificacion funcional completa del sistema | | [Esquema de Base de Datos](./database-schema.md) | Diagrama ER, entidades y reglas de negocio | +| [Arquitectura de API](./api-architecture.md) | Estructura de capas y endpoints | --- diff --git a/api-architecture.md b/api-architecture.md new file mode 100644 index 0000000..a975a4f --- /dev/null +++ b/api-architecture.md @@ -0,0 +1,134 @@ +# Arquitectura de API - Parhelion Logistics + +Documentacion tecnica de la estructura API-First del backend Parhelion. + +## Estado Actual + +**Version:** 0.5.0 +**Enfoque:** API-First (Skeleton Endpoints) +**Arquitectura:** Clean Architecture + Domain-Driven Design + +--- + +## Capas del API (API Layers) + +El backend esta organizado en 5 capas logicas que agrupan los endpoints segun su dominio: + +### Core Layer + +Gestion de identidad, usuarios y estructura organizacional. + +| Endpoint | Entidad | Estado | +| ---------------- | -------- | -------- | +| `/api/tenants` | Tenant | Skeleton | +| `/api/users` | User | Skeleton | +| `/api/roles` | Role | Skeleton | +| `/api/employees` | Employee | Skeleton | +| `/api/clients` | Client | Skeleton | + +### Warehouse Layer + +Gestion de almacenes, zonas e inventario. + +| Endpoint | Entidad | Estado | +| ----------------------------- | -------------------- | -------- | +| `/api/locations` | Location | Skeleton | +| `/api/warehouse-zones` | WarehouseZone | Skeleton | +| `/api/warehouse-operators` | WarehouseOperator | Skeleton | +| `/api/inventory-stocks` | InventoryStock | Skeleton | +| `/api/inventory-transactions` | InventoryTransaction | Skeleton | + +### Fleet Layer + +Gestion de flotilla, choferes y turnos. + +| Endpoint | Entidad | Estado | +| ----------------- | -------- | -------- | +| `/api/trucks` | Truck | Skeleton | +| `/api/drivers` | Driver | Skeleton | +| `/api/shifts` | Shift | Skeleton | +| `/api/fleet-logs` | FleetLog | Skeleton | + +### Shipment Layer + +Gestion de envios, items y trazabilidad. + +| Endpoint | Entidad | Estado | +| --------------------------- | ------------------ | -------- | +| `/api/shipments` | Shipment | Skeleton | +| `/api/shipment-items` | ShipmentItem | Skeleton | +| `/api/shipment-checkpoints` | ShipmentCheckpoint | Skeleton | +| `/api/shipment-documents` | ShipmentDocument | Skeleton | +| `/api/catalog-items` | CatalogItem | Skeleton | + +### Network Layer + +Gestion de red logistica Hub and Spoke. + +| Endpoint | Entidad | Estado | +| ----------------------- | -------------- | -------- | +| `/api/network-links` | NetworkLink | Skeleton | +| `/api/route-blueprints` | RouteBlueprint | Skeleton | +| `/api/route-steps` | RouteStep | Skeleton | + +--- + +## Autenticacion + +Todos los endpoints protegidos requieren JWT Bearer token: + +```http +Authorization: Bearer +``` + +El token se obtiene via `/api/auth/login` con credenciales validas. + +--- + +## Health Endpoints + +| Endpoint | Descripcion | +| ---------------- | ----------------------------- | +| `GET /health` | Estado del servicio | +| `GET /health/db` | Conectividad de base de datos | + +--- + +## Documentacion Interactiva + +Swagger UI disponible en entorno de desarrollo: + +``` +http://localhost:5100/swagger +``` + +--- + +## Base de Datos + +- **Tablas:** 24 +- **Migraciones:** Aplicadas (EF Core Code First) +- **Provider:** PostgreSQL 17 + +--- + +## Pendientes + +Los siguientes items quedan pendientes para futuras versiones: + +- Implementacion de logica CRUD completa en cada endpoint +- Validaciones de DTOs con FluentValidation +- Calculos de peso volumetrico y costos +- Reglas de negocio (compatibilidad de carga, cadena de frio) +- Generacion de documentos legales (Carta Porte, POD) +- Tests unitarios y de integracion por endpoint + +--- + +## Notas de Desarrollo + +La gestion de endpoints durante desarrollo utiliza herramientas privadas que no forman parte del repositorio. Estas herramientas contienen credenciales y configuraciones sensibles que no deben exponerse publicamente. + +--- + +**Ultima actualizacion:** 2025-12-15 diff --git a/backend/src/Parhelion.API/Controllers/CatalogItemsController.cs b/backend/src/Parhelion.API/Controllers/CatalogItemsController.cs new file mode 100644 index 0000000..97414d3 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/CatalogItemsController.cs @@ -0,0 +1,201 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Catalog; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador CRUD para el Catálogo de Productos. +/// Gestiona los SKUs y sus propiedades (dimensiones, manejo especial, etc). +/// +[ApiController] +[Route("api/catalog-items")] +[Authorize] +public class CatalogItemsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public CatalogItemsController(ParhelionDbContext context) + { + _context = context; + } + + /// + /// Obtiene todos los CatalogItems del tenant. + /// + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.CatalogItems + .Where(x => !x.IsDeleted) + .OrderBy(x => x.Sku) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Obtiene un CatalogItem por su ID. + /// + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.CatalogItems + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + { + return NotFound(new { error = "CatalogItem no encontrado" }); + } + + return Ok(MapToResponse(item)); + } + + /// + /// Busca CatalogItems por SKU (búsqueda parcial). + /// + [HttpGet("search")] + public async Task>> SearchBySku([FromQuery] string sku) + { + if (string.IsNullOrWhiteSpace(sku)) + { + return BadRequest(new { error = "El parámetro 'sku' es requerido" }); + } + + var items = await _context.CatalogItems + .Where(x => !x.IsDeleted && x.Sku.ToLower().Contains(sku.ToLower())) + .OrderBy(x => x.Sku) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Crea un nuevo CatalogItem. + /// + [HttpPost] + public async Task> Create([FromBody] CreateCatalogItemRequest request) + { + // Verificar que el SKU no exista ya en el tenant + var existingSku = await _context.CatalogItems + .AnyAsync(x => x.Sku == request.Sku && !x.IsDeleted); + + if (existingSku) + { + return Conflict(new { error = $"Ya existe un producto con SKU '{request.Sku}'" }); + } + + // Obtener TenantId del usuario actual + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + { + return Unauthorized(new { error = "No se pudo determinar el tenant del usuario" }); + } + + var item = new CatalogItem + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Sku = request.Sku, + Name = request.Name, + Description = request.Description, + BaseUom = request.BaseUom, + DefaultWeightKg = request.DefaultWeightKg, + DefaultWidthCm = request.DefaultWidthCm, + DefaultHeightCm = request.DefaultHeightCm, + DefaultLengthCm = request.DefaultLengthCm, + RequiresRefrigeration = request.RequiresRefrigeration, + IsHazardous = request.IsHazardous, + IsFragile = request.IsFragile, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.CatalogItems.Add(item); + await _context.SaveChangesAsync(); + + return CreatedAtAction( + nameof(GetById), + new { id = item.Id }, + MapToResponse(item)); + } + + /// + /// Actualiza un CatalogItem existente. + /// + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateCatalogItemRequest request) + { + var item = await _context.CatalogItems + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + { + return NotFound(new { error = "CatalogItem no encontrado" }); + } + + item.Name = request.Name; + item.Description = request.Description; + item.BaseUom = request.BaseUom; + item.DefaultWeightKg = request.DefaultWeightKg; + item.DefaultWidthCm = request.DefaultWidthCm; + item.DefaultHeightCm = request.DefaultHeightCm; + item.DefaultLengthCm = request.DefaultLengthCm; + item.RequiresRefrigeration = request.RequiresRefrigeration; + item.IsHazardous = request.IsHazardous; + item.IsFragile = request.IsFragile; + item.IsActive = request.IsActive; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return Ok(MapToResponse(item)); + } + + /// + /// Elimina (soft-delete) un CatalogItem. + /// + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.CatalogItems + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + { + return NotFound(new { error = "CatalogItem no encontrado" }); + } + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return NoContent(); + } + + // ========== Mapping Helper ========== + + private static CatalogItemResponse MapToResponse(CatalogItem item) => new( + item.Id, + item.Sku, + item.Name, + item.Description, + item.BaseUom, + item.DefaultWeightKg, + item.DefaultWidthCm, + item.DefaultHeightCm, + item.DefaultLengthCm, + item.DefaultVolumeM3, + item.RequiresRefrigeration, + item.IsHazardous, + item.IsFragile, + item.IsActive, + item.CreatedAt, + item.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/ClientsController.cs b/backend/src/Parhelion.API/Controllers/ClientsController.cs new file mode 100644 index 0000000..00e2949 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/ClientsController.cs @@ -0,0 +1,172 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de clientes (remitentes/destinatarios). +/// +[ApiController] +[Route("api/clients")] +[Authorize] +public class ClientsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public ClientsController(ParhelionDbContext context) + { + _context = context; + } + + /// + /// Obtiene todos los clientes del tenant. + /// + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Clients + .Where(x => !x.IsDeleted) + .OrderBy(x => x.CompanyName) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Obtiene un cliente por ID. + /// + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Clients + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Cliente no encontrado" }); + + return Ok(MapToResponse(item)); + } + + /// + /// Busca clientes por nombre o email. + /// + [HttpGet("search")] + public async Task>> Search([FromQuery] string q) + { + if (string.IsNullOrWhiteSpace(q)) + return BadRequest(new { error = "El parámetro 'q' es requerido" }); + + var query = q.ToLower(); + var items = await _context.Clients + .Where(x => !x.IsDeleted && + (x.CompanyName.ToLower().Contains(query) || + x.ContactName.ToLower().Contains(query) || + x.Email.ToLower().Contains(query))) + .OrderBy(x => x.CompanyName) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Crea un nuevo cliente. + /// + [HttpPost] + public async Task> Create([FromBody] CreateClientRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var item = new Client + { + Id = Guid.NewGuid(), + TenantId = tenantId, + CompanyName = request.CompanyName, + TradeName = request.TradeName, + ContactName = request.ContactName, + Email = request.Email, + Phone = request.Phone, + TaxId = request.TaxId, + LegalName = request.LegalName, + BillingAddress = request.BillingAddress, + ShippingAddress = request.ShippingAddress, + PreferredProductTypes = request.PreferredProductTypes, + Priority = Enum.TryParse(request.Priority, out var priority) + ? priority : ClientPriority.Normal, + Notes = request.Notes, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.Clients.Add(item); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + /// + /// Actualiza un cliente existente. + /// + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateClientRequest request) + { + var item = await _context.Clients + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Cliente no encontrado" }); + + item.CompanyName = request.CompanyName; + item.TradeName = request.TradeName; + item.ContactName = request.ContactName; + item.Email = request.Email; + item.Phone = request.Phone; + item.TaxId = request.TaxId; + item.LegalName = request.LegalName; + item.BillingAddress = request.BillingAddress; + item.ShippingAddress = request.ShippingAddress; + item.PreferredProductTypes = request.PreferredProductTypes; + item.Priority = Enum.TryParse(request.Priority, out var priority) + ? priority : ClientPriority.Normal; + item.IsActive = request.IsActive; + item.Notes = request.Notes; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + /// + /// Elimina (soft-delete) un cliente. + /// + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Clients + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Cliente no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return NoContent(); + } + + private static ClientResponse MapToResponse(Client x) => new( + x.Id, x.CompanyName, x.TradeName, x.ContactName, x.Email, x.Phone, + x.TaxId, x.LegalName, x.BillingAddress, x.ShippingAddress, + x.PreferredProductTypes, x.Priority.ToString(), x.IsActive, x.Notes, + x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/DriversController.cs b/backend/src/Parhelion.API/Controllers/DriversController.cs new file mode 100644 index 0000000..27a9c81 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/DriversController.cs @@ -0,0 +1,147 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de choferes. +/// +[ApiController] +[Route("api/drivers")] +[Authorize] +public class DriversController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public DriversController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Drivers + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.DefaultTruck) + .Include(x => x.CurrentTruck) + .Where(x => !x.IsDeleted) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Drivers + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.DefaultTruck) + .Include(x => x.CurrentTruck) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Chofer no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("active")] + public async Task>> Active() + { + var items = await _context.Drivers + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.DefaultTruck) + .Include(x => x.CurrentTruck) + .Where(x => !x.IsDeleted && x.Status == DriverStatus.Available) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("by-status/{status}")] + public async Task>> ByStatus(string status) + { + if (!Enum.TryParse(status, out var driverStatus)) + return BadRequest(new { error = "Estatus de chofer inválido" }); + + var items = await _context.Drivers + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.DefaultTruck) + .Include(x => x.CurrentTruck) + .Where(x => !x.IsDeleted && x.Status == driverStatus) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateDriverRequest request) + { + var item = new Driver + { + Id = Guid.NewGuid(), + EmployeeId = request.EmployeeId, + LicenseNumber = request.LicenseNumber, + LicenseType = request.LicenseType, + LicenseExpiration = request.LicenseExpiration, + DefaultTruckId = request.DefaultTruckId, + Status = Enum.TryParse(request.Status, out var s) ? s : DriverStatus.Available, + CreatedAt = DateTime.UtcNow + }; + + _context.Drivers.Add(item); + await _context.SaveChangesAsync(); + + item = await _context.Drivers + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.DefaultTruck) + .Include(x => x.CurrentTruck) + .FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateDriverRequest request) + { + var item = await _context.Drivers + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.DefaultTruck) + .Include(x => x.CurrentTruck) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Chofer no encontrado" }); + + item.LicenseNumber = request.LicenseNumber; + item.LicenseType = request.LicenseType; + item.LicenseExpiration = request.LicenseExpiration; + item.DefaultTruckId = request.DefaultTruckId; + item.CurrentTruckId = request.CurrentTruckId; + item.Status = Enum.TryParse(request.Status, out var s) ? s : item.Status; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Drivers.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Chofer no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static DriverResponse MapToResponse(Driver x) => new( + x.Id, x.EmployeeId, x.Employee?.User?.FullName ?? "", + x.LicenseNumber, x.LicenseType, x.LicenseExpiration, + x.DefaultTruckId, x.DefaultTruck?.Plate, + x.CurrentTruckId, x.CurrentTruck?.Plate, + x.Status.ToString(), x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/EmployeesController.cs b/backend/src/Parhelion.API/Controllers/EmployeesController.cs new file mode 100644 index 0000000..e4126fa --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/EmployeesController.cs @@ -0,0 +1,165 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de empleados. +/// +[ApiController] +[Route("api/employees")] +[Authorize] +public class EmployeesController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public EmployeesController(ParhelionDbContext context) + { + _context = context; + } + + /// + /// Obtiene todos los empleados del tenant. + /// + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Employees + .Include(x => x.User) + .Where(x => !x.IsDeleted) + .OrderBy(x => x.User.FullName) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Obtiene un empleado por ID. + /// + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Employees + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Empleado no encontrado" }); + + return Ok(MapToResponse(item)); + } + + /// + /// Obtiene empleados por departamento. + /// + [HttpGet("by-department/{department}")] + public async Task>> ByDepartment(string department) + { + var items = await _context.Employees + .Include(x => x.User) + .Where(x => !x.IsDeleted && x.Department == department) + .OrderBy(x => x.User.FullName) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Crea un nuevo empleado. + /// + [HttpPost] + public async Task> Create([FromBody] CreateEmployeeRequest request) + { + var existingUser = await _context.Employees + .AnyAsync(x => x.UserId == request.UserId && !x.IsDeleted); + + if (existingUser) + return Conflict(new { error = "Este usuario ya tiene un registro de empleado" }); + + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var item = new Employee + { + Id = Guid.NewGuid(), + TenantId = tenantId, + UserId = request.UserId, + Phone = request.Phone, + Rfc = request.Rfc, + Nss = request.Nss, + Curp = request.Curp, + EmergencyContact = request.EmergencyContact, + EmergencyPhone = request.EmergencyPhone, + HireDate = request.HireDate, + ShiftId = request.ShiftId, + Department = request.Department, + CreatedAt = DateTime.UtcNow + }; + + _context.Employees.Add(item); + await _context.SaveChangesAsync(); + + item = await _context.Employees.Include(x => x.User).FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + /// + /// Actualiza un empleado existente. + /// + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateEmployeeRequest request) + { + var item = await _context.Employees + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Empleado no encontrado" }); + + item.Phone = request.Phone; + item.Rfc = request.Rfc; + item.Nss = request.Nss; + item.Curp = request.Curp; + item.EmergencyContact = request.EmergencyContact; + item.EmergencyPhone = request.EmergencyPhone; + item.HireDate = request.HireDate; + item.ShiftId = request.ShiftId; + item.Department = request.Department; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + /// + /// Elimina (soft-delete) un empleado. + /// + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Employees + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Empleado no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return NoContent(); + } + + private static EmployeeResponse MapToResponse(Employee x) => new( + x.Id, x.UserId, x.User?.FullName ?? "", x.User?.Email ?? "", + x.Phone, x.Rfc, x.Nss, x.Curp, x.EmergencyContact, x.EmergencyPhone, + x.HireDate, x.ShiftId, x.Department, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/FleetLogsController.cs b/backend/src/Parhelion.API/Controllers/FleetLogsController.cs new file mode 100644 index 0000000..b5b9ebe --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/FleetLogsController.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para bitácora de flotilla (cambios de camión). +/// Los logs son inmutables - solo se crean y consultan. +/// +[ApiController] +[Route("api/fleet-logs")] +[Authorize] +public class FleetLogsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public FleetLogsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.FleetLogs + .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) + .Include(x => x.OldTruck) + .Include(x => x.NewTruck) + .Include(x => x.CreatedBy) + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.Timestamp) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.FleetLogs + .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) + .Include(x => x.OldTruck) + .Include(x => x.NewTruck) + .Include(x => x.CreatedBy) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Log no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-driver/{driverId:guid}")] + public async Task>> ByDriver(Guid driverId) + { + var items = await _context.FleetLogs + .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) + .Include(x => x.OldTruck) + .Include(x => x.NewTruck) + .Include(x => x.CreatedBy) + .Where(x => !x.IsDeleted && x.DriverId == driverId) + .OrderByDescending(x => x.Timestamp) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("by-truck/{truckId:guid}")] + public async Task>> ByTruck(Guid truckId) + { + var items = await _context.FleetLogs + .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) + .Include(x => x.OldTruck) + .Include(x => x.NewTruck) + .Include(x => x.CreatedBy) + .Where(x => !x.IsDeleted && (x.OldTruckId == truckId || x.NewTruckId == truckId)) + .OrderByDescending(x => x.Timestamp) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateFleetLogRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); + if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId)) + return Unauthorized(new { error = "No se pudo determinar el usuario" }); + + var item = new FleetLog + { + Id = Guid.NewGuid(), + TenantId = tenantId, + DriverId = request.DriverId, + OldTruckId = request.OldTruckId, + NewTruckId = request.NewTruckId, + Reason = Enum.TryParse(request.Reason, out var r) ? r : FleetLogReason.Reassignment, + Timestamp = DateTime.UtcNow, + CreatedByUserId = userId, + CreatedAt = DateTime.UtcNow + }; + + _context.FleetLogs.Add(item); + + // Update driver's current truck + var driver = await _context.Drivers.FirstOrDefaultAsync(d => d.Id == request.DriverId); + if (driver != null) + { + driver.CurrentTruckId = request.NewTruckId; + } + + await _context.SaveChangesAsync(); + + item = await _context.FleetLogs + .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) + .Include(x => x.OldTruck) + .Include(x => x.NewTruck) + .Include(x => x.CreatedBy) + .FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + // No PUT/DELETE - logs are immutable + + private static FleetLogResponse MapToResponse(FleetLog x) => new( + x.Id, x.DriverId, x.Driver?.Employee?.User?.FullName ?? "", + x.OldTruckId, x.OldTruck?.Plate, + x.NewTruckId, x.NewTruck?.Plate ?? "", + x.Reason.ToString(), x.Timestamp, + x.CreatedByUserId, x.CreatedBy?.FullName ?? "", + x.CreatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/InventoryStocksController.cs b/backend/src/Parhelion.API/Controllers/InventoryStocksController.cs new file mode 100644 index 0000000..f95581c --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/InventoryStocksController.cs @@ -0,0 +1,141 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para inventario (stocks). +/// +[ApiController] +[Route("api/inventory-stocks")] +[Authorize] +public class InventoryStocksController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public InventoryStocksController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.InventoryStocks + .Include(x => x.Zone) + .Include(x => x.Product) + .Where(x => !x.IsDeleted) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.InventoryStocks + .Include(x => x.Zone) + .Include(x => x.Product) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Stock no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-product/{productId:guid}")] + public async Task>> ByProduct(Guid productId) + { + var items = await _context.InventoryStocks + .Include(x => x.Zone) + .Include(x => x.Product) + .Where(x => !x.IsDeleted && x.ProductId == productId) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("by-zone/{zoneId:guid}")] + public async Task>> ByZone(Guid zoneId) + { + var items = await _context.InventoryStocks + .Include(x => x.Zone) + .Include(x => x.Product) + .Where(x => !x.IsDeleted && x.ZoneId == zoneId) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateInventoryStockRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var item = new InventoryStock + { + Id = Guid.NewGuid(), + TenantId = tenantId, + ZoneId = request.ZoneId, + ProductId = request.ProductId, + Quantity = request.Quantity, + QuantityReserved = request.QuantityReserved, + BatchNumber = request.BatchNumber, + ExpiryDate = request.ExpiryDate, + UnitCost = request.UnitCost, + CreatedAt = DateTime.UtcNow + }; + + _context.InventoryStocks.Add(item); + await _context.SaveChangesAsync(); + + item = await _context.InventoryStocks + .Include(x => x.Zone) + .Include(x => x.Product) + .FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateInventoryStockRequest request) + { + var item = await _context.InventoryStocks + .Include(x => x.Zone) + .Include(x => x.Product) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Stock no encontrado" }); + + item.Quantity = request.Quantity; + item.QuantityReserved = request.QuantityReserved; + item.BatchNumber = request.BatchNumber; + item.ExpiryDate = request.ExpiryDate; + item.LastCountDate = request.LastCountDate; + item.UnitCost = request.UnitCost; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.InventoryStocks.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Stock no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static InventoryStockResponse MapToResponse(InventoryStock x) => new( + x.Id, x.ZoneId, x.Zone?.Name ?? "", x.ProductId, x.Product?.Name ?? "", x.Product?.Sku ?? "", + x.Quantity, x.QuantityReserved, x.QuantityAvailable, x.BatchNumber, x.ExpiryDate, + x.LastCountDate, x.UnitCost, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/InventoryTransactionsController.cs b/backend/src/Parhelion.API/Controllers/InventoryTransactionsController.cs new file mode 100644 index 0000000..33a319d --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/InventoryTransactionsController.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para transacciones de inventario (Kardex). +/// Las transacciones son inmutables - solo se crean y consultan. +/// +[ApiController] +[Route("api/inventory-transactions")] +[Authorize] +public class InventoryTransactionsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public InventoryTransactionsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.InventoryTransactions + .Include(x => x.Product) + .Include(x => x.OriginZone) + .Include(x => x.DestinationZone) + .Include(x => x.PerformedBy) + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.Timestamp) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.InventoryTransactions + .Include(x => x.Product) + .Include(x => x.OriginZone) + .Include(x => x.DestinationZone) + .Include(x => x.PerformedBy) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Transacción no encontrada" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-product/{productId:guid}")] + public async Task>> ByProduct(Guid productId) + { + var items = await _context.InventoryTransactions + .Include(x => x.Product) + .Include(x => x.OriginZone) + .Include(x => x.DestinationZone) + .Include(x => x.PerformedBy) + .Where(x => !x.IsDeleted && x.ProductId == productId) + .OrderByDescending(x => x.Timestamp) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("by-zone/{zoneId:guid}")] + public async Task>> ByZone(Guid zoneId) + { + var items = await _context.InventoryTransactions + .Include(x => x.Product) + .Include(x => x.OriginZone) + .Include(x => x.DestinationZone) + .Include(x => x.PerformedBy) + .Where(x => !x.IsDeleted && (x.OriginZoneId == zoneId || x.DestinationZoneId == zoneId)) + .OrderByDescending(x => x.Timestamp) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateInventoryTransactionRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); + if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId)) + return Unauthorized(new { error = "No se pudo determinar el usuario" }); + + var item = new InventoryTransaction + { + Id = Guid.NewGuid(), + TenantId = tenantId, + ProductId = request.ProductId, + OriginZoneId = request.OriginZoneId, + DestinationZoneId = request.DestinationZoneId, + Quantity = request.Quantity, + TransactionType = Enum.TryParse(request.TransactionType, out var t) + ? t : InventoryTransactionType.Adjustment, + PerformedByUserId = userId, + ShipmentId = request.ShipmentId, + BatchNumber = request.BatchNumber, + Remarks = request.Remarks, + Timestamp = DateTime.UtcNow, + CreatedAt = DateTime.UtcNow + }; + + _context.InventoryTransactions.Add(item); + await _context.SaveChangesAsync(); + + item = await _context.InventoryTransactions + .Include(x => x.Product) + .Include(x => x.OriginZone) + .Include(x => x.DestinationZone) + .Include(x => x.PerformedBy) + .FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + // No PUT/DELETE - transactions are immutable + + private static InventoryTransactionResponse MapToResponse(InventoryTransaction x) => new( + x.Id, x.ProductId, x.Product?.Name ?? "", + x.OriginZoneId, x.OriginZone?.Name, + x.DestinationZoneId, x.DestinationZone?.Name, + x.Quantity, x.TransactionType.ToString(), + x.PerformedByUserId, x.PerformedBy?.FullName ?? "", + x.ShipmentId, x.BatchNumber, x.Remarks, + x.Timestamp, x.CreatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/LocationsController.cs b/backend/src/Parhelion.API/Controllers/LocationsController.cs new file mode 100644 index 0000000..e7510d2 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/LocationsController.cs @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de ubicaciones (almacenes, hubs, cross-docks). +/// +[ApiController] +[Route("api/locations")] +[Authorize] +public class LocationsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public LocationsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Locations + .Where(x => !x.IsDeleted) + .OrderBy(x => x.Code) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Locations.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Ubicación no encontrada" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-type/{type}")] + public async Task>> ByType(string type) + { + if (!Enum.TryParse(type, out var locType)) + return BadRequest(new { error = "Tipo de ubicación inválido" }); + + var items = await _context.Locations + .Where(x => !x.IsDeleted && x.Type == locType) + .OrderBy(x => x.Code) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateLocationRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var existing = await _context.Locations.AnyAsync(x => x.Code == request.Code && !x.IsDeleted); + if (existing) return Conflict(new { error = $"Ya existe ubicación con código '{request.Code}'" }); + + var item = new Location + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Code = request.Code, + Name = request.Name, + Type = Enum.TryParse(request.Type, out var t) ? t : LocationType.Warehouse, + FullAddress = request.FullAddress, + Latitude = request.Latitude, + Longitude = request.Longitude, + CanReceive = request.CanReceive, + CanDispatch = request.CanDispatch, + IsInternal = request.IsInternal, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.Locations.Add(item); + await _context.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateLocationRequest request) + { + var item = await _context.Locations.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Ubicación no encontrada" }); + + item.Code = request.Code; + item.Name = request.Name; + item.Type = Enum.TryParse(request.Type, out var t) ? t : item.Type; + item.FullAddress = request.FullAddress; + item.Latitude = request.Latitude; + item.Longitude = request.Longitude; + item.CanReceive = request.CanReceive; + item.CanDispatch = request.CanDispatch; + item.IsInternal = request.IsInternal; + item.IsActive = request.IsActive; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Locations.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Ubicación no encontrada" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static LocationResponse MapToResponse(Location x) => new( + x.Id, x.Code, x.Name, x.Type.ToString(), x.FullAddress, + x.Latitude, x.Longitude, x.CanReceive, x.CanDispatch, + x.IsInternal, x.IsActive, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/NetworkLinksController.cs b/backend/src/Parhelion.API/Controllers/NetworkLinksController.cs new file mode 100644 index 0000000..9d13004 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/NetworkLinksController.cs @@ -0,0 +1,129 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Network; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para enlaces de red logística. +/// +[ApiController] +[Route("api/network-links")] +[Authorize] +public class NetworkLinksController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public NetworkLinksController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.NetworkLinks + .Include(x => x.OriginLocation) + .Include(x => x.DestinationLocation) + .Where(x => !x.IsDeleted) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.NetworkLinks + .Include(x => x.OriginLocation) + .Include(x => x.DestinationLocation) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Enlace no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-location/{locationId:guid}")] + public async Task>> ByLocation(Guid locationId) + { + var items = await _context.NetworkLinks + .Include(x => x.OriginLocation) + .Include(x => x.DestinationLocation) + .Where(x => !x.IsDeleted && + (x.OriginLocationId == locationId || x.DestinationLocationId == locationId)) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateNetworkLinkRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var item = new NetworkLink + { + Id = Guid.NewGuid(), + TenantId = tenantId, + OriginLocationId = request.OriginLocationId, + DestinationLocationId = request.DestinationLocationId, + LinkType = Enum.TryParse(request.LinkType, out var lt) ? lt : NetworkLinkType.FirstMile, + TransitTime = request.TransitTime, + IsBidirectional = request.IsBidirectional, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.NetworkLinks.Add(item); + await _context.SaveChangesAsync(); + + item = await _context.NetworkLinks + .Include(x => x.OriginLocation) + .Include(x => x.DestinationLocation) + .FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateNetworkLinkRequest request) + { + var item = await _context.NetworkLinks + .Include(x => x.OriginLocation) + .Include(x => x.DestinationLocation) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Enlace no encontrado" }); + + item.LinkType = Enum.TryParse(request.LinkType, out var lt) ? lt : item.LinkType; + item.TransitTime = request.TransitTime; + item.IsBidirectional = request.IsBidirectional; + item.IsActive = request.IsActive; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.NetworkLinks.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Enlace no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static NetworkLinkResponse MapToResponse(NetworkLink x) => new( + x.Id, x.OriginLocationId, x.OriginLocation?.Name ?? "", + x.DestinationLocationId, x.DestinationLocation?.Name ?? "", + x.LinkType.ToString(), x.TransitTime, x.IsBidirectional, x.IsActive, + x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/RolesController.cs b/backend/src/Parhelion.API/Controllers/RolesController.cs new file mode 100644 index 0000000..82e8e39 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/RolesController.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de roles. +/// Los roles son globales (no multi-tenant). +/// +[ApiController] +[Route("api/roles")] +[Authorize] +public class RolesController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public RolesController(ParhelionDbContext context) + { + _context = context; + } + + /// + /// Obtiene todos los roles. + /// + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Roles + .Where(x => !x.IsDeleted) + .OrderBy(x => x.Name) + .Select(x => new RoleResponse(x.Id, x.Name, x.Description, x.CreatedAt)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Obtiene un rol por ID. + /// + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Roles + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Rol no encontrado" }); + + return Ok(new RoleResponse(item.Id, item.Name, item.Description, item.CreatedAt)); + } + + /// + /// Crea un nuevo rol. + /// + [HttpPost] + public async Task> Create([FromBody] CreateRoleRequest request) + { + var existing = await _context.Roles + .AnyAsync(x => x.Name == request.Name && !x.IsDeleted); + + if (existing) + return Conflict(new { error = "Ya existe un rol con ese nombre" }); + + var item = new Role + { + Id = Guid.NewGuid(), + Name = request.Name, + Description = request.Description, + CreatedAt = DateTime.UtcNow + }; + + _context.Roles.Add(item); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetById), new { id = item.Id }, + new RoleResponse(item.Id, item.Name, item.Description, item.CreatedAt)); + } + + /// + /// Actualiza un rol existente. + /// + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateRoleRequest request) + { + var item = await _context.Roles + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Rol no encontrado" }); + + item.Name = request.Name; + item.Description = request.Description; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(new RoleResponse(item.Id, item.Name, item.Description, item.CreatedAt)); + } + + /// + /// Elimina (soft-delete) un rol. + /// + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Roles + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Rol no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return NoContent(); + } +} diff --git a/backend/src/Parhelion.API/Controllers/RouteBlueprintsController.cs b/backend/src/Parhelion.API/Controllers/RouteBlueprintsController.cs new file mode 100644 index 0000000..77deed1 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/RouteBlueprintsController.cs @@ -0,0 +1,112 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Network; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para rutas predefinidas. +/// +[ApiController] +[Route("api/route-blueprints")] +[Authorize] +public class RouteBlueprintsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public RouteBlueprintsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.RouteBlueprints + .Where(x => !x.IsDeleted) + .OrderBy(x => x.Name) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.RouteBlueprints.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Ruta no encontrada" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("active")] + public async Task>> Active() + { + var items = await _context.RouteBlueprints + .Where(x => !x.IsDeleted && x.IsActive) + .OrderBy(x => x.Name) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateRouteBlueprintRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var item = new RouteBlueprint + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Name = request.Name, + Description = request.Description, + TotalSteps = 0, + TotalTransitTime = TimeSpan.Zero, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.RouteBlueprints.Add(item); + await _context.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateRouteBlueprintRequest request) + { + var item = await _context.RouteBlueprints.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Ruta no encontrada" }); + + item.Name = request.Name; + item.Description = request.Description; + item.TotalSteps = request.TotalSteps; + item.TotalTransitTime = request.TotalTransitTime; + item.IsActive = request.IsActive; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.RouteBlueprints.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Ruta no encontrada" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static RouteBlueprintResponse MapToResponse(RouteBlueprint x) => new( + x.Id, x.Name, x.Description, x.TotalSteps, x.TotalTransitTime, + x.IsActive, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/RouteStepsController.cs b/backend/src/Parhelion.API/Controllers/RouteStepsController.cs new file mode 100644 index 0000000..e653edc --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/RouteStepsController.cs @@ -0,0 +1,141 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Network; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para pasos de ruta. +/// +[ApiController] +[Route("api/route-steps")] +[Authorize] +public class RouteStepsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public RouteStepsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.RouteSteps + .Include(x => x.RouteBlueprint) + .Include(x => x.Location) + .Where(x => !x.IsDeleted) + .OrderBy(x => x.RouteBlueprintId).ThenBy(x => x.StepOrder) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.RouteSteps + .Include(x => x.RouteBlueprint) + .Include(x => x.Location) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Paso no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-route/{routeId:guid}")] + public async Task>> ByRoute(Guid routeId) + { + var items = await _context.RouteSteps + .Include(x => x.RouteBlueprint) + .Include(x => x.Location) + .Where(x => !x.IsDeleted && x.RouteBlueprintId == routeId) + .OrderBy(x => x.StepOrder) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateRouteStepRequest request) + { + var item = new RouteStep + { + Id = Guid.NewGuid(), + RouteBlueprintId = request.RouteBlueprintId, + LocationId = request.LocationId, + StepOrder = request.StepOrder, + StandardTransitTime = request.StandardTransitTime, + StepType = Enum.TryParse(request.StepType, out var st) ? st : RouteStepType.Origin, + CreatedAt = DateTime.UtcNow + }; + + _context.RouteSteps.Add(item); + + // Update route totals + var route = await _context.RouteBlueprints.FirstOrDefaultAsync(r => r.Id == request.RouteBlueprintId); + if (route != null) + { + route.TotalSteps++; + route.TotalTransitTime += request.StandardTransitTime; + } + + await _context.SaveChangesAsync(); + + item = await _context.RouteSteps + .Include(x => x.RouteBlueprint) + .Include(x => x.Location) + .FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateRouteStepRequest request) + { + var item = await _context.RouteSteps + .Include(x => x.RouteBlueprint) + .Include(x => x.Location) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Paso no encontrado" }); + + item.LocationId = request.LocationId; + item.StepOrder = request.StepOrder; + item.StandardTransitTime = request.StandardTransitTime; + item.StepType = Enum.TryParse(request.StepType, out var st) ? st : item.StepType; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.RouteSteps.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Paso no encontrado" }); + + // Update route totals + var route = await _context.RouteBlueprints.FirstOrDefaultAsync(r => r.Id == item.RouteBlueprintId); + if (route != null) + { + route.TotalSteps--; + route.TotalTransitTime -= item.StandardTransitTime; + } + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static RouteStepResponse MapToResponse(RouteStep x) => new( + x.Id, x.RouteBlueprintId, x.RouteBlueprint?.Name ?? "", + x.LocationId, x.Location?.Name ?? "", + x.StepOrder, x.StandardTransitTime, x.StepType.ToString(), + x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/SchemaController.cs b/backend/src/Parhelion.API/Controllers/SchemaController.cs new file mode 100644 index 0000000..7bdb505 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/SchemaController.cs @@ -0,0 +1,283 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Expone metadatos del schema de base de datos para herramientas de administración. +/// Este endpoint es de solo lectura y no expone datos, solo estructura. +/// +[ApiController] +[Route("api/[controller]")] +public class SchemaController : ControllerBase +{ + private readonly ParhelionDbContext _context; + private static readonly object _cacheLock = new(); + private static SchemaMetadataResponse? _cachedSchema; + private static DateTime _cacheExpiry = DateTime.MinValue; + private const int CacheTtlMinutes = 60; // Cache por 1 hora + + public SchemaController(ParhelionDbContext context) + { + _context = context; + } + + /// + /// Obtiene metadatos del schema de base de datos. + /// Información pública: nombres de tablas, columnas y relaciones. + /// No expone datos sensibles ni requiere autenticación. + /// + [HttpGet("metadata")] + [ResponseCache(Duration = 3600)] // Browser cache 1 hora + public ActionResult GetSchemaMetadata() + { + // Check cache + lock (_cacheLock) + { + if (_cachedSchema != null && DateTime.UtcNow < _cacheExpiry) + { + return Ok(_cachedSchema); + } + } + + var schema = BuildSchemaFromEfCore(); + + // Update cache + lock (_cacheLock) + { + _cachedSchema = schema; + _cacheExpiry = DateTime.UtcNow.AddMinutes(CacheTtlMinutes); + } + + return Ok(schema); + } + + /// + /// Fuerza recarga del cache de schema (requiere autenticación en producción). + /// + [HttpPost("refresh")] + public ActionResult RefreshCache() + { + lock (_cacheLock) + { + _cachedSchema = null; + _cacheExpiry = DateTime.MinValue; + } + return Ok(new { message = "Schema cache cleared" }); + } + + private SchemaMetadataResponse BuildSchemaFromEfCore() + { + var tables = new List(); + var model = _context.Model; + + // Categorización por módulo + var moduleMapping = new Dictionary + { + // Core + { "Tenant", "core" }, + { "User", "core" }, + { "Role", "core" }, + { "RefreshToken", "core" }, + { "Client", "core" }, + + // Employee + { "Employee", "employee" }, + { "Shift", "employee" }, + + // Fleet + { "Driver", "fleet" }, + { "Truck", "fleet" }, + { "FleetLog", "fleet" }, + + // Warehouse + { "Location", "warehouse" }, + { "WarehouseZone", "warehouse" }, + { "WarehouseOperator", "warehouse" }, + + // Inventory + { "CatalogItem", "inventory" }, + { "InventoryStock", "inventory" }, + { "InventoryTransaction", "inventory" }, + + // Shipment + { "Shipment", "shipment" }, + { "ShipmentItem", "shipment" }, + { "ShipmentCheckpoint", "shipment" }, + { "ShipmentDocument", "shipment" }, + + // Network/Routing + { "NetworkLink", "network" }, + { "RouteBlueprint", "network" }, + { "RouteStep", "network" } + }; + + var descriptions = new Dictionary + { + { "Tenant", "Multi-tenant root entity" }, + { "User", "System users with roles" }, + { "Role", "Admin, Driver, Warehouse, Demo" }, + { "RefreshToken", "JWT refresh tokens" }, + { "Client", "B2B clients (senders/recipients)" }, + { "Employee", "Employee profiles (v0.4.3)" }, + { "Shift", "Work shifts configuration" }, + { "Driver", "Fleet drivers with MX legal data" }, + { "Truck", "DryBox, Refrigerated, HAZMAT..." }, + { "FleetLog", "Driver-Truck changes log" }, + { "Location", "Hubs, Warehouses, Cross-docks" }, + { "WarehouseZone", "Zones within locations" }, + { "WarehouseOperator", "Operators assigned to zones" }, + { "CatalogItem", "Product catalog (v0.4.4)" }, + { "InventoryStock", "Stock by zone/lot (v0.4.4)" }, + { "InventoryTransaction", "Kardex movements (v0.4.4)" }, + { "Shipment", "Shipments PAR-XXXXXX" }, + { "ShipmentItem", "Items with volumetric weight" }, + { "ShipmentCheckpoint", "Immutable tracking events" }, + { "ShipmentDocument", "B2B docs: Waybill, POD..." }, + { "NetworkLink", "FirstMile, LineHaul, LastMile" }, + { "RouteBlueprint", "Predefined Hub & Spoke routes" }, + { "RouteStep", "Route stops with transit times" } + }; + + // Posiciones para layout visual (grid layout) + var positions = new Dictionary + { + // Row 1: Core + { "Tenant", (50, 50) }, + { "Role", (280, 50) }, + { "User", (510, 50) }, + { "RefreshToken", (740, 50) }, + + // Row 2: Employee + Client + { "Employee", (50, 300) }, + { "Shift", (280, 300) }, + { "Client", (510, 300) }, + + // Row 3: Fleet + { "Truck", (50, 550) }, + { "Driver", (280, 550) }, + { "FleetLog", (510, 550) }, + + // Row 4: Warehouse + Inventory (RIGHT SIDE) + { "Location", (970, 50) }, + { "WarehouseZone", (1200, 50) }, + { "WarehouseOperator", (970, 300) }, + { "CatalogItem", (1200, 300) }, + { "InventoryStock", (970, 550) }, + { "InventoryTransaction", (1200, 550) }, + + // Row 5: Shipment (BOTTOM) + { "Shipment", (50, 800) }, + { "ShipmentItem", (280, 800) }, + { "ShipmentCheckpoint", (510, 800) }, + { "ShipmentDocument", (740, 800) }, + + // Row 6: Network (BOTTOM RIGHT) + { "NetworkLink", (970, 800) }, + { "RouteBlueprint", (1200, 800) }, + { "RouteStep", (1430, 800) } + }; + + foreach (var entityType in model.GetEntityTypes()) + { + var entityName = entityType.ClrType.Name; + + // Skip owned types + if (entityType.IsOwned()) continue; + + var fields = new List(); + + foreach (var property in entityType.GetProperties()) + { + var isPk = property.IsPrimaryKey(); + var isFk = property.IsForeignKey(); + string? fkTarget = null; + + if (isFk) + { + var fkEntity = property.GetContainingForeignKeys() + .FirstOrDefault()?.PrincipalEntityType?.ClrType.Name; + fkTarget = fkEntity != null ? $"{fkEntity}s" : null; + } + + fields.Add(new FieldMetadata + { + Name = property.Name, + Pk = isPk, + Fk = fkTarget, + Type = GetSimpleTypeName(property.ClrType), + IsNullable = property.IsNullable + }); + } + + var pos = positions.GetValueOrDefault(entityName, (50, 50)); + + tables.Add(new TableMetadata + { + Name = $"{entityName}s", // Pluralize + Type = moduleMapping.GetValueOrDefault(entityName, "core"), + Description = descriptions.GetValueOrDefault(entityName, entityName), + X = pos.Item1, + Y = pos.Item2, + Fields = fields + }); + } + + return new SchemaMetadataResponse + { + Version = "0.4.5", + GeneratedAt = DateTime.UtcNow, + TableCount = tables.Count, + Tables = tables.OrderBy(t => t.Type).ThenBy(t => t.Name).ToList() + }; + } + + private static string GetSimpleTypeName(Type type) + { + if (Nullable.GetUnderlyingType(type) is { } underlying) + type = underlying; + + return type.Name switch + { + nameof(Guid) => "uuid", + nameof(String) => "string", + nameof(Int32) => "int", + nameof(Int64) => "long", + nameof(Decimal) => "decimal", + nameof(Boolean) => "bool", + nameof(DateTime) => "datetime", + nameof(TimeSpan) => "timespan", + _ when type.IsEnum => "enum", + _ => type.Name.ToLower() + }; + } +} + +// DTOs +public record SchemaMetadataResponse +{ + public string Version { get; init; } = ""; + public DateTime GeneratedAt { get; init; } + public int TableCount { get; init; } + public List Tables { get; init; } = new(); +} + +public record TableMetadata +{ + public string Name { get; init; } = ""; + public string Type { get; init; } = ""; + public string Description { get; init; } = ""; + public int X { get; init; } + public int Y { get; init; } + public List Fields { get; init; } = new(); +} + +public record FieldMetadata +{ + public string Name { get; init; } = ""; + public bool Pk { get; init; } + public string? Fk { get; init; } + public string Type { get; init; } = ""; + public bool IsNullable { get; init; } +} diff --git a/backend/src/Parhelion.API/Controllers/ShiftsController.cs b/backend/src/Parhelion.API/Controllers/ShiftsController.cs new file mode 100644 index 0000000..17ed407 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/ShiftsController.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para turnos de trabajo. +/// +[ApiController] +[Route("api/shifts")] +[Authorize] +public class ShiftsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public ShiftsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Shifts + .Where(x => !x.IsDeleted) + .OrderBy(x => x.Name) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Shifts.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Turno no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpPost] + public async Task> Create([FromBody] CreateShiftRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var item = new Shift + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Name = request.Name, + StartTime = request.StartTime, + EndTime = request.EndTime, + DaysOfWeek = request.DaysOfWeek, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.Shifts.Add(item); + await _context.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateShiftRequest request) + { + var item = await _context.Shifts.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Turno no encontrado" }); + + item.Name = request.Name; + item.StartTime = request.StartTime; + item.EndTime = request.EndTime; + item.DaysOfWeek = request.DaysOfWeek; + item.IsActive = request.IsActive; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Shifts.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Turno no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static ShiftResponse MapToResponse(Shift x) => new( + x.Id, x.Name, x.StartTime, x.EndTime, x.DaysOfWeek, + x.IsActive, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/ShipmentCheckpointsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentCheckpointsController.cs new file mode 100644 index 0000000..77c9936 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/ShipmentCheckpointsController.cs @@ -0,0 +1,109 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para checkpoints de envío (trazabilidad). +/// Los checkpoints son inmutables. +/// +[ApiController] +[Route("api/shipment-checkpoints")] +[Authorize] +public class ShipmentCheckpointsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public ShipmentCheckpointsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await GetCheckpointWithIncludes() + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.Timestamp) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await GetCheckpointWithIncludes().FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Checkpoint no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-shipment/{shipmentId:guid}")] + public async Task>> ByShipment(Guid shipmentId) + { + var items = await GetCheckpointWithIncludes() + .Where(x => !x.IsDeleted && x.ShipmentId == shipmentId) + .OrderByDescending(x => x.Timestamp) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateShipmentCheckpointRequest request) + { + var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); + if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId)) + return Unauthorized(new { error = "No se pudo determinar el usuario" }); + + var item = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = request.ShipmentId, + LocationId = request.LocationId, + StatusCode = Enum.TryParse(request.StatusCode, out var s) ? s : CheckpointStatus.Loaded, + Remarks = request.Remarks, + Timestamp = DateTime.UtcNow, + CreatedByUserId = userId, + HandledByDriverId = request.HandledByDriverId, + LoadedOntoTruckId = request.LoadedOntoTruckId, + ActionType = request.ActionType, + PreviousCustodian = request.PreviousCustodian, + NewCustodian = request.NewCustodian, + HandledByWarehouseOperatorId = request.HandledByWarehouseOperatorId, + Latitude = request.Latitude, + Longitude = request.Longitude, + CreatedAt = DateTime.UtcNow + }; + + _context.ShipmentCheckpoints.Add(item); + await _context.SaveChangesAsync(); + + item = await GetCheckpointWithIncludes().FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + // No PUT/DELETE - checkpoints are immutable + + private IQueryable GetCheckpointWithIncludes() => _context.ShipmentCheckpoints + .Include(x => x.Location) + .Include(x => x.CreatedBy) + .Include(x => x.HandledByDriver).ThenInclude(d => d!.Employee).ThenInclude(e => e.User) + .Include(x => x.LoadedOntoTruck); + + private static ShipmentCheckpointResponse MapToResponse(ShipmentCheckpoint x) => new( + x.Id, x.ShipmentId, x.LocationId, x.Location?.Name, + x.StatusCode.ToString(), x.Remarks, x.Timestamp, + x.CreatedByUserId, x.CreatedBy?.FullName ?? "", + x.HandledByDriverId, x.HandledByDriver?.Employee?.User?.FullName, + x.LoadedOntoTruckId, x.LoadedOntoTruck?.Plate, + x.ActionType, x.PreviousCustodian, x.NewCustodian, + x.Latitude, x.Longitude, x.CreatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/ShipmentDocumentsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentDocumentsController.cs new file mode 100644 index 0000000..c034efe --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/ShipmentDocumentsController.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para documentos de envío. +/// +[ApiController] +[Route("api/shipment-documents")] +[Authorize] +public class ShipmentDocumentsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public ShipmentDocumentsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.ShipmentDocuments + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.GeneratedAt) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.ShipmentDocuments.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Documento no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-shipment/{shipmentId:guid}")] + public async Task>> ByShipment(Guid shipmentId) + { + var items = await _context.ShipmentDocuments + .Where(x => !x.IsDeleted && x.ShipmentId == shipmentId) + .OrderByDescending(x => x.GeneratedAt) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("by-type/{type}")] + public async Task>> ByType(string type) + { + if (!Enum.TryParse(type, out var docType)) + return BadRequest(new { error = "Tipo de documento inválido" }); + + var items = await _context.ShipmentDocuments + .Where(x => !x.IsDeleted && x.DocumentType == docType) + .OrderByDescending(x => x.GeneratedAt) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateShipmentDocumentRequest request) + { + var item = new ShipmentDocument + { + Id = Guid.NewGuid(), + ShipmentId = request.ShipmentId, + DocumentType = Enum.TryParse(request.DocumentType, out var dt) ? dt : DocumentType.ServiceOrder, + FileUrl = request.FileUrl, + GeneratedBy = request.GeneratedBy, + GeneratedAt = DateTime.UtcNow, + ExpiresAt = request.ExpiresAt, + CreatedAt = DateTime.UtcNow + }; + + _context.ShipmentDocuments.Add(item); + await _context.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.ShipmentDocuments.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Documento no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static ShipmentDocumentResponse MapToResponse(ShipmentDocument x) => new( + x.Id, x.ShipmentId, x.DocumentType.ToString(), + x.FileUrl, x.GeneratedBy, x.GeneratedAt, x.ExpiresAt, x.CreatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/ShipmentItemsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentItemsController.cs new file mode 100644 index 0000000..027166a --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/ShipmentItemsController.cs @@ -0,0 +1,136 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para items de envío. +/// +[ApiController] +[Route("api/shipment-items")] +[Authorize] +public class ShipmentItemsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public ShipmentItemsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.ShipmentItems + .Include(x => x.Product) + .Where(x => !x.IsDeleted) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.ShipmentItems + .Include(x => x.Product) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Item no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-shipment/{shipmentId:guid}")] + public async Task>> ByShipment(Guid shipmentId) + { + var items = await _context.ShipmentItems + .Include(x => x.Product) + .Where(x => !x.IsDeleted && x.ShipmentId == shipmentId) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateShipmentItemRequest request) + { + var item = new ShipmentItem + { + Id = Guid.NewGuid(), + ShipmentId = request.ShipmentId, + ProductId = request.ProductId, + Sku = request.Sku, + Description = request.Description, + PackagingType = Enum.TryParse(request.PackagingType, out var pt) ? pt : PackagingType.Box, + Quantity = request.Quantity, + WeightKg = request.WeightKg, + WidthCm = request.WidthCm, + HeightCm = request.HeightCm, + LengthCm = request.LengthCm, + DeclaredValue = request.DeclaredValue, + IsFragile = request.IsFragile, + IsHazardous = request.IsHazardous, + RequiresRefrigeration = request.RequiresRefrigeration, + StackingInstructions = request.StackingInstructions, + CreatedAt = DateTime.UtcNow + }; + + _context.ShipmentItems.Add(item); + await _context.SaveChangesAsync(); + + item = await _context.ShipmentItems.Include(x => x.Product).FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateShipmentItemRequest request) + { + var item = await _context.ShipmentItems.Include(x => x.Product) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Item no encontrado" }); + + item.Sku = request.Sku; + item.Description = request.Description; + item.PackagingType = Enum.TryParse(request.PackagingType, out var pt) ? pt : item.PackagingType; + item.Quantity = request.Quantity; + item.WeightKg = request.WeightKg; + item.WidthCm = request.WidthCm; + item.HeightCm = request.HeightCm; + item.LengthCm = request.LengthCm; + item.DeclaredValue = request.DeclaredValue; + item.IsFragile = request.IsFragile; + item.IsHazardous = request.IsHazardous; + item.RequiresRefrigeration = request.RequiresRefrigeration; + item.StackingInstructions = request.StackingInstructions; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.ShipmentItems.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Item no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static ShipmentItemResponse MapToResponse(ShipmentItem x) => new( + x.Id, x.ShipmentId, x.ProductId, x.Product?.Name, + x.Sku, x.Description, x.PackagingType.ToString(), + x.Quantity, x.WeightKg, x.WidthCm, x.HeightCm, x.LengthCm, + x.VolumeM3, x.VolumetricWeightKg, x.DeclaredValue, + x.IsFragile, x.IsHazardous, x.RequiresRefrigeration, + x.StackingInstructions, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/ShipmentsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentsController.cs new file mode 100644 index 0000000..0cf2028 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/ShipmentsController.cs @@ -0,0 +1,182 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de envíos. +/// +[ApiController] +[Route("api/shipments")] +[Authorize] +public class ShipmentsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public ShipmentsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Shipments + .Include(x => x.OriginLocation) + .Include(x => x.DestinationLocation) + .Include(x => x.Sender) + .Include(x => x.RecipientClient) + .Include(x => x.Truck) + .Include(x => x.Driver).ThenInclude(d => d!.Employee).ThenInclude(e => e.User) + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.CreatedAt) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await GetShipmentWithIncludes().FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Envío no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-tracking/{trackingNumber}")] + public async Task> ByTracking(string trackingNumber) + { + var item = await GetShipmentWithIncludes() + .FirstOrDefaultAsync(x => x.TrackingNumber == trackingNumber && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Envío no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-status/{status}")] + public async Task>> ByStatus(string status) + { + if (!Enum.TryParse(status, out var shipmentStatus)) + return BadRequest(new { error = "Estatus inválido" }); + + var items = await GetShipmentWithIncludes() + .Where(x => !x.IsDeleted && x.Status == shipmentStatus) + .OrderByDescending(x => x.CreatedAt) + .Take(100) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateShipmentRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var trackingNumber = GenerateTrackingNumber(); + var item = new Domain.Entities.Shipment + { + Id = Guid.NewGuid(), + TenantId = tenantId, + TrackingNumber = trackingNumber, + QrCodeData = $"PAR:{trackingNumber}", + OriginLocationId = request.OriginLocationId, + DestinationLocationId = request.DestinationLocationId, + SenderId = request.SenderId, + RecipientClientId = request.RecipientClientId, + RecipientName = request.RecipientName, + RecipientPhone = request.RecipientPhone, + TotalWeightKg = request.TotalWeightKg, + TotalVolumeM3 = request.TotalVolumeM3, + DeclaredValue = request.DeclaredValue, + SatMerchandiseCode = request.SatMerchandiseCode, + DeliveryInstructions = request.DeliveryInstructions, + Priority = Enum.TryParse(request.Priority, out var p) ? p : ShipmentPriority.Normal, + Status = ShipmentStatus.PendingApproval, + CreatedAt = DateTime.UtcNow + }; + + _context.Shipments.Add(item); + await _context.SaveChangesAsync(); + + item = await GetShipmentWithIncludes().FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateShipmentRequest request) + { + var item = await GetShipmentWithIncludes().FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Envío no encontrado" }); + + item.AssignedRouteId = request.AssignedRouteId; + item.CurrentStepOrder = request.CurrentStepOrder; + item.DeliveryInstructions = request.DeliveryInstructions; + item.Priority = Enum.TryParse(request.Priority, out var p) ? p : item.Priority; + item.Status = Enum.TryParse(request.Status, out var s) ? s : item.Status; + item.TruckId = request.TruckId; + item.DriverId = request.DriverId; + item.WasQrScanned = request.WasQrScanned; + item.IsDelayed = request.IsDelayed; + item.ScheduledDeparture = request.ScheduledDeparture; + item.PickupWindowStart = request.PickupWindowStart; + item.PickupWindowEnd = request.PickupWindowEnd; + item.EstimatedArrival = request.EstimatedArrival; + item.AssignedAt = request.AssignedAt; + item.DeliveredAt = request.DeliveredAt; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Shipments.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Envío no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private IQueryable GetShipmentWithIncludes() => _context.Shipments + .Include(x => x.OriginLocation) + .Include(x => x.DestinationLocation) + .Include(x => x.Sender) + .Include(x => x.RecipientClient) + .Include(x => x.Truck) + .Include(x => x.Driver).ThenInclude(d => d!.Employee).ThenInclude(e => e.User); + + private static string GenerateTrackingNumber() + { + var random = new Random(); + return $"PAR-{random.Next(100000, 999999)}"; + } + + private static ShipmentResponse MapToResponse(Domain.Entities.Shipment x) => new( + x.Id, x.TrackingNumber, x.QrCodeData, + x.OriginLocationId, x.OriginLocation?.Name ?? "", + x.DestinationLocationId, x.DestinationLocation?.Name ?? "", + x.SenderId, x.Sender?.CompanyName, + x.RecipientClientId, x.RecipientClient?.CompanyName, + x.RecipientName, x.RecipientPhone, + x.TotalWeightKg, x.TotalVolumeM3, x.DeclaredValue, + x.SatMerchandiseCode, x.DeliveryInstructions, + x.Priority.ToString(), x.Status.ToString(), + x.TruckId, x.Truck?.Plate, + x.DriverId, x.Driver?.Employee?.User?.FullName, + x.WasQrScanned, x.IsDelayed, + x.ScheduledDeparture, x.EstimatedArrival, x.DeliveredAt, + x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/TenantsController.cs b/backend/src/Parhelion.API/Controllers/TenantsController.cs new file mode 100644 index 0000000..1b7bd07 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/TenantsController.cs @@ -0,0 +1,158 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de Tenants (empresas clientes del sistema). +/// Solo accesible por Super Admins. +/// +[ApiController] +[Route("api/tenants")] +[Authorize] +public class TenantsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public TenantsController(ParhelionDbContext context) + { + _context = context; + } + + /// + /// Obtiene todos los tenants (solo Super Admin). + /// + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Tenants + .IgnoreQueryFilters() + .Where(x => !x.IsDeleted) + .OrderBy(x => x.CompanyName) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Obtiene el tenant actual del usuario logueado. + /// + [HttpGet("current")] + public async Task> GetCurrent() + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + { + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + } + + var tenant = await _context.Tenants + .IgnoreQueryFilters() + .FirstOrDefaultAsync(x => x.Id == tenantId && !x.IsDeleted); + + if (tenant == null) + return NotFound(new { error = "Tenant no encontrado" }); + + return Ok(MapToResponse(tenant)); + } + + /// + /// Obtiene un tenant por ID. + /// + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Tenants + .IgnoreQueryFilters() + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Tenant no encontrado" }); + + return Ok(MapToResponse(item)); + } + + /// + /// Crea un nuevo tenant. + /// + [HttpPost] + public async Task> Create([FromBody] CreateTenantRequest request) + { + var existing = await _context.Tenants + .IgnoreQueryFilters() + .AnyAsync(x => x.ContactEmail == request.ContactEmail && !x.IsDeleted); + + if (existing) + return Conflict(new { error = "Ya existe un tenant con ese email" }); + + var item = new Tenant + { + Id = Guid.NewGuid(), + CompanyName = request.CompanyName, + ContactEmail = request.ContactEmail, + FleetSize = request.FleetSize, + DriverCount = request.DriverCount, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.Tenants.Add(item); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + /// + /// Actualiza un tenant existente. + /// + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateTenantRequest request) + { + var item = await _context.Tenants + .IgnoreQueryFilters() + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Tenant no encontrado" }); + + item.CompanyName = request.CompanyName; + item.ContactEmail = request.ContactEmail; + item.FleetSize = request.FleetSize; + item.DriverCount = request.DriverCount; + item.IsActive = request.IsActive; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + /// + /// Elimina (soft-delete) un tenant. + /// + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Tenants + .IgnoreQueryFilters() + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Tenant no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return NoContent(); + } + + private static TenantResponse MapToResponse(Tenant x) => new( + x.Id, x.CompanyName, x.ContactEmail, x.FleetSize, x.DriverCount, + x.IsActive, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/TrucksController.cs b/backend/src/Parhelion.API/Controllers/TrucksController.cs new file mode 100644 index 0000000..fecdb58 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/TrucksController.cs @@ -0,0 +1,153 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de camiones. +/// +[ApiController] +[Route("api/trucks")] +[Authorize] +public class TrucksController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public TrucksController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Trucks + .Where(x => !x.IsDeleted) + .OrderBy(x => x.Plate) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Trucks.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Camión no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("available")] + public async Task>> Available() + { + var items = await _context.Trucks + .Where(x => !x.IsDeleted && x.IsActive) + .OrderBy(x => x.Plate) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("by-type/{type}")] + public async Task>> ByType(string type) + { + if (!Enum.TryParse(type, out var truckType)) + return BadRequest(new { error = "Tipo de camión inválido" }); + + var items = await _context.Trucks + .Where(x => !x.IsDeleted && x.Type == truckType) + .OrderBy(x => x.Plate) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateTruckRequest request) + { + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var existing = await _context.Trucks.AnyAsync(x => x.Plate == request.Plate && !x.IsDeleted); + if (existing) return Conflict(new { error = $"Ya existe camión con placa '{request.Plate}'" }); + + var item = new Truck + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Plate = request.Plate, + Model = request.Model, + Type = Enum.TryParse(request.Type, out var t) ? t : TruckType.DryBox, + MaxCapacityKg = request.MaxCapacityKg, + MaxVolumeM3 = request.MaxVolumeM3, + IsActive = true, + Vin = request.Vin, + EngineNumber = request.EngineNumber, + Year = request.Year, + Color = request.Color, + InsurancePolicy = request.InsurancePolicy, + InsuranceExpiration = request.InsuranceExpiration, + VerificationNumber = request.VerificationNumber, + VerificationExpiration = request.VerificationExpiration, + CreatedAt = DateTime.UtcNow + }; + + _context.Trucks.Add(item); + await _context.SaveChangesAsync(); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateTruckRequest request) + { + var item = await _context.Trucks.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Camión no encontrado" }); + + item.Plate = request.Plate; + item.Model = request.Model; + item.Type = Enum.TryParse(request.Type, out var t) ? t : item.Type; + item.MaxCapacityKg = request.MaxCapacityKg; + item.MaxVolumeM3 = request.MaxVolumeM3; + item.IsActive = request.IsActive; + item.Vin = request.Vin; + item.EngineNumber = request.EngineNumber; + item.Year = request.Year; + item.Color = request.Color; + item.InsurancePolicy = request.InsurancePolicy; + item.InsuranceExpiration = request.InsuranceExpiration; + item.VerificationNumber = request.VerificationNumber; + item.VerificationExpiration = request.VerificationExpiration; + item.LastMaintenanceDate = request.LastMaintenanceDate; + item.NextMaintenanceDate = request.NextMaintenanceDate; + item.CurrentOdometerKm = request.CurrentOdometerKm; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Trucks.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Camión no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static TruckResponse MapToResponse(Truck x) => new( + x.Id, x.Plate, x.Model, x.Type.ToString(), x.MaxCapacityKg, x.MaxVolumeM3, x.IsActive, + x.Vin, x.EngineNumber, x.Year, x.Color, x.InsurancePolicy, x.InsuranceExpiration, + x.VerificationNumber, x.VerificationExpiration, x.LastMaintenanceDate, + x.NextMaintenanceDate, x.CurrentOdometerKm, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/UsersController.cs b/backend/src/Parhelion.API/Controllers/UsersController.cs new file mode 100644 index 0000000..9d97c02 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/UsersController.cs @@ -0,0 +1,169 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.Auth; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para gestión de usuarios. +/// +[ApiController] +[Route("api/users")] +[Authorize] +public class UsersController : ControllerBase +{ + private readonly ParhelionDbContext _context; + private readonly IPasswordHasher _passwordHasher; + + public UsersController(ParhelionDbContext context, IPasswordHasher passwordHasher) + { + _context = context; + _passwordHasher = passwordHasher; + } + + /// + /// Obtiene todos los usuarios del tenant. + /// + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.Users + .Include(x => x.Role) + .Where(x => !x.IsDeleted) + .OrderBy(x => x.FullName) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Obtiene un usuario por ID. + /// + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.Users + .Include(x => x.Role) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Usuario no encontrado" }); + + return Ok(MapToResponse(item)); + } + + /// + /// Busca usuarios por nombre o email. + /// + [HttpGet("search")] + public async Task>> Search([FromQuery] string q) + { + if (string.IsNullOrWhiteSpace(q)) + return BadRequest(new { error = "El parámetro 'q' es requerido" }); + + var query = q.ToLower(); + var items = await _context.Users + .Include(x => x.Role) + .Where(x => !x.IsDeleted && + (x.FullName.ToLower().Contains(query) || x.Email.ToLower().Contains(query))) + .OrderBy(x => x.FullName) + .Select(x => MapToResponse(x)) + .ToListAsync(); + + return Ok(items); + } + + /// + /// Crea un nuevo usuario. + /// + [HttpPost] + public async Task> Create([FromBody] CreateUserRequest request) + { + var existingEmail = await _context.Users + .AnyAsync(x => x.Email == request.Email && !x.IsDeleted); + + if (existingEmail) + return Conflict(new { error = "Ya existe un usuario con ese email" }); + + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var item = new User + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Email = request.Email, + PasswordHash = _passwordHasher.HashPassword(request.Password), + FullName = request.FullName, + RoleId = request.RoleId, + IsDemoUser = request.IsDemoUser, + UsesArgon2 = false, + IsSuperAdmin = false, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.Users.Add(item); + await _context.SaveChangesAsync(); + + // Reload with Role + item = await _context.Users.Include(x => x.Role).FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + /// + /// Actualiza un usuario existente. + /// + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateUserRequest request) + { + var item = await _context.Users + .Include(x => x.Role) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Usuario no encontrado" }); + + item.FullName = request.FullName; + item.RoleId = request.RoleId; + item.IsActive = request.IsActive; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + // Reload with updated Role + item = await _context.Users.Include(x => x.Role).FirstAsync(x => x.Id == item.Id); + return Ok(MapToResponse(item)); + } + + /// + /// Elimina (soft-delete) un usuario. + /// + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.Users + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + + if (item == null) + return NotFound(new { error = "Usuario no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return NoContent(); + } + + private static UserResponse MapToResponse(User x) => new( + x.Id, x.Email, x.FullName, x.RoleId, x.Role?.Name ?? "", + x.IsDemoUser, x.IsSuperAdmin, x.LastLogin, x.IsActive, + x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/WarehouseOperatorsController.cs b/backend/src/Parhelion.API/Controllers/WarehouseOperatorsController.cs new file mode 100644 index 0000000..885d70b --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/WarehouseOperatorsController.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para operadores de almacén. +/// +[ApiController] +[Route("api/warehouse-operators")] +[Authorize] +public class WarehouseOperatorsController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public WarehouseOperatorsController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.WarehouseOperators + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.AssignedLocation) + .Include(x => x.PrimaryZone) + .Where(x => !x.IsDeleted) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.WarehouseOperators + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.AssignedLocation) + .Include(x => x.PrimaryZone) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Operador no encontrado" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-location/{locationId:guid}")] + public async Task>> ByLocation(Guid locationId) + { + var items = await _context.WarehouseOperators + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.AssignedLocation) + .Include(x => x.PrimaryZone) + .Where(x => !x.IsDeleted && x.AssignedLocationId == locationId) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateWarehouseOperatorRequest request) + { + var item = new WarehouseOperator + { + Id = Guid.NewGuid(), + EmployeeId = request.EmployeeId, + AssignedLocationId = request.AssignedLocationId, + PrimaryZoneId = request.PrimaryZoneId, + CreatedAt = DateTime.UtcNow + }; + + _context.WarehouseOperators.Add(item); + await _context.SaveChangesAsync(); + + item = await _context.WarehouseOperators + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.AssignedLocation) + .Include(x => x.PrimaryZone) + .FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateWarehouseOperatorRequest request) + { + var item = await _context.WarehouseOperators + .Include(x => x.Employee).ThenInclude(e => e.User) + .Include(x => x.AssignedLocation) + .Include(x => x.PrimaryZone) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Operador no encontrado" }); + + item.AssignedLocationId = request.AssignedLocationId; + item.PrimaryZoneId = request.PrimaryZoneId; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.WarehouseOperators.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Operador no encontrado" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static WarehouseOperatorResponse MapToResponse(WarehouseOperator x) => new( + x.Id, x.EmployeeId, x.Employee?.User?.FullName ?? "", + x.AssignedLocationId, x.AssignedLocation?.Name ?? "", + x.PrimaryZoneId, x.PrimaryZone?.Name, + x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Controllers/WarehouseZonesController.cs b/backend/src/Parhelion.API/Controllers/WarehouseZonesController.cs new file mode 100644 index 0000000..b5438c1 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/WarehouseZonesController.cs @@ -0,0 +1,113 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para zonas de bodega. +/// +[ApiController] +[Route("api/warehouse-zones")] +[Authorize] +public class WarehouseZonesController : ControllerBase +{ + private readonly ParhelionDbContext _context; + + public WarehouseZonesController(ParhelionDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task>> GetAll() + { + var items = await _context.WarehouseZones + .Include(x => x.Location) + .Where(x => !x.IsDeleted) + .OrderBy(x => x.Code) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _context.WarehouseZones + .Include(x => x.Location) + .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Zona no encontrada" }); + return Ok(MapToResponse(item)); + } + + [HttpGet("by-location/{locationId:guid}")] + public async Task>> ByLocation(Guid locationId) + { + var items = await _context.WarehouseZones + .Include(x => x.Location) + .Where(x => !x.IsDeleted && x.LocationId == locationId) + .OrderBy(x => x.Code) + .Select(x => MapToResponse(x)) + .ToListAsync(); + return Ok(items); + } + + [HttpPost] + public async Task> Create([FromBody] CreateWarehouseZoneRequest request) + { + var item = new WarehouseZone + { + Id = Guid.NewGuid(), + LocationId = request.LocationId, + Code = request.Code, + Name = request.Name, + Type = Enum.TryParse(request.Type, out var t) ? t : WarehouseZoneType.Storage, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + _context.WarehouseZones.Add(item); + await _context.SaveChangesAsync(); + + item = await _context.WarehouseZones.Include(x => x.Location).FirstAsync(x => x.Id == item.Id); + return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + } + + [HttpPut("{id:guid}")] + public async Task> Update(Guid id, [FromBody] UpdateWarehouseZoneRequest request) + { + var item = await _context.WarehouseZones.Include(x => x.Location).FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Zona no encontrada" }); + + item.Code = request.Code; + item.Name = request.Name; + item.Type = Enum.TryParse(request.Type, out var t) ? t : item.Type; + item.IsActive = request.IsActive; + item.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return Ok(MapToResponse(item)); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var item = await _context.WarehouseZones.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (item == null) return NotFound(new { error = "Zona no encontrada" }); + + item.IsDeleted = true; + item.DeletedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return NoContent(); + } + + private static WarehouseZoneResponse MapToResponse(WarehouseZone x) => new( + x.Id, x.LocationId, x.Location?.Name ?? "", x.Code, x.Name, + x.Type.ToString(), x.IsActive, x.CreatedAt, x.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.API/Data/DataSeeder.cs b/backend/src/Parhelion.API/Data/DataSeeder.cs new file mode 100644 index 0000000..9b07c68 --- /dev/null +++ b/backend/src/Parhelion.API/Data/DataSeeder.cs @@ -0,0 +1,90 @@ +using Parhelion.Domain.Entities; +using Parhelion.Application.Auth; +using Parhelion.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; + +namespace Parhelion.API.Data; + +/// +/// Seed inicial del sistema con SuperUser y DefaultTenant. +/// Se ejecuta solo si no existen datos en la base de datos. +/// +public static class DataSeeder +{ + /// + /// Aplica el seeder si la base de datos está vacía. + /// + public static async Task SeedAsync(IServiceProvider services) + { + using var scope = services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var passwordHasher = scope.ServiceProvider.GetRequiredService(); + var config = scope.ServiceProvider.GetRequiredService(); + + // Skip si ya hay datos + if (await context.Tenants.AnyAsync()) + { + return; + } + + // Datos del SuperUser desde config o defaults seguros + var superEmail = config["Seed:SuperUserEmail"] ?? "metacodex@parhelion.com"; + var superPassword = config["Seed:SuperUserPassword"]; + + if (string.IsNullOrEmpty(superPassword)) + { + Console.WriteLine("⚠️ SEED: No SuperUserPassword configured. Skipping seeder."); + Console.WriteLine(" Set Seed:SuperUserPassword in .env or appsettings to enable seeding."); + return; + } + + Console.WriteLine("🌱 Seeding database with initial data..."); + + // 1. Crear DefaultTenant + var defaultTenant = new Tenant + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000001"), + CompanyName = "Parhelion Logistics", + ContactEmail = "admin@parhelion.com", + FleetSize = 0, + DriverCount = 0, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.Tenants.Add(defaultTenant); + + // 2. Crear Role SuperAdmin + var superAdminRole = new Role + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000001"), + Name = "SuperAdmin", + Description = "Super Administrator with full system access", + CreatedAt = DateTime.UtcNow + }; + context.Roles.Add(superAdminRole); + + // 3. Crear SuperUser con password hasheado (work factor 14 para admin) + var superUser = new User + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000001"), + TenantId = defaultTenant.Id, + Email = superEmail, + PasswordHash = passwordHasher.HashPassword(superPassword, useArgon2: true), + FullName = "MetaCodeX SuperAdmin", + RoleId = superAdminRole.Id, + IsActive = true, + UsesArgon2 = true, // Indica que usa el hash más fuerte + CreatedAt = DateTime.UtcNow + }; + context.Users.Add(superUser); + + await context.SaveChangesAsync(); + + Console.WriteLine("✅ SEED: SuperUser created successfully"); + Console.WriteLine($" Email: {superEmail}"); + Console.WriteLine($" Role: SuperAdmin"); + Console.WriteLine($" Tenant: Parhelion Logistics"); + } +} diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index daa5afc..7dcc9fb 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -147,5 +147,7 @@ .WithName("DatabaseHealthCheck") .WithOpenApi(); -app.Run(); +// Seed initial data (SuperUser, DefaultTenant) if database is empty +await Parhelion.API.Data.DataSeeder.SeedAsync(app.Services); +app.Run(); diff --git a/backend/src/Parhelion.Application/DTOs/Catalog/CatalogItemDtos.cs b/backend/src/Parhelion.Application/DTOs/Catalog/CatalogItemDtos.cs new file mode 100644 index 0000000..c99f587 --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Catalog/CatalogItemDtos.cs @@ -0,0 +1,58 @@ +namespace Parhelion.Application.DTOs.Catalog; + +/// +/// Request para crear un nuevo CatalogItem. +/// +public record CreateCatalogItemRequest( + string Sku, + string Name, + string? Description, + string BaseUom, + decimal DefaultWeightKg, + decimal DefaultWidthCm, + decimal DefaultHeightCm, + decimal DefaultLengthCm, + bool RequiresRefrigeration, + bool IsHazardous, + bool IsFragile +); + +/// +/// Request para actualizar un CatalogItem existente. +/// +public record UpdateCatalogItemRequest( + string Name, + string? Description, + string BaseUom, + decimal DefaultWeightKg, + decimal DefaultWidthCm, + decimal DefaultHeightCm, + decimal DefaultLengthCm, + bool RequiresRefrigeration, + bool IsHazardous, + bool IsFragile, + bool IsActive +); + +/// +/// Response DTO para CatalogItem. +/// Incluye todos los campos relevantes para el cliente. +/// +public record CatalogItemResponse( + Guid Id, + string Sku, + string Name, + string? Description, + string BaseUom, + decimal DefaultWeightKg, + decimal DefaultWidthCm, + decimal DefaultHeightCm, + decimal DefaultLengthCm, + decimal DefaultVolumeM3, + bool RequiresRefrigeration, + bool IsHazardous, + bool IsFragile, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt +); diff --git a/backend/src/Parhelion.Application/DTOs/Core/CoreDtos.cs b/backend/src/Parhelion.Application/DTOs/Core/CoreDtos.cs new file mode 100644 index 0000000..49718de --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Core/CoreDtos.cs @@ -0,0 +1,175 @@ +namespace Parhelion.Application.DTOs.Core; + +// ========== TENANT DTOs ========== + +public record CreateTenantRequest( + string CompanyName, + string ContactEmail, + int FleetSize, + int DriverCount +); + +public record UpdateTenantRequest( + string CompanyName, + string ContactEmail, + int FleetSize, + int DriverCount, + bool IsActive +); + +public record TenantResponse( + Guid Id, + string CompanyName, + string ContactEmail, + int FleetSize, + int DriverCount, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== USER DTOs ========== + +public record CreateUserRequest( + string Email, + string Password, + string FullName, + Guid RoleId, + bool IsDemoUser = false +); + +public record UpdateUserRequest( + string FullName, + Guid RoleId, + bool IsActive +); + +public record UserResponse( + Guid Id, + string Email, + string FullName, + Guid RoleId, + string RoleName, + bool IsDemoUser, + bool IsSuperAdmin, + DateTime? LastLogin, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== ROLE DTOs ========== + +public record CreateRoleRequest( + string Name, + string? Description +); + +public record UpdateRoleRequest( + string Name, + string? Description +); + +public record RoleResponse( + Guid Id, + string Name, + string? Description, + DateTime CreatedAt +); + +// ========== EMPLOYEE DTOs ========== + +public record CreateEmployeeRequest( + Guid UserId, + string Phone, + string? Rfc, + string? Nss, + string? Curp, + string? EmergencyContact, + string? EmergencyPhone, + DateTime? HireDate, + Guid? ShiftId, + string? Department +); + +public record UpdateEmployeeRequest( + string Phone, + string? Rfc, + string? Nss, + string? Curp, + string? EmergencyContact, + string? EmergencyPhone, + DateTime? HireDate, + Guid? ShiftId, + string? Department +); + +public record EmployeeResponse( + Guid Id, + Guid UserId, + string UserFullName, + string UserEmail, + string Phone, + string? Rfc, + string? Nss, + string? Curp, + string? EmergencyContact, + string? EmergencyPhone, + DateTime? HireDate, + Guid? ShiftId, + string? Department, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== CLIENT DTOs ========== + +public record CreateClientRequest( + string CompanyName, + string? TradeName, + string ContactName, + string Email, + string Phone, + string? TaxId, + string? LegalName, + string? BillingAddress, + string ShippingAddress, + string? PreferredProductTypes, + string Priority, + string? Notes +); + +public record UpdateClientRequest( + string CompanyName, + string? TradeName, + string ContactName, + string Email, + string Phone, + string? TaxId, + string? LegalName, + string? BillingAddress, + string ShippingAddress, + string? PreferredProductTypes, + string Priority, + bool IsActive, + string? Notes +); + +public record ClientResponse( + Guid Id, + string CompanyName, + string? TradeName, + string ContactName, + string Email, + string Phone, + string? TaxId, + string? LegalName, + string? BillingAddress, + string ShippingAddress, + string? PreferredProductTypes, + string Priority, + bool IsActive, + string? Notes, + DateTime CreatedAt, + DateTime? UpdatedAt +); diff --git a/backend/src/Parhelion.Application/DTOs/Fleet/FleetDtos.cs b/backend/src/Parhelion.Application/DTOs/Fleet/FleetDtos.cs new file mode 100644 index 0000000..01bfbab --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Fleet/FleetDtos.cs @@ -0,0 +1,150 @@ +namespace Parhelion.Application.DTOs.Fleet; + +// ========== TRUCK DTOs ========== + +public record CreateTruckRequest( + string Plate, + string Model, + string Type, + decimal MaxCapacityKg, + decimal MaxVolumeM3, + string? Vin, + string? EngineNumber, + int? Year, + string? Color, + string? InsurancePolicy, + DateTime? InsuranceExpiration, + string? VerificationNumber, + DateTime? VerificationExpiration +); + +public record UpdateTruckRequest( + string Plate, + string Model, + string Type, + decimal MaxCapacityKg, + decimal MaxVolumeM3, + bool IsActive, + string? Vin, + string? EngineNumber, + int? Year, + string? Color, + string? InsurancePolicy, + DateTime? InsuranceExpiration, + string? VerificationNumber, + DateTime? VerificationExpiration, + DateTime? LastMaintenanceDate, + DateTime? NextMaintenanceDate, + decimal? CurrentOdometerKm +); + +public record TruckResponse( + Guid Id, + string Plate, + string Model, + string Type, + decimal MaxCapacityKg, + decimal MaxVolumeM3, + bool IsActive, + string? Vin, + string? EngineNumber, + int? Year, + string? Color, + string? InsurancePolicy, + DateTime? InsuranceExpiration, + string? VerificationNumber, + DateTime? VerificationExpiration, + DateTime? LastMaintenanceDate, + DateTime? NextMaintenanceDate, + decimal? CurrentOdometerKm, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== DRIVER DTOs ========== + +public record CreateDriverRequest( + Guid EmployeeId, + string LicenseNumber, + string? LicenseType, + DateTime? LicenseExpiration, + Guid? DefaultTruckId, + string Status +); + +public record UpdateDriverRequest( + string LicenseNumber, + string? LicenseType, + DateTime? LicenseExpiration, + Guid? DefaultTruckId, + Guid? CurrentTruckId, + string Status +); + +public record DriverResponse( + Guid Id, + Guid EmployeeId, + string EmployeeName, + string LicenseNumber, + string? LicenseType, + DateTime? LicenseExpiration, + Guid? DefaultTruckId, + string? DefaultTruckPlate, + Guid? CurrentTruckId, + string? CurrentTruckPlate, + string Status, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== SHIFT DTOs ========== + +public record CreateShiftRequest( + string Name, + TimeOnly StartTime, + TimeOnly EndTime, + string DaysOfWeek +); + +public record UpdateShiftRequest( + string Name, + TimeOnly StartTime, + TimeOnly EndTime, + string DaysOfWeek, + bool IsActive +); + +public record ShiftResponse( + Guid Id, + string Name, + TimeOnly StartTime, + TimeOnly EndTime, + string DaysOfWeek, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== FLEET LOG DTOs ========== + +public record CreateFleetLogRequest( + Guid DriverId, + Guid? OldTruckId, + Guid NewTruckId, + string Reason +); + +public record FleetLogResponse( + Guid Id, + Guid DriverId, + string DriverName, + Guid? OldTruckId, + string? OldTruckPlate, + Guid NewTruckId, + string NewTruckPlate, + string Reason, + DateTime Timestamp, + Guid CreatedByUserId, + string CreatedByName, + DateTime CreatedAt +); diff --git a/backend/src/Parhelion.Application/DTOs/Network/NetworkDtos.cs b/backend/src/Parhelion.Application/DTOs/Network/NetworkDtos.cs new file mode 100644 index 0000000..7addfb6 --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Network/NetworkDtos.cs @@ -0,0 +1,88 @@ +namespace Parhelion.Application.DTOs.Network; + +// ========== NETWORK LINK DTOs ========== + +public record CreateNetworkLinkRequest( + Guid OriginLocationId, + Guid DestinationLocationId, + string LinkType, + TimeSpan TransitTime, + bool IsBidirectional +); + +public record UpdateNetworkLinkRequest( + string LinkType, + TimeSpan TransitTime, + bool IsBidirectional, + bool IsActive +); + +public record NetworkLinkResponse( + Guid Id, + Guid OriginLocationId, + string OriginLocationName, + Guid DestinationLocationId, + string DestinationLocationName, + string LinkType, + TimeSpan TransitTime, + bool IsBidirectional, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== ROUTE BLUEPRINT DTOs ========== + +public record CreateRouteBlueprintRequest( + string Name, + string? Description +); + +public record UpdateRouteBlueprintRequest( + string Name, + string? Description, + int TotalSteps, + TimeSpan TotalTransitTime, + bool IsActive +); + +public record RouteBlueprintResponse( + Guid Id, + string Name, + string? Description, + int TotalSteps, + TimeSpan TotalTransitTime, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== ROUTE STEP DTOs ========== + +public record CreateRouteStepRequest( + Guid RouteBlueprintId, + Guid LocationId, + int StepOrder, + TimeSpan StandardTransitTime, + string StepType +); + +public record UpdateRouteStepRequest( + Guid LocationId, + int StepOrder, + TimeSpan StandardTransitTime, + string StepType +); + +public record RouteStepResponse( + Guid Id, + Guid RouteBlueprintId, + string RouteName, + Guid LocationId, + string LocationName, + int StepOrder, + TimeSpan StandardTransitTime, + string StepType, + DateTime CreatedAt, + DateTime? UpdatedAt +); diff --git a/backend/src/Parhelion.Application/DTOs/Shipment/ShipmentDtos.cs b/backend/src/Parhelion.Application/DTOs/Shipment/ShipmentDtos.cs new file mode 100644 index 0000000..6167024 --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Shipment/ShipmentDtos.cs @@ -0,0 +1,190 @@ +namespace Parhelion.Application.DTOs.Shipment; + +// ========== SHIPMENT DTOs ========== + +public record CreateShipmentRequest( + Guid OriginLocationId, + Guid DestinationLocationId, + Guid? SenderId, + Guid? RecipientClientId, + string RecipientName, + string? RecipientPhone, + decimal TotalWeightKg, + decimal TotalVolumeM3, + decimal? DeclaredValue, + string? SatMerchandiseCode, + string? DeliveryInstructions, + string Priority +); + +public record UpdateShipmentRequest( + Guid? AssignedRouteId, + int? CurrentStepOrder, + string? DeliveryInstructions, + string Priority, + string Status, + Guid? TruckId, + Guid? DriverId, + bool WasQrScanned, + bool IsDelayed, + DateTime? ScheduledDeparture, + DateTime? PickupWindowStart, + DateTime? PickupWindowEnd, + DateTime? EstimatedArrival, + DateTime? AssignedAt, + DateTime? DeliveredAt +); + +public record ShipmentResponse( + Guid Id, + string TrackingNumber, + string QrCodeData, + Guid OriginLocationId, + string OriginLocationName, + Guid DestinationLocationId, + string DestinationLocationName, + Guid? SenderId, + string? SenderName, + Guid? RecipientClientId, + string? RecipientClientName, + string RecipientName, + string? RecipientPhone, + decimal TotalWeightKg, + decimal TotalVolumeM3, + decimal? DeclaredValue, + string? SatMerchandiseCode, + string? DeliveryInstructions, + string Priority, + string Status, + Guid? TruckId, + string? TruckPlate, + Guid? DriverId, + string? DriverName, + bool WasQrScanned, + bool IsDelayed, + DateTime? ScheduledDeparture, + DateTime? EstimatedArrival, + DateTime? DeliveredAt, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== SHIPMENT ITEM DTOs ========== + +public record CreateShipmentItemRequest( + Guid ShipmentId, + Guid? ProductId, + string? Sku, + string Description, + string PackagingType, + int Quantity, + decimal WeightKg, + decimal WidthCm, + decimal HeightCm, + decimal LengthCm, + decimal DeclaredValue, + bool IsFragile, + bool IsHazardous, + bool RequiresRefrigeration, + string? StackingInstructions +); + +public record UpdateShipmentItemRequest( + string? Sku, + string Description, + string PackagingType, + int Quantity, + decimal WeightKg, + decimal WidthCm, + decimal HeightCm, + decimal LengthCm, + decimal DeclaredValue, + bool IsFragile, + bool IsHazardous, + bool RequiresRefrigeration, + string? StackingInstructions +); + +public record ShipmentItemResponse( + Guid Id, + Guid ShipmentId, + Guid? ProductId, + string? ProductName, + string? Sku, + string Description, + string PackagingType, + int Quantity, + decimal WeightKg, + decimal WidthCm, + decimal HeightCm, + decimal LengthCm, + decimal VolumeM3, + decimal VolumetricWeightKg, + decimal DeclaredValue, + bool IsFragile, + bool IsHazardous, + bool RequiresRefrigeration, + string? StackingInstructions, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== SHIPMENT CHECKPOINT DTOs ========== + +public record CreateShipmentCheckpointRequest( + Guid ShipmentId, + Guid? LocationId, + string StatusCode, + string? Remarks, + Guid? HandledByDriverId, + Guid? LoadedOntoTruckId, + string? ActionType, + string? PreviousCustodian, + string? NewCustodian, + Guid? HandledByWarehouseOperatorId, + decimal? Latitude, + decimal? Longitude +); + +public record ShipmentCheckpointResponse( + Guid Id, + Guid ShipmentId, + Guid? LocationId, + string? LocationName, + string StatusCode, + string? Remarks, + DateTime Timestamp, + Guid CreatedByUserId, + string CreatedByName, + Guid? HandledByDriverId, + string? DriverName, + Guid? LoadedOntoTruckId, + string? TruckPlate, + string? ActionType, + string? PreviousCustodian, + string? NewCustodian, + decimal? Latitude, + decimal? Longitude, + DateTime CreatedAt +); + +// ========== SHIPMENT DOCUMENT DTOs ========== + +public record CreateShipmentDocumentRequest( + Guid ShipmentId, + string DocumentType, + string FileUrl, + string GeneratedBy, + DateTime? ExpiresAt +); + +public record ShipmentDocumentResponse( + Guid Id, + Guid ShipmentId, + string DocumentType, + string FileUrl, + string GeneratedBy, + DateTime GeneratedAt, + DateTime? ExpiresAt, + DateTime CreatedAt +); diff --git a/backend/src/Parhelion.Application/DTOs/Warehouse/WarehouseDtos.cs b/backend/src/Parhelion.Application/DTOs/Warehouse/WarehouseDtos.cs new file mode 100644 index 0000000..8292c5c --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Warehouse/WarehouseDtos.cs @@ -0,0 +1,168 @@ +namespace Parhelion.Application.DTOs.Warehouse; + +// ========== LOCATION DTOs ========== + +public record CreateLocationRequest( + string Code, + string Name, + string Type, + string FullAddress, + decimal? Latitude, + decimal? Longitude, + bool CanReceive, + bool CanDispatch, + bool IsInternal +); + +public record UpdateLocationRequest( + string Code, + string Name, + string Type, + string FullAddress, + decimal? Latitude, + decimal? Longitude, + bool CanReceive, + bool CanDispatch, + bool IsInternal, + bool IsActive +); + +public record LocationResponse( + Guid Id, + string Code, + string Name, + string Type, + string FullAddress, + decimal? Latitude, + decimal? Longitude, + bool CanReceive, + bool CanDispatch, + bool IsInternal, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== WAREHOUSE ZONE DTOs ========== + +public record CreateWarehouseZoneRequest( + Guid LocationId, + string Code, + string Name, + string Type +); + +public record UpdateWarehouseZoneRequest( + string Code, + string Name, + string Type, + bool IsActive +); + +public record WarehouseZoneResponse( + Guid Id, + Guid LocationId, + string LocationName, + string Code, + string Name, + string Type, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== WAREHOUSE OPERATOR DTOs ========== + +public record CreateWarehouseOperatorRequest( + Guid EmployeeId, + Guid AssignedLocationId, + Guid? PrimaryZoneId +); + +public record UpdateWarehouseOperatorRequest( + Guid AssignedLocationId, + Guid? PrimaryZoneId +); + +public record WarehouseOperatorResponse( + Guid Id, + Guid EmployeeId, + string EmployeeName, + Guid AssignedLocationId, + string LocationName, + Guid? PrimaryZoneId, + string? ZoneName, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== INVENTORY STOCK DTOs ========== + +public record CreateInventoryStockRequest( + Guid ZoneId, + Guid ProductId, + decimal Quantity, + decimal QuantityReserved, + string? BatchNumber, + DateTime? ExpiryDate, + decimal? UnitCost +); + +public record UpdateInventoryStockRequest( + decimal Quantity, + decimal QuantityReserved, + string? BatchNumber, + DateTime? ExpiryDate, + DateTime? LastCountDate, + decimal? UnitCost +); + +public record InventoryStockResponse( + Guid Id, + Guid ZoneId, + string ZoneName, + Guid ProductId, + string ProductName, + string ProductSku, + decimal Quantity, + decimal QuantityReserved, + decimal QuantityAvailable, + string? BatchNumber, + DateTime? ExpiryDate, + DateTime? LastCountDate, + decimal? UnitCost, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +// ========== INVENTORY TRANSACTION DTOs ========== + +public record CreateInventoryTransactionRequest( + Guid ProductId, + Guid? OriginZoneId, + Guid? DestinationZoneId, + decimal Quantity, + string TransactionType, + Guid? ShipmentId, + string? BatchNumber, + string? Remarks +); + +public record InventoryTransactionResponse( + Guid Id, + Guid ProductId, + string ProductName, + Guid? OriginZoneId, + string? OriginZoneName, + Guid? DestinationZoneId, + string? DestinationZoneName, + decimal Quantity, + string TransactionType, + Guid PerformedByUserId, + string PerformedByName, + Guid? ShipmentId, + string? BatchNumber, + string? Remarks, + DateTime Timestamp, + DateTime CreatedAt +); diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..19506e2 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,141 @@ +# Parhelion WMS - API Reference v0.5.0 + +Documentación técnica de los endpoints REST API del sistema Parhelion Logistics. + +## Autenticación + +Todos los endpoints protegidos requieren un JWT Bearer token: + +```http +Authorization: Bearer +``` + +### Obtener Token + +```bash +POST /api/auth/login +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "password123" +} +``` + +--- + +## Endpoints por Capa + +### 🔶 Core Layer (5 endpoints) + +| Endpoint | Métodos | Descripción | +| ---------------- | ---------------------- | -------------------------------------- | +| `/api/tenants` | GET, POST, PUT, DELETE | Multi-tenant management | +| `/api/users` | GET, POST, PUT, DELETE | User accounts | +| `/api/roles` | GET, POST, PUT, DELETE | Role definitions (Admin, Driver, etc.) | +| `/api/employees` | GET, POST, PUT, DELETE | Employee profiles | +| `/api/clients` | GET, POST, PUT, DELETE | B2B clients (senders/recipients) | + +### 🏭 Warehouse Layer (5 endpoints) + +| Endpoint | Métodos | Descripción | +| ----------------------------- | ---------------------- | ----------------------------- | +| `/api/locations` | GET, POST, PUT, DELETE | Hubs, Warehouses, Cross-docks | +| `/api/warehouse-zones` | GET, POST, PUT, DELETE | Zones within locations | +| `/api/warehouse-operators` | GET, POST, PUT, DELETE | Operators assigned to zones | +| `/api/inventory-stocks` | GET, POST, PUT, DELETE | Stock by zone/lot | +| `/api/inventory-transactions` | GET, POST | Kardex movements | + +### 🚛 Fleet Layer (4 endpoints) + +| Endpoint | Métodos | Descripción | +| ----------------- | ---------------------- | -------------------------------- | +| `/api/trucks` | GET, POST, PUT, DELETE | DryBox, Refrigerated, HAZMAT | +| `/api/drivers` | GET, POST, PUT, DELETE | Fleet drivers with MX legal data | +| `/api/shifts` | GET, POST, PUT, DELETE | Work shifts configuration | +| `/api/fleet-logs` | GET, POST | Driver-Truck assignment log | + +### 📦 Shipment Layer (5 endpoints) + +| Endpoint | Métodos | Descripción | +| --------------------------- | ---------------------- | ---------------------------- | +| `/api/shipments` | GET, POST, PUT, DELETE | Shipments PAR-XXXXXX | +| `/api/shipment-items` | GET, POST, PUT, DELETE | Items with volumetric weight | +| `/api/shipment-checkpoints` | GET, POST | Immutable tracking events | +| `/api/shipment-documents` | GET, POST, DELETE | B2B docs: Waybill, POD | +| `/api/catalog-items` | GET, POST, PUT, DELETE | Product catalog | + +### 🌐 Network Layer (3 endpoints) + +| Endpoint | Métodos | Descripción | +| ----------------------- | ---------------------- | ------------------------------ | +| `/api/network-links` | GET, POST, PUT, DELETE | FirstMile, LineHaul, LastMile | +| `/api/route-blueprints` | GET, POST, PUT, DELETE | Predefined Hub & Spoke routes | +| `/api/route-steps` | GET, POST, PUT, DELETE | Route stops with transit times | + +--- + +## Health Endpoints + +```bash +GET /health # Service status +GET /health/db # Database connectivity +``` + +--- + +## Schema Metadata + +```bash +GET /api/Schema/metadata # Database schema for tooling +POST /api/Schema/refresh # Force cache refresh +``` + +--- + +## Swagger UI + +Documentación interactiva disponible en: + +``` +http://localhost:5100/swagger +``` + +> **Nota**: Swagger está configurado solo para entornos de desarrollo. En producción se deshabilita automáticamente. + +--- + +## Respuestas Estándar + +### Exitoso (200/201) + +```json +{ + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "createdAt": "2025-12-14T23:00:00Z", + ... +} +``` + +### Error de Autenticación (401) + +```json +{ + "error": "Email o contraseña incorrectos" +} +``` + +### Error de Validación (400) + +```json +{ + "errors": { + "Field": ["Mensaje de error"] + } +} +``` + +--- + +**Versión**: 0.5.0 +**Última actualización**: 2025-12-14 From 5aa1a5a0189c0f784d5e079e3b052a8a52df1ed7 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Tue, 16 Dec 2025 03:11:09 +0000 Subject: [PATCH 20/34] v0.5.1: Foundation Layer + Repository Pattern + xUnit Tests ## Added / Agregado - IGenericRepository with CRUD operations and soft delete - ITenantRepository with automatic TenantId filtering - IUnitOfWork for transaction coordination across repositories - Common DTOs: PagedRequest, PagedResult, BaseDto, OperationResult - xUnit test fixtures: InMemoryDbFixture, TestDataBuilder - Unit tests: PaginationDtoTests (11), GenericRepositoryTests (9) ## Modified / Modificado - CI/CD updated to v0.5.1 with explicit 'Run xUnit Tests' step - PostgreSQL upgraded to version 17 in Docker and CI - Swagger UI enabled in all environments for Tailscale access - README, CHANGELOG, api-architecture updated with v0.5.1 info ## Technical Notes / Notas Tecnicas - Foundation layer is prerequisite for Services layer (v0.5.2+) - Business logic tests will be implemented in phases 3-8 - Private development tools (control-panel) excluded from repository --- .agent/docs/neobrutalism-components.md | 210 ++ .github/workflows/ci.yml | 16 +- .gitignore | 2 +- CHANGELOG.md | 47 + README.md | 6 +- api-architecture.md | 63 +- backend/Dockerfile | 32 +- backend/src/Parhelion.API/Program.cs | 10 +- .../DTOs/Common/BaseDto.cs | 52 + .../DTOs/Common/PagedRequest.cs | 56 + .../DTOs/Common/PagedResult.cs | 39 + .../Interfaces/IGenericRepository.cs | 142 ++ .../Interfaces/IUnitOfWork.cs | 65 + .../Repositories/GenericRepository.cs | 225 ++ .../Repositories/UnitOfWork.cs | 180 ++ .../Fixtures/InMemoryDbFixture.cs | 197 ++ .../Unit/DTOs/PaginationDtoTests.cs | 145 ++ .../Repositories/GenericRepositoryTests.cs | 162 ++ .../parhelion_dev_backup_20251216_013659.sql | 2007 +++++++++++++++++ docker-compose.yml | 29 +- 20 files changed, 3626 insertions(+), 59 deletions(-) create mode 100644 .agent/docs/neobrutalism-components.md create mode 100644 backend/src/Parhelion.Application/DTOs/Common/BaseDto.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Common/PagedRequest.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Common/PagedResult.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/IGenericRepository.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/IUnitOfWork.cs create mode 100644 backend/src/Parhelion.Infrastructure/Repositories/GenericRepository.cs create mode 100644 backend/src/Parhelion.Infrastructure/Repositories/UnitOfWork.cs create mode 100644 backend/tests/Parhelion.Tests/Fixtures/InMemoryDbFixture.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/DTOs/PaginationDtoTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Repositories/GenericRepositoryTests.cs create mode 100644 backups/parhelion_dev_backup_20251216_013659.sql diff --git a/.agent/docs/neobrutalism-components.md b/.agent/docs/neobrutalism-components.md new file mode 100644 index 0000000..3a7b417 --- /dev/null +++ b/.agent/docs/neobrutalism-components.md @@ -0,0 +1,210 @@ +# Neobrutalism UI Components Reference + +Referencia rápida de componentes de [neobrutalism.dev](https://www.neobrutalism.dev/) para diseño en Parhelion WMS. + +## Instalación Base + +```bash +# Usar con shadcn CLI +pnpm dlx shadcn@latest add https://neobrutalism.dev/r/[component].json +``` + +--- + +## Componentes por Categoría + +### 🎨 Básicos + +| Componente | Descripción | Instalación | +| ---------- | ---------------------------------------------------------- | ------------- | +| **Button** | Botones con variantes: default, reverse, noShadow, neutral | `button.json` | +| **Badge** | Etiquetas: default, neutral, withIcon | `badge.json` | +| **Card** | Contenedores con CardHeader, CardContent, CardFooter | `card.json` | +| **Avatar** | Imágenes de perfil circulares | `avatar.json` | + +### 📝 Formularios + +| Componente | Descripción | Instalación | +| --------------- | ---------------------------- | ------------------ | +| **Input** | Campos de texto | `input.json` | +| **Checkbox** | Casillas de verificación | `checkbox.json` | +| **Switch** | Toggle on/off | `switch.json` | +| **Select** | Dropdown de opciones | `select.json` | +| **Slider** | Control deslizante | `slider.json` | +| **Radio Group** | Grupo de opciones exclusivas | `radio-group.json` | +| **Label** | Etiquetas para inputs | `label.json` | +| **Textarea** | Área de texto multilínea | `textarea.json` | + +### 📊 Datos + +| Componente | Descripción | Instalación | +| -------------- | ------------------------------------------- | ----------------- | +| **Table** | Tablas con TableHeader, TableRow, TableCell | `table.json` | +| **Data Table** | Tablas avanzadas con sorting/filtering | `data-table.json` | +| **Progress** | Barras de progreso | `progress.json` | +| **Chart** | Gráficos (basado en Recharts) | `chart.json` | + +### 🧭 Navegación + +| Componente | Descripción | Instalación | +| ------------------- | ----------------------------------------------- | ---------------------- | +| **Tabs** | Pestañas con TabsList, TabsTrigger, TabsContent | `tabs.json` | +| **Breadcrumb** | Migas de pan | `breadcrumb.json` | +| **Navigation Menu** | Menú de navegación | `navigation-menu.json` | +| **Menubar** | Barra de menú | `menubar.json` | +| **Sidebar** | Barra lateral | `sidebar.json` | +| **Pagination** | Paginación | `pagination.json` | + +### 💬 Overlays + +| Componente | Descripción | Instalación | +| ----------------- | ------------------------------- | -------------------- | +| **Dialog** | Modales | `dialog.json` | +| **Alert Dialog** | Diálogos de confirmación | `alert-dialog.json` | +| **Sheet** | Paneles deslizantes | `sheet.json` | +| **Drawer** | Cajón desde abajo (mobile) | `drawer.json` | +| **Popover** | Popups contextuales | `popover.json` | +| **Dropdown Menu** | Menús desplegables | `dropdown-menu.json` | +| **Context Menu** | Menú contextual (click derecho) | `context-menu.json` | +| **Hover Card** | Tarjeta al hover | `hover-card.json` | +| **Tooltip** | Tooltips | `tooltip.json` | + +### 🎭 Especiales + +| Componente | Descripción | Instalación | +| --------------- | ----------------------------- | ------------------ | +| **Accordion** | Secciones colapsables | `accordion.json` | +| **Collapsible** | Contenido colapsable | `collapsible.json` | +| **Carousel** | Carrusel de imágenes | `carousel.json` | +| **Marquee** | Texto en movimiento | `marquee.json` | +| **Image Card** | Tarjeta con imagen | `image-card.json` | +| **Calendar** | Selector de fecha | `calendar.json` | +| **Date Picker** | Picker de fechas | `date-picker.json` | +| **Sonner** | Notificaciones toast | `sonner.json` | +| **Alert** | Mensajes de alerta | `alert.json` | +| **Skeleton** | Placeholders de carga | `skeleton.json` | +| **Scroll Area** | Área con scroll personalizado | `scroll-area.json` | +| **Resizable** | Paneles redimensionables | `resizable.json` | +| **Command** | Command palette (⌘K) | `command.json` | +| **Combobox** | Input con autocompletado | `combobox.json` | + +--- + +## Ejemplos de Uso + +### Button + +```jsx +import { Button } from '@/components/ui/button' + + + + + +``` + +### Card + +```jsx +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from "@/components/ui/card"; + + + + Título + Descripción + + Contenido aquí + + + +; +``` + +### Tabs + +```jsx +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; + + + + Tab 1 + Tab 2 + + Contenido 1 + Contenido 2 +; +``` + +### Progress + +```jsx +import { Progress } from "@/components/ui/progress"; + +; +``` + +### Table + +```jsx +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from "@/components/ui/table"; + + + + + Columna 1 + Columna 2 + + + + + Dato 1 + Dato 2 + + +
; +``` + +### Switch + +```jsx +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; + +
+ + +
; +``` + +--- + +## Características del Estilo + +- **Bordes gruesos**: 2px sólidos, típicamente negros +- **Sombras offset**: Box-shadow desplazadas (4px 4px 0) +- **Colores vivos**: Paletas llamativas, no sutiles +- **Sin bordes redondeados**: Esquinas rectas o mínimamente redondeadas +- **Alto contraste**: Texto legible sobre fondos brillantes +- **Hover states**: Transformaciones y cambios de color prominentes + +## Links + +- 📘 [Documentación oficial](https://www.neobrutalism.dev/docs) +- 🎨 [Estilos](https://www.neobrutalism.dev/styling) +- 📊 [Charts](https://www.neobrutalism.dev/charts) +- 🖼️ [Figma](https://www.neobrutalism.dev/docs/figma) +- 💻 [GitHub](https://github.com/ekmas/neobrutalism-components) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a13ad9..2f760c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ # =================================== # PARHELION CI - Build & Test Pipeline # Se ejecuta en cada push/PR a develop y main -# v0.5.0: API Skeleton endpoints, 24 tablas en base de datos +# v0.5.1: Foundation + Repository Pattern + xUnit Tests # =================================== name: CI Pipeline @@ -15,7 +15,7 @@ on: jobs: # ===== BACKEND (.NET 8) + PostgreSQL Tests ===== backend: - name: Backend Build & Integration Tests + name: Backend Build & xUnit Tests runs-on: ubuntu-latest defaults: run: @@ -24,7 +24,7 @@ jobs: # PostgreSQL service para tests de integracion reales services: postgres: - image: postgres:16 + image: postgres:17 env: POSTGRES_USER: parhelion_test POSTGRES_PASSWORD: test_password_ci @@ -55,15 +55,15 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore - # Tests de integracion con InMemory DB (rapidos, no requieren DB real) - - name: Run Integration Tests (InMemory) + # xUnit Tests (repository, pagination, foundation) + - name: Run xUnit Tests run: dotnet test --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" - name: Upload Test Results uses: actions/upload-artifact@v4 if: always() with: - name: test-results + name: backend-test-results path: backend/tests/**/test-results.trx # Validar que las migraciones se aplican correctamente a PostgreSQL @@ -75,10 +75,10 @@ jobs: env: ASPNETCORE_ENVIRONMENT: Testing - - name: Verify Database Schema + - name: Verify Database Schema (24 tables) run: | echo "Verificando que todas las tablas existen en PostgreSQL (24 tablas esperadas)..." - PGPASSWORD=test_password_ci psql -h localhost -U parhelion_test -d parhelion_test_db -c "\dt" | grep -E "(Tenants|Users|Employees|Shifts|Drivers|Trucks|Locations|Shipments|CatalogItems|InventoryStocks|InventoryTransactions)" && echo "Schema validation passed (v0.5.0)" + PGPASSWORD=test_password_ci psql -h localhost -U parhelion_test -d parhelion_test_db -c "\dt" | grep -E "(Tenants|Users|Employees|Shifts|Drivers|Trucks|Locations|Shipments|CatalogItems|InventoryStocks|InventoryTransactions)" && echo "Schema validation passed (v0.5.1)" # ===== FRONTEND ADMIN (Angular) ===== frontend-admin: diff --git a/.gitignore b/.gitignore index 84d04d8..3f5a850 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,4 @@ TestResults/ # ===== Local Development Tools (NEVER in production) ===== # Control panel is a private development tool - never committed to repository control-panel/ -.agent/ +agent/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c38484..d40c18d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,53 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.5.1] - 2025-12-16 + +### Agregado + +- **Foundation Layer (Repository Pattern)**: + + - `IGenericRepository` - Operaciones CRUD genericas con soft delete + - `ITenantRepository` - Repositorio con aislamiento multi-tenant + - `IUnitOfWork` - Coordinacion de transacciones entre repositorios + - `GenericRepository` y `TenantRepository` implementaciones + - `UnitOfWork` con todos los repositorios (Core, Fleet, Warehouse, Shipment, Network) + +- **DTOs Comunes**: + + - `PagedRequest` - Paginacion generica con ordenamiento y busqueda + - `PagedResult` - Respuesta paginada con metadata (TotalPages, HasNext, etc.) + - `BaseDto`, `TenantDto` - DTOs base con campos de auditoria + - `OperationResult`, `OperationResult` - Respuestas estandarizadas + +- **Infraestructura Docker**: + + - `docker-compose.yml` actualizado con servicios: postgres, api, admin, operaciones, campo, tunnel + - PostgreSQL 17 con volumen externo `postgres_pgdata` + - Healthchecks configurados para todos los servicios + - Cloudflare Tunnel para acceso remoto + +- **xUnit Tests (28 tests)**: + + - `PaginationDtoTests` - Validacion de paginacion + - `GenericRepositoryTests` - CRUD, soft delete, queries + - `InMemoryDbFixture` - Fixture con datos de prueba + - `TestDataBuilder` - Builder pattern para entidades de test + +### Modificado + +- CI/CD actualizado a v0.5.1 con paso explicito `Run xUnit Tests` +- PostgreSQL actualizado a version 17 en CI +- Swagger UI habilitado en todos los entornos (desarrollo via Tailscale) + +### Notas Tecnicas + +- Herramientas de desarrollo local (control-panel) excluidas del repositorio +- Foundation layer es prerequisito para Services layer (v0.5.2+) +- Tests de logica de negocio se implementan en fases 3-8 + +--- + ## [0.5.0] - 2025-12-15 ### Agregado diff --git a/README.md b/README.md index d26cde0..a783eee 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. -> **Estado:** Development Preview v0.5.0 - API Skeleton Implementado +> **Estado:** Development Preview v0.5.1 - Foundation Layer + Repository Pattern --- @@ -35,6 +35,8 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in - [x] **Infrastructure Layer:** EF Core + PostgreSQL + Migrations - [x] **API Skeleton:** 22 endpoints base para todas las entidades - [x] **Autenticacion:** JWT con roles SuperAdmin/Admin/Driver/Warehouse +- [x] **Repository Pattern:** GenericRepository + UnitOfWork + Soft Delete +- [x] **xUnit Tests:** 28 tests de foundation (paginacion, CRUD) ### Gestion de Flotilla @@ -77,7 +79,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in | Capa | Tecnología | Usuario | | :----------------------- | :------------------------------------ | :---------- | | **Backend** | C# / .NET 8 Web API | - | -| **Base de Datos** | PostgreSQL 16 | - | +| **Base de Datos** | PostgreSQL 17 | - | | **ORM** | Entity Framework Core (Code First) | - | | **Frontend (Admin)** | Angular 18+ (Material Design) | Admin | | **Frontend (Operacion)** | React + Vite + Tailwind CSS (PWA) | Almacenista | diff --git a/api-architecture.md b/api-architecture.md index a975a4f..e65da9f 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -4,8 +4,8 @@ Documentacion tecnica de la estructura API-First del backend Parhelion. ## Estado Actual -**Version:** 0.5.0 -**Enfoque:** API-First (Skeleton Endpoints) +**Version:** 0.5.1 +**Enfoque:** API-First (Skeleton + Foundation Layer) **Arquitectura:** Clean Architecture + Domain-Driven Design --- @@ -73,6 +73,29 @@ Gestion de red logistica Hub and Spoke. --- +## Foundation Layer (v0.5.1) + +Infraestructura base para operaciones CRUD y transacciones. + +### Repository Pattern + +| Interfaz | Implementacion | Descripcion | +| ----------------------- | ------------------- | -------------------------------- | +| `IGenericRepository` | `GenericRepository` | CRUD generico con soft delete | +| `ITenantRepository` | `TenantRepository` | Filtrado automatico por TenantId | +| `IUnitOfWork` | `UnitOfWork` | Coordinacion de transacciones | + +### DTOs Comunes + +| DTO | Uso | +| ----------------- | ----------------------------------------- | +| `PagedRequest` | Paginacion con Sort, Search, ActiveOnly | +| `PagedResult` | Respuesta con TotalPages, HasNext, etc | +| `BaseDto` | Campos comunes (Id, CreatedAt, UpdatedAt) | +| `OperationResult` | Respuestas estandarizadas | + +--- + ## Autenticacion Todos los endpoints protegidos requieren JWT Bearer token: @@ -94,16 +117,6 @@ El token se obtiene via `/api/auth/login` con credenciales validas. --- -## Documentacion Interactiva - -Swagger UI disponible en entorno de desarrollo: - -``` -http://localhost:5100/swagger -``` - ---- - ## Base de Datos - **Tablas:** 24 @@ -112,16 +125,26 @@ http://localhost:5100/swagger --- -## Pendientes +## Tests (xUnit) + +| Test Suite | Tests | Cobertura | +| ------------------------ | ------ | -------------------------- | +| `PaginationDtoTests` | 11 | PagedRequest, PagedResult | +| `GenericRepositoryTests` | 9 | CRUD, Soft Delete, Queries | +| **Total** | **28** | Foundation layer | + +--- + +## Pendientes (v0.5.2+) Los siguientes items quedan pendientes para futuras versiones: -- Implementacion de logica CRUD completa en cada endpoint -- Validaciones de DTOs con FluentValidation -- Calculos de peso volumetrico y costos -- Reglas de negocio (compatibilidad de carga, cadena de frio) -- Generacion de documentos legales (Carta Porte, POD) -- Tests unitarios y de integracion por endpoint +- [ ] Servicios CRUD por entidad (TenantService, ShipmentService, etc.) +- [ ] Validaciones de DTOs con FluentValidation +- [ ] Calculos de peso volumetrico y costos +- [ ] Reglas de negocio (compatibilidad de carga, cadena de frio) +- [ ] Generacion de documentos legales (Carta Porte, POD) +- [ ] Tests de logica de negocio (Fases 3-8) --- @@ -131,4 +154,4 @@ La gestion de endpoints durante desarrollo utiliza herramientas privadas que no --- -**Ultima actualizacion:** 2025-12-15 +**Ultima actualizacion:** 2025-12-16 diff --git a/backend/Dockerfile b/backend/Dockerfile index 3d31afe..2728372 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -5,33 +5,43 @@ # --- STAGE 1: Build --- FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -WORKDIR /src +WORKDIR /app + +# Copiar archivo de solución +COPY Parhelion.sln ./ -# Copiar archivos de proyecto primero (cache de dependencias) -COPY src/Parhelion.Domain/*.csproj ./Parhelion.Domain/ -COPY src/Parhelion.Application/*.csproj ./Parhelion.Application/ -COPY src/Parhelion.Infrastructure/*.csproj ./Parhelion.Infrastructure/ -COPY src/Parhelion.API/*.csproj ./Parhelion.API/ +# Copiar todos los archivos de proyecto manteniendo estructura src/ +COPY src/Parhelion.Domain/*.csproj ./src/Parhelion.Domain/ +COPY src/Parhelion.Application/*.csproj ./src/Parhelion.Application/ +COPY src/Parhelion.Infrastructure/*.csproj ./src/Parhelion.Infrastructure/ +COPY src/Parhelion.API/*.csproj ./src/Parhelion.API/ -# Restaurar dependencias -RUN dotnet restore Parhelion.API/Parhelion.API.csproj +# Copiar archivos de proyecto de tests +COPY tests/Parhelion.Tests/*.csproj ./tests/Parhelion.Tests/ + +# Restaurar dependencias de toda la solución +RUN dotnet restore Parhelion.sln # Copiar todo el código fuente -COPY src/ ./ +COPY src/ ./src/ +COPY tests/ ./tests/ # Build de la aplicación -RUN dotnet publish Parhelion.API/Parhelion.API.csproj -c Release -o /app/publish --no-restore +RUN dotnet publish src/Parhelion.API/Parhelion.API.csproj -c Release -o /publish # --- STAGE 2: Runtime --- FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app +# Instalar curl para healthcheck (como root) +RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* + # Crear usuario no-root para seguridad RUN adduser --disabled-password --gecos '' appuser USER appuser # Copiar artefactos del build -COPY --from=build /app/publish . +COPY --from=build /publish . # Puerto por defecto EXPOSE 5000 diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index 7dcc9fb..7c25627 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -97,11 +97,13 @@ } // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) +// Swagger habilitado en todos los entornos para acceso via Tailscale +app.UseSwagger(); +app.UseSwaggerUI(options => { - app.UseSwagger(); - app.UseSwaggerUI(); -} + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Parhelion API v1"); + options.RoutePrefix = "swagger"; +}); app.UseCors("DevCors"); diff --git a/backend/src/Parhelion.Application/DTOs/Common/BaseDto.cs b/backend/src/Parhelion.Application/DTOs/Common/BaseDto.cs new file mode 100644 index 0000000..4f3f42c --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Common/BaseDto.cs @@ -0,0 +1,52 @@ +namespace Parhelion.Application.DTOs.Common; + +/// +/// DTO base con campos de auditoría comunes. +/// +public abstract class BaseDto +{ + public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + +/// +/// DTO base para entidades multi-tenant. +/// +public abstract class TenantDto : BaseDto +{ + public Guid TenantId { get; set; } +} + +/// +/// Respuesta estándar de operación exitosa. +/// +public class OperationResult +{ + public bool Success { get; set; } + public string? Message { get; set; } + public Guid? Id { get; set; } + + public static OperationResult Ok(string? message = null) + => new() { Success = true, Message = message }; + + public static OperationResult Ok(Guid id, string? message = null) + => new() { Success = true, Id = id, Message = message }; + + public static OperationResult Fail(string message) + => new() { Success = false, Message = message }; +} + +/// +/// Respuesta estándar con datos. +/// +public class OperationResult : OperationResult +{ + public T? Data { get; set; } + + public static OperationResult Ok(T data, string? message = null) + => new() { Success = true, Data = data, Message = message }; + + public new static OperationResult Fail(string message) + => new() { Success = false, Message = message }; +} diff --git a/backend/src/Parhelion.Application/DTOs/Common/PagedRequest.cs b/backend/src/Parhelion.Application/DTOs/Common/PagedRequest.cs new file mode 100644 index 0000000..db4a6d7 --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Common/PagedRequest.cs @@ -0,0 +1,56 @@ +namespace Parhelion.Application.DTOs.Common; + +/// +/// Request para paginación y ordenamiento de listados. +/// Usado en todos los endpoints GET que retornan colecciones. +/// +public class PagedRequest +{ + private int _page = 1; + private int _pageSize = 20; + private const int MaxPageSize = 100; + + /// + /// Número de página (1-indexed). + /// + public int Page + { + get => _page; + set => _page = value < 1 ? 1 : value; + } + + /// + /// Cantidad de registros por página (max 100). + /// + public int PageSize + { + get => _pageSize; + set => _pageSize = value > MaxPageSize ? MaxPageSize : (value < 1 ? 1 : value); + } + + /// + /// Campo por el cual ordenar (ej: "CreatedAt", "Name"). + /// + public string? SortBy { get; set; } + + /// + /// True para orden descendente. + /// + public bool SortDescending { get; set; } + + /// + /// Búsqueda de texto libre (aplica a campos específicos por entidad). + /// + public string? Search { get; set; } + + /// + /// Filtrar solo activos (IsActive = true, IsDeleted = false). + /// Default: true + /// + public bool ActiveOnly { get; set; } = true; + + /// + /// Calcula el offset para skip en querys. + /// + public int Skip => (Page - 1) * PageSize; +} diff --git a/backend/src/Parhelion.Application/DTOs/Common/PagedResult.cs b/backend/src/Parhelion.Application/DTOs/Common/PagedResult.cs new file mode 100644 index 0000000..4b2352c --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Common/PagedResult.cs @@ -0,0 +1,39 @@ +namespace Parhelion.Application.DTOs.Common; + +/// +/// Respuesta paginada genérica para listados. +/// Incluye metadata de paginación para frontend. +/// +/// Tipo del DTO de cada item. +public class PagedResult +{ + public IEnumerable Items { get; set; } = Enumerable.Empty(); + public int TotalCount { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); + public bool HasPreviousPage => Page > 1; + public bool HasNextPage => Page < TotalPages; + + public PagedResult() { } + + public PagedResult(IEnumerable items, int totalCount, int page, int pageSize) + { + Items = items; + TotalCount = totalCount; + Page = page; + PageSize = pageSize; + } + + /// + /// Crea un resultado vacío. + /// + public static PagedResult Empty(int page = 1, int pageSize = 20) + => new(Enumerable.Empty(), 0, page, pageSize); + + /// + /// Crea resultado desde una lista ya paginada. + /// + public static PagedResult From(IEnumerable items, int totalCount, PagedRequest request) + => new(items, totalCount, request.Page, request.PageSize); +} diff --git a/backend/src/Parhelion.Application/Interfaces/IGenericRepository.cs b/backend/src/Parhelion.Application/Interfaces/IGenericRepository.cs new file mode 100644 index 0000000..f15dec0 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/IGenericRepository.cs @@ -0,0 +1,142 @@ +using System.Linq.Expressions; +using Parhelion.Application.DTOs.Common; +using Parhelion.Domain.Common; + +namespace Parhelion.Application.Interfaces; + +/// +/// Repository genérico para operaciones CRUD. +/// Implementa multi-tenancy y soft delete automáticamente. +/// +/// Tipo de entidad del dominio +public interface IGenericRepository where TEntity : BaseEntity +{ + // ========== READ OPERATIONS ========== + + /// + /// Obtiene una entidad por su ID. + /// Respeta soft delete y multi-tenancy. + /// + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// Obtiene una entidad por su ID con includes. + /// + Task GetByIdAsync(Guid id, params Expression>[] includes); + + /// + /// Obtiene todas las entidades (respeta query filters). + /// + Task> GetAllAsync(CancellationToken cancellationToken = default); + + /// + /// Obtiene entidades paginadas. + /// + Task<(IEnumerable Items, int TotalCount)> GetPagedAsync( + PagedRequest request, + Expression>? filter = null, + Func, IOrderedQueryable>? orderBy = null, + CancellationToken cancellationToken = default); + + /// + /// Busca entidades que cumplan un predicado. + /// + Task> FindAsync( + Expression> predicate, + CancellationToken cancellationToken = default); + + /// + /// Obtiene la primera entidad que cumpla un predicado. + /// + Task FirstOrDefaultAsync( + Expression> predicate, + CancellationToken cancellationToken = default); + + /// + /// Verifica si existe al menos una entidad que cumpla el predicado. + /// + Task AnyAsync( + Expression> predicate, + CancellationToken cancellationToken = default); + + /// + /// Cuenta entidades que cumplan el predicado. + /// + Task CountAsync( + Expression>? predicate = null, + CancellationToken cancellationToken = default); + + // ========== WRITE OPERATIONS ========== + + /// + /// Agrega una nueva entidad. + /// + Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); + + /// + /// Agrega múltiples entidades. + /// + Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default); + + /// + /// Actualiza una entidad existente. + /// + void Update(TEntity entity); + + /// + /// Actualiza múltiples entidades. + /// + void UpdateRange(IEnumerable entities); + + /// + /// Elimina una entidad (soft delete por defecto). + /// + void Delete(TEntity entity); + + /// + /// Elimina múltiples entidades (soft delete). + /// + void DeleteRange(IEnumerable entities); + + /// + /// Elimina físicamente una entidad (use con precaución). + /// + void HardDelete(TEntity entity); + + // ========== QUERYABLE ACCESS ========== + + /// + /// Obtiene IQueryable para queries complejas. + /// NOTA: Usar solo cuando los métodos anteriores no son suficientes. + /// + IQueryable Query(); + + /// + /// IQueryable sin tracking para lecturas. + /// + IQueryable QueryNoTracking(); +} + +/// +/// Repository específico para entidades multi-tenant. +/// Agrega filtrado automático por TenantId. +/// +/// Tipo de entidad del dominio +public interface ITenantRepository : IGenericRepository + where TEntity : TenantEntity +{ + /// + /// Obtiene todas las entidades del tenant actual. + /// + Task> GetAllForTenantAsync( + Guid tenantId, + CancellationToken cancellationToken = default); + + /// + /// Busca entidades del tenant actual. + /// + Task> FindForTenantAsync( + Guid tenantId, + Expression> predicate, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/IUnitOfWork.cs b/backend/src/Parhelion.Application/Interfaces/IUnitOfWork.cs new file mode 100644 index 0000000..a52b6e0 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/IUnitOfWork.cs @@ -0,0 +1,65 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces; + +/// +/// Unit of Work para coordinar transacciones entre repositorios. +/// Garantiza que múltiples operaciones se confirmen o reviertan juntas. +/// +public interface IUnitOfWork : IDisposable +{ + // ========== CORE REPOSITORIES ========== + IGenericRepository Tenants { get; } + ITenantRepository Users { get; } + IGenericRepository Roles { get; } + ITenantRepository Employees { get; } + ITenantRepository Clients { get; } + + // ========== FLEET REPOSITORIES ========== + ITenantRepository Trucks { get; } + IGenericRepository Drivers { get; } + ITenantRepository Shifts { get; } + ITenantRepository FleetLogs { get; } + + // ========== WAREHOUSE REPOSITORIES ========== + ITenantRepository Locations { get; } + IGenericRepository WarehouseZones { get; } + IGenericRepository WarehouseOperators { get; } + ITenantRepository InventoryStocks { get; } + ITenantRepository InventoryTransactions { get; } + ITenantRepository CatalogItems { get; } + + // ========== SHIPMENT REPOSITORIES ========== + ITenantRepository Shipments { get; } + IGenericRepository ShipmentItems { get; } + IGenericRepository ShipmentCheckpoints { get; } + IGenericRepository ShipmentDocuments { get; } + + // ========== NETWORK REPOSITORIES ========== + ITenantRepository NetworkLinks { get; } + ITenantRepository RouteBlueprints { get; } + IGenericRepository RouteSteps { get; } + + // ========== TRANSACTION CONTROL ========== + + /// + /// Guarda todos los cambios pendientes. + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// Inicia una transacción explícita. + /// + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Confirma la transacción actual. + /// + Task CommitTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Revierte la transacción actual. + /// + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Infrastructure/Repositories/GenericRepository.cs b/backend/src/Parhelion.Infrastructure/Repositories/GenericRepository.cs new file mode 100644 index 0000000..32d1cbd --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Repositories/GenericRepository.cs @@ -0,0 +1,225 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.Interfaces; +using Parhelion.Domain.Common; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.Infrastructure.Repositories; + +/// +/// Implementación genérica del Repository Pattern. +/// Maneja soft delete automáticamente. +/// +public class GenericRepository : IGenericRepository + where TEntity : BaseEntity +{ + protected readonly ParhelionDbContext _context; + protected readonly DbSet _dbSet; + + public GenericRepository(ParhelionDbContext context) + { + _context = context; + _dbSet = context.Set(); + } + + // ========== READ OPERATIONS ========== + + public virtual async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _dbSet.FindAsync(new object[] { id }, cancellationToken); + } + + public virtual async Task GetByIdAsync(Guid id, params Expression>[] includes) + { + IQueryable query = _dbSet; + + foreach (var include in includes) + { + query = query.Include(include); + } + + return await query.FirstOrDefaultAsync(e => e.Id == id); + } + + public virtual async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _dbSet.ToListAsync(cancellationToken); + } + + public virtual async Task<(IEnumerable Items, int TotalCount)> GetPagedAsync( + PagedRequest request, + Expression>? filter = null, + Func, IOrderedQueryable>? orderBy = null, + CancellationToken cancellationToken = default) + { + IQueryable query = _dbSet.AsNoTracking(); + + if (filter != null) + { + query = query.Where(filter); + } + + var totalCount = await query.CountAsync(cancellationToken); + + if (orderBy != null) + { + query = orderBy(query); + } + else + { + // Default: ordenar por CreatedAt descendente + query = query.OrderByDescending(e => e.CreatedAt); + } + + var items = await query + .Skip(request.Skip) + .Take(request.PageSize) + .ToListAsync(cancellationToken); + + return (items, totalCount); + } + + public virtual async Task> FindAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + return await _dbSet.Where(predicate).ToListAsync(cancellationToken); + } + + public virtual async Task FirstOrDefaultAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + return await _dbSet.FirstOrDefaultAsync(predicate, cancellationToken); + } + + public virtual async Task AnyAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + return await _dbSet.AnyAsync(predicate, cancellationToken); + } + + public virtual async Task CountAsync( + Expression>? predicate = null, + CancellationToken cancellationToken = default) + { + return predicate == null + ? await _dbSet.CountAsync(cancellationToken) + : await _dbSet.CountAsync(predicate, cancellationToken); + } + + // ========== WRITE OPERATIONS ========== + + public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) + { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + entity.CreatedAt = DateTime.UtcNow; + + await _dbSet.AddAsync(entity, cancellationToken); + return entity; + } + + public virtual async Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) + { + var now = DateTime.UtcNow; + foreach (var entity in entities) + { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + entity.CreatedAt = now; + } + + await _dbSet.AddRangeAsync(entities, cancellationToken); + } + + public virtual void Update(TEntity entity) + { + entity.UpdatedAt = DateTime.UtcNow; + _dbSet.Update(entity); + } + + public virtual void UpdateRange(IEnumerable entities) + { + var now = DateTime.UtcNow; + foreach (var entity in entities) + { + entity.UpdatedAt = now; + } + _dbSet.UpdateRange(entities); + } + + public virtual void Delete(TEntity entity) + { + // Soft delete + entity.IsDeleted = true; + entity.DeletedAt = DateTime.UtcNow; + _dbSet.Update(entity); + } + + public virtual void DeleteRange(IEnumerable entities) + { + var now = DateTime.UtcNow; + foreach (var entity in entities) + { + entity.IsDeleted = true; + entity.DeletedAt = now; + } + _dbSet.UpdateRange(entities); + } + + public virtual void HardDelete(TEntity entity) + { + _dbSet.Remove(entity); + } + + // ========== QUERYABLE ACCESS ========== + + public IQueryable Query() + { + return _dbSet.AsQueryable(); + } + + public IQueryable QueryNoTracking() + { + return _dbSet.AsNoTracking(); + } +} + +/// +/// Repository para entidades multi-tenant. +/// Agrega filtrado automático por TenantId. +/// +public class TenantRepository : GenericRepository, ITenantRepository + where TEntity : TenantEntity +{ + public TenantRepository(ParhelionDbContext context) : base(context) + { + } + + public async Task> GetAllForTenantAsync( + Guid tenantId, + CancellationToken cancellationToken = default) + { + return await _dbSet + .Where(e => e.TenantId == tenantId) + .ToListAsync(cancellationToken); + } + + public async Task> FindForTenantAsync( + Guid tenantId, + Expression> predicate, + CancellationToken cancellationToken = default) + { + return await _dbSet + .Where(e => e.TenantId == tenantId) + .Where(predicate) + .ToListAsync(cancellationToken); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Repositories/UnitOfWork.cs b/backend/src/Parhelion.Infrastructure/Repositories/UnitOfWork.cs new file mode 100644 index 0000000..eed6178 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Repositories/UnitOfWork.cs @@ -0,0 +1,180 @@ +using Microsoft.EntityFrameworkCore.Storage; +using Parhelion.Application.Interfaces; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.Infrastructure.Repositories; + +/// +/// Unit of Work implementation. +/// Coordina repositorios y transacciones. +/// +public class UnitOfWork : IUnitOfWork +{ + private readonly ParhelionDbContext _context; + private IDbContextTransaction? _currentTransaction; + + // Lazy-loaded repositories + private IGenericRepository? _tenants; + private ITenantRepository? _users; + private IGenericRepository? _roles; + private ITenantRepository? _employees; + private ITenantRepository? _clients; + + private ITenantRepository? _trucks; + private IGenericRepository? _drivers; + private ITenantRepository? _shifts; + private ITenantRepository? _fleetLogs; + + private ITenantRepository? _locations; + private IGenericRepository? _warehouseZones; + private IGenericRepository? _warehouseOperators; + private ITenantRepository? _inventoryStocks; + private ITenantRepository? _inventoryTransactions; + private ITenantRepository? _catalogItems; + + private ITenantRepository? _shipments; + private IGenericRepository? _shipmentItems; + private IGenericRepository? _shipmentCheckpoints; + private IGenericRepository? _shipmentDocuments; + + private ITenantRepository? _networkLinks; + private ITenantRepository? _routeBlueprints; + private IGenericRepository? _routeSteps; + + public UnitOfWork(ParhelionDbContext context) + { + _context = context; + } + + // ========== CORE REPOSITORIES ========== + + public IGenericRepository Tenants => + _tenants ??= new GenericRepository(_context); + + public ITenantRepository Users => + _users ??= new TenantRepository(_context); + + public IGenericRepository Roles => + _roles ??= new GenericRepository(_context); + + public ITenantRepository Employees => + _employees ??= new TenantRepository(_context); + + public ITenantRepository Clients => + _clients ??= new TenantRepository(_context); + + // ========== FLEET REPOSITORIES ========== + + public ITenantRepository Trucks => + _trucks ??= new TenantRepository(_context); + + public IGenericRepository Drivers => + _drivers ??= new GenericRepository(_context); + + public ITenantRepository Shifts => + _shifts ??= new TenantRepository(_context); + + public ITenantRepository FleetLogs => + _fleetLogs ??= new TenantRepository(_context); + + // ========== WAREHOUSE REPOSITORIES ========== + + public ITenantRepository Locations => + _locations ??= new TenantRepository(_context); + + public IGenericRepository WarehouseZones => + _warehouseZones ??= new GenericRepository(_context); + + public IGenericRepository WarehouseOperators => + _warehouseOperators ??= new GenericRepository(_context); + + public ITenantRepository InventoryStocks => + _inventoryStocks ??= new TenantRepository(_context); + + public ITenantRepository InventoryTransactions => + _inventoryTransactions ??= new TenantRepository(_context); + + public ITenantRepository CatalogItems => + _catalogItems ??= new TenantRepository(_context); + + // ========== SHIPMENT REPOSITORIES ========== + + public ITenantRepository Shipments => + _shipments ??= new TenantRepository(_context); + + public IGenericRepository ShipmentItems => + _shipmentItems ??= new GenericRepository(_context); + + public IGenericRepository ShipmentCheckpoints => + _shipmentCheckpoints ??= new GenericRepository(_context); + + public IGenericRepository ShipmentDocuments => + _shipmentDocuments ??= new GenericRepository(_context); + + // ========== NETWORK REPOSITORIES ========== + + public ITenantRepository NetworkLinks => + _networkLinks ??= new TenantRepository(_context); + + public ITenantRepository RouteBlueprints => + _routeBlueprints ??= new TenantRepository(_context); + + public IGenericRepository RouteSteps => + _routeSteps ??= new GenericRepository(_context); + + // ========== TRANSACTION CONTROL ========== + + public async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return await _context.SaveChangesAsync(cancellationToken); + } + + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) + { + _currentTransaction = await _context.Database.BeginTransactionAsync(cancellationToken); + } + + public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction != null) + { + await _currentTransaction.CommitAsync(cancellationToken); + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + } + + public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction != null) + { + await _currentTransaction.RollbackAsync(cancellationToken); + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + } + + // ========== DISPOSE ========== + + private bool _disposed; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _currentTransaction?.Dispose(); + _context.Dispose(); + } + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/backend/tests/Parhelion.Tests/Fixtures/InMemoryDbFixture.cs b/backend/tests/Parhelion.Tests/Fixtures/InMemoryDbFixture.cs new file mode 100644 index 0000000..4862ddd --- /dev/null +++ b/backend/tests/Parhelion.Tests/Fixtures/InMemoryDbFixture.cs @@ -0,0 +1,197 @@ +using Microsoft.EntityFrameworkCore; +using Parhelion.Domain.Entities; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.Tests.Fixtures; + +/// +/// Fixture para tests con base de datos en memoria. +/// Proporciona un DbContext limpio para cada test. +/// +public class InMemoryDbFixture : IDisposable +{ + /// + /// Crea un nuevo DbContext para el test. + /// Cada llamada crea una base de datos única. + /// + public ParhelionDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .EnableSensitiveDataLogging() + .Options; + + var context = new ParhelionDbContext(options); + context.Database.EnsureCreated(); + return context; + } + + /// + /// Crea un DbContext con datos de prueba sembrados. + /// + public ParhelionDbContext CreateSeededContext() + { + var context = CreateContext(); + SeedTestData(context); + return context; + } + + private void SeedTestData(ParhelionDbContext context) + { + // Roles seed + var adminRole = new Role + { + Id = Guid.Parse("11111111-1111-1111-1111-111111111111"), + Name = "Admin", + Description = "Administrador", + CreatedAt = DateTime.UtcNow + }; + var driverRole = new Role + { + Id = Guid.Parse("22222222-2222-2222-2222-222222222222"), + Name = "Driver", + Description = "Chofer", + CreatedAt = DateTime.UtcNow + }; + var warehouseRole = new Role + { + Id = Guid.Parse("44444444-4444-4444-4444-444444444444"), + Name = "Warehouse", + Description = "Almacenista", + CreatedAt = DateTime.UtcNow + }; + + context.Roles.AddRange(adminRole, driverRole, warehouseRole); + + // Test Tenant + var tenant = new Tenant + { + Id = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), + CompanyName = "Test Company", + ContactEmail = "test@company.com", + FleetSize = 5, + DriverCount = 3, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.Tenants.Add(tenant); + + // Test User (Admin) + var adminUser = new User + { + Id = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), + TenantId = tenant.Id, + Email = "admin@test.com", + PasswordHash = "hashedpassword", + FullName = "Test Admin", + RoleId = adminRole.Id, + IsActive = true, + IsDemoUser = false, + IsSuperAdmin = false, + CreatedAt = DateTime.UtcNow + }; + context.Users.Add(adminUser); + + context.SaveChanges(); + } + + public void Dispose() + { + // No cleanup needed for in-memory database + } +} + +/// +/// Builder para crear datos de prueba. +/// +public static class TestDataBuilder +{ + public static Tenant CreateTenant(string name = "Test Co", bool isActive = true) + { + return new Tenant + { + Id = Guid.NewGuid(), + CompanyName = name, + ContactEmail = $"{name.ToLower().Replace(" ", "")}@test.com", + FleetSize = 5, + DriverCount = 3, + IsActive = isActive, + CreatedAt = DateTime.UtcNow + }; + } + + public static User CreateUser(Guid tenantId, Guid roleId, string email = "user@test.com") + { + return new User + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Email = email, + PasswordHash = "hashedpassword", + FullName = "Test User", + RoleId = roleId, + IsActive = true, + IsDemoUser = false, + IsSuperAdmin = false, + CreatedAt = DateTime.UtcNow + }; + } + + public static Truck CreateTruck(Guid tenantId, string plate = "TEST-001") + { + return new Truck + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Plate = plate, + Model = "Test Model", + Type = Domain.Enums.TruckType.DryBox, + MaxCapacityKg = 10000, + MaxVolumeM3 = 50, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + } + + public static Location CreateLocation(Guid tenantId, string code = "TST") + { + return new Location + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Code = code, + Name = $"Test Location {code}", + Type = Domain.Enums.LocationType.RegionalHub, + FullAddress = "123 Test Street", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + } + + public static Shipment CreateShipment( + Guid tenantId, + Guid originId, + Guid destinationId, + string trackingNumber = "PAR-TEST01") + { + return new Shipment + { + Id = Guid.NewGuid(), + TenantId = tenantId, + TrackingNumber = trackingNumber, + QrCodeData = Guid.NewGuid().ToString(), + OriginLocationId = originId, + DestinationLocationId = destinationId, + RecipientName = "Test Recipient", + RecipientPhone = "555-0100", + TotalWeightKg = 100, + TotalVolumeM3 = 1, + Priority = Domain.Enums.ShipmentPriority.Normal, + Status = Domain.Enums.ShipmentStatus.PendingApproval, + CreatedAt = DateTime.UtcNow + }; + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/DTOs/PaginationDtoTests.cs b/backend/tests/Parhelion.Tests/Unit/DTOs/PaginationDtoTests.cs new file mode 100644 index 0000000..589f494 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/DTOs/PaginationDtoTests.cs @@ -0,0 +1,145 @@ +using Parhelion.Application.DTOs.Common; +using Xunit; + +namespace Parhelion.Tests.Unit.DTOs; + +/// +/// Tests para DTOs de paginación. +/// +public class PaginationDtoTests +{ + [Fact] + public void PagedRequest_DefaultValues_AreCorrect() + { + // Arrange & Act + var request = new PagedRequest(); + + // Assert + Assert.Equal(1, request.Page); + Assert.Equal(20, request.PageSize); + Assert.True(request.ActiveOnly); + Assert.Equal(0, request.Skip); + } + + [Fact] + public void PagedRequest_PageLessThanOne_SetsToOne() + { + // Arrange + var request = new PagedRequest { Page = -5 }; + + // Assert + Assert.Equal(1, request.Page); + } + + [Fact] + public void PagedRequest_PageSizeExceedsMax_CapsAtMax() + { + // Arrange + var request = new PagedRequest { PageSize = 500 }; + + // Assert + Assert.Equal(100, request.PageSize); + } + + [Fact] + public void PagedRequest_Skip_CalculatesCorrectly() + { + // Arrange + var request = new PagedRequest { Page = 3, PageSize = 10 }; + + // Assert + Assert.Equal(20, request.Skip); // (3-1) * 10 = 20 + } + + [Fact] + public void PagedResult_Empty_ReturnsEmptyResult() + { + // Arrange & Act + var result = PagedResult.Empty(2, 25); + + // Assert + Assert.Empty(result.Items); + Assert.Equal(0, result.TotalCount); + Assert.Equal(2, result.Page); + Assert.Equal(25, result.PageSize); + Assert.Equal(0, result.TotalPages); + Assert.True(result.HasPreviousPage); // Page 2 always has previous + Assert.False(result.HasNextPage); + } + + [Fact] + public void PagedResult_WithItems_CalculatesMetadata() + { + // Arrange + var items = new[] { "a", "b", "c" }; + + // Act + var result = new PagedResult(items, totalCount: 25, page: 2, pageSize: 10); + + // Assert + Assert.Equal(3, result.Items.Count()); + Assert.Equal(25, result.TotalCount); + Assert.Equal(3, result.TotalPages); // 25 / 10 = 2.5 -> 3 + Assert.True(result.HasPreviousPage); + Assert.True(result.HasNextPage); + } + + [Fact] + public void PagedResult_FirstPage_HasNoPreviousPage() + { + // Arrange & Act + var result = new PagedResult(new[] { 1, 2, 3 }, 30, page: 1, pageSize: 10); + + // Assert + Assert.False(result.HasPreviousPage); + Assert.True(result.HasNextPage); + } + + [Fact] + public void PagedResult_LastPage_HasNoNextPage() + { + // Arrange & Act + var result = new PagedResult(new[] { 1, 2, 3 }, 23, page: 3, pageSize: 10); + + // Assert + Assert.True(result.HasPreviousPage); + Assert.False(result.HasNextPage); + } + + [Fact] + public void OperationResult_Ok_SetsSuccessTrue() + { + // Arrange & Act + var result = OperationResult.Ok("Test message"); + + // Assert + Assert.True(result.Success); + Assert.Equal("Test message", result.Message); + } + + [Fact] + public void OperationResult_Fail_SetsSuccessFalse() + { + // Arrange & Act + var result = OperationResult.Fail("Error message"); + + // Assert + Assert.False(result.Success); + Assert.Equal("Error message", result.Message); + } + + [Fact] + public void OperationResultGeneric_Ok_IncludesData() + { + // Arrange + var data = new { Name = "Test", Value = 42 }; + + // Act + var result = OperationResult.Ok(data, "Success"); + + // Assert + Assert.True(result.Success); + Assert.NotNull(result.Data); + Assert.Equal("Success", result.Message); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Repositories/GenericRepositoryTests.cs b/backend/tests/Parhelion.Tests/Unit/Repositories/GenericRepositoryTests.cs new file mode 100644 index 0000000..140984b --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Repositories/GenericRepositoryTests.cs @@ -0,0 +1,162 @@ +using Parhelion.Infrastructure.Repositories; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Repositories; + +/// +/// Tests para GenericRepository. +/// +public class GenericRepositoryTests : IClassFixture +{ + private readonly InMemoryDbFixture _fixture; + + public GenericRepositoryTests(InMemoryDbFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task AddAsync_NewEntity_SetsIdAndCreatedAt() + { + // Arrange + using var context = _fixture.CreateContext(); + var repository = new GenericRepository(context); + var tenant = TestDataBuilder.CreateTenant("New Tenant"); + tenant.Id = Guid.Empty; // Simular entidad sin ID + + // Act + var result = await repository.AddAsync(tenant); + await context.SaveChangesAsync(); + + // Assert + Assert.NotEqual(Guid.Empty, result.Id); + Assert.True(result.CreatedAt != default); + } + + [Fact] + public async Task GetByIdAsync_ExistingEntity_ReturnsEntity() + { + // Arrange + using var context = _fixture.CreateSeededContext(); + var repository = new GenericRepository(context); + var expectedId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + + // Act + var result = await repository.GetByIdAsync(expectedId); + + // Assert + Assert.NotNull(result); + Assert.Equal("Test Company", result.CompanyName); + } + + [Fact] + public async Task GetByIdAsync_NonExistingEntity_ReturnsNull() + { + // Arrange + using var context = _fixture.CreateContext(); + var repository = new GenericRepository(context); + + // Act + var result = await repository.GetByIdAsync(Guid.NewGuid()); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task Delete_Entity_SetsSoftDeleteFlags() + { + // Arrange + using var context = _fixture.CreateContext(); + var repository = new GenericRepository(context); + var tenant = TestDataBuilder.CreateTenant("To Delete"); + await repository.AddAsync(tenant); + await context.SaveChangesAsync(); + + // Act + repository.Delete(tenant); + await context.SaveChangesAsync(); + + // Assert + Assert.True(tenant.IsDeleted); + Assert.NotNull(tenant.DeletedAt); + } + + [Fact] + public async Task Update_Entity_SetsUpdatedAt() + { + // Arrange + using var context = _fixture.CreateContext(); + var repository = new GenericRepository(context); + var tenant = TestDataBuilder.CreateTenant("To Update"); + await repository.AddAsync(tenant); + await context.SaveChangesAsync(); + + // Act + tenant.CompanyName = "Updated Name"; + repository.Update(tenant); + await context.SaveChangesAsync(); + + // Assert + Assert.NotNull(tenant.UpdatedAt); + Assert.Equal("Updated Name", tenant.CompanyName); + } + + [Fact] + public async Task AnyAsync_WithMatchingPredicate_ReturnsTrue() + { + // Arrange + using var context = _fixture.CreateSeededContext(); + var repository = new GenericRepository(context); + + // Act + var exists = await repository.AnyAsync(t => t.CompanyName == "Test Company"); + + // Assert + Assert.True(exists); + } + + [Fact] + public async Task AnyAsync_WithNonMatchingPredicate_ReturnsFalse() + { + // Arrange + using var context = _fixture.CreateSeededContext(); + var repository = new GenericRepository(context); + + // Act + var exists = await repository.AnyAsync(t => t.CompanyName == "Non Existing"); + + // Assert + Assert.False(exists); + } + + [Fact] + public async Task CountAsync_ReturnsCorrectCount() + { + // Arrange + using var context = _fixture.CreateSeededContext(); + var repository = new GenericRepository(context); + + // Act + var count = await repository.CountAsync(); + + // Assert + Assert.Equal(3, count); // Admin, Driver, Warehouse from seed + } + + [Fact] + public async Task FindAsync_WithPredicate_ReturnsMatchingEntities() + { + // Arrange + using var context = _fixture.CreateSeededContext(); + var repository = new GenericRepository(context); + + // Act + var results = await repository.FindAsync(r => r.Name.Contains("Admin")); + + // Assert + Assert.Single(results); + Assert.Equal("Admin", results.First().Name); + } +} diff --git a/backups/parhelion_dev_backup_20251216_013659.sql b/backups/parhelion_dev_backup_20251216_013659.sql new file mode 100644 index 0000000..c9d7b3e --- /dev/null +++ b/backups/parhelion_dev_backup_20251216_013659.sql @@ -0,0 +1,2007 @@ +-- +-- PostgreSQL database dump +-- + +\restrict vPbXrNWUlquOeEJ5QzhfD8JnUNQKiT4WuaC9WX29tPKWN2DF69yiKlPnA6Wcagr + +-- Dumped from database version 17.6 (Debian 17.6-2.pgdg13+1) +-- Dumped by pg_dump version 17.6 (Debian 17.6-2.pgdg13+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: CatalogItems; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."CatalogItems" ( + "Id" uuid NOT NULL, + "Sku" character varying(50) NOT NULL, + "Name" character varying(200) NOT NULL, + "Description" character varying(1000), + "BaseUom" character varying(20) NOT NULL, + "DefaultWeightKg" numeric(10,3) NOT NULL, + "DefaultWidthCm" numeric(10,2) NOT NULL, + "DefaultHeightCm" numeric(10,2) NOT NULL, + "DefaultLengthCm" numeric(10,2) NOT NULL, + "RequiresRefrigeration" boolean NOT NULL, + "IsHazardous" boolean NOT NULL, + "IsFragile" boolean NOT NULL, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid, + "TenantId" uuid NOT NULL +); + + +ALTER TABLE public."CatalogItems" OWNER TO "MetaCodeX"; + +-- +-- Name: Clients; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Clients" ( + "Id" uuid NOT NULL, + "CompanyName" character varying(200) NOT NULL, + "TradeName" character varying(200), + "ContactName" character varying(150) NOT NULL, + "Email" character varying(150) NOT NULL, + "Phone" character varying(30) NOT NULL, + "TaxId" character varying(20), + "LegalName" character varying(300), + "BillingAddress" character varying(500), + "ShippingAddress" character varying(500) NOT NULL, + "PreferredProductTypes" character varying(300), + "Priority" character varying(20) NOT NULL, + "IsActive" boolean NOT NULL, + "Notes" character varying(1000), + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Clients" OWNER TO "MetaCodeX"; + +-- +-- Name: Drivers; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Drivers" ( + "Id" uuid NOT NULL, + "EmployeeId" uuid NOT NULL, + "LicenseNumber" character varying(50) NOT NULL, + "DefaultTruckId" uuid, + "CurrentTruckId" uuid, + "Status" integer NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid, + "LicenseExpiration" timestamp with time zone, + "LicenseType" character varying(10), + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Drivers" OWNER TO "MetaCodeX"; + +-- +-- Name: Employees; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Employees" ( + "Id" uuid NOT NULL, + "UserId" uuid NOT NULL, + "Phone" character varying(20) NOT NULL, + "Rfc" character varying(13), + "Nss" character varying(11), + "Curp" character varying(18), + "EmergencyContact" character varying(200), + "EmergencyPhone" character varying(20), + "HireDate" timestamp with time zone, + "ShiftId" uuid, + "Department" character varying(50), + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Employees" OWNER TO "MetaCodeX"; + +-- +-- Name: FleetLogs; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."FleetLogs" ( + "Id" uuid NOT NULL, + "DriverId" uuid NOT NULL, + "OldTruckId" uuid, + "NewTruckId" uuid NOT NULL, + "Reason" integer NOT NULL, + "Timestamp" timestamp with time zone NOT NULL, + "CreatedByUserId" uuid NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."FleetLogs" OWNER TO "MetaCodeX"; + +-- +-- Name: InventoryStocks; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."InventoryStocks" ( + "Id" uuid NOT NULL, + "ZoneId" uuid NOT NULL, + "ProductId" uuid NOT NULL, + "Quantity" numeric(18,4) NOT NULL, + "QuantityReserved" numeric(18,4) NOT NULL, + "BatchNumber" character varying(100), + "ExpiryDate" timestamp with time zone, + "LastCountDate" timestamp with time zone, + "UnitCost" numeric(18,4), + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid, + "TenantId" uuid NOT NULL +); + + +ALTER TABLE public."InventoryStocks" OWNER TO "MetaCodeX"; + +-- +-- Name: InventoryTransactions; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."InventoryTransactions" ( + "Id" uuid NOT NULL, + "ProductId" uuid NOT NULL, + "OriginZoneId" uuid, + "DestinationZoneId" uuid, + "Quantity" numeric(18,4) NOT NULL, + "TransactionType" integer NOT NULL, + "PerformedByUserId" uuid NOT NULL, + "ShipmentId" uuid, + "BatchNumber" character varying(100), + "Remarks" character varying(500), + "Timestamp" timestamp with time zone NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid, + "TenantId" uuid NOT NULL +); + + +ALTER TABLE public."InventoryTransactions" OWNER TO "MetaCodeX"; + +-- +-- Name: Locations; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Locations" ( + "Id" uuid NOT NULL, + "Code" character varying(10) NOT NULL, + "Name" character varying(100) NOT NULL, + "Type" integer NOT NULL, + "FullAddress" character varying(500) NOT NULL, + "CanReceive" boolean NOT NULL, + "CanDispatch" boolean NOT NULL, + "IsInternal" boolean NOT NULL, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid, + "Latitude" numeric(9,6), + "Longitude" numeric(9,6) +); + + +ALTER TABLE public."Locations" OWNER TO "MetaCodeX"; + +-- +-- Name: NetworkLinks; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."NetworkLinks" ( + "Id" uuid NOT NULL, + "OriginLocationId" uuid NOT NULL, + "DestinationLocationId" uuid NOT NULL, + "LinkType" integer NOT NULL, + "TransitTime" interval NOT NULL, + "IsBidirectional" boolean NOT NULL, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."NetworkLinks" OWNER TO "MetaCodeX"; + +-- +-- Name: RefreshTokens; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."RefreshTokens" ( + "Id" uuid NOT NULL, + "UserId" uuid NOT NULL, + "TokenHash" character varying(256) NOT NULL, + "ExpiresAt" timestamp with time zone NOT NULL, + "IsRevoked" boolean DEFAULT false NOT NULL, + "RevokedAt" timestamp with time zone, + "RevokedReason" character varying(200), + "CreatedFromIp" character varying(45), + "UserAgent" character varying(500), + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."RefreshTokens" OWNER TO "MetaCodeX"; + +-- +-- Name: Roles; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Roles" ( + "Id" uuid NOT NULL, + "Name" character varying(50) NOT NULL, + "Description" text, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Roles" OWNER TO "MetaCodeX"; + +-- +-- Name: RouteBlueprints; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."RouteBlueprints" ( + "Id" uuid NOT NULL, + "Name" character varying(100) NOT NULL, + "Description" character varying(500), + "TotalSteps" integer NOT NULL, + "TotalTransitTime" interval NOT NULL, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."RouteBlueprints" OWNER TO "MetaCodeX"; + +-- +-- Name: RouteSteps; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."RouteSteps" ( + "Id" uuid NOT NULL, + "RouteBlueprintId" uuid NOT NULL, + "LocationId" uuid NOT NULL, + "StepOrder" integer NOT NULL, + "StandardTransitTime" interval NOT NULL, + "StepType" integer NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."RouteSteps" OWNER TO "MetaCodeX"; + +-- +-- Name: Shifts; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Shifts" ( + "Id" uuid NOT NULL, + "Name" character varying(100) NOT NULL, + "StartTime" time without time zone NOT NULL, + "EndTime" time without time zone NOT NULL, + "DaysOfWeek" character varying(50) NOT NULL, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Shifts" OWNER TO "MetaCodeX"; + +-- +-- Name: ShipmentCheckpoints; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."ShipmentCheckpoints" ( + "Id" uuid NOT NULL, + "ShipmentId" uuid NOT NULL, + "LocationId" uuid, + "StatusCode" integer NOT NULL, + "Remarks" character varying(1000), + "Timestamp" timestamp with time zone NOT NULL, + "CreatedByUserId" uuid NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "ActionType" character varying(50), + "HandledByDriverId" uuid, + "LoadedOntoTruckId" uuid, + "NewCustodian" character varying(200), + "PreviousCustodian" character varying(200), + "HandledByWarehouseOperatorId" uuid, + "LastModifiedByUserId" uuid, + "Latitude" numeric(9,6), + "Longitude" numeric(9,6) +); + + +ALTER TABLE public."ShipmentCheckpoints" OWNER TO "MetaCodeX"; + +-- +-- Name: ShipmentDocuments; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."ShipmentDocuments" ( + "Id" uuid NOT NULL, + "ShipmentId" uuid NOT NULL, + "DocumentType" integer NOT NULL, + "FileUrl" character varying(500) NOT NULL, + "GeneratedBy" character varying(50) NOT NULL, + "GeneratedAt" timestamp with time zone NOT NULL, + "ExpiresAt" timestamp with time zone, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."ShipmentDocuments" OWNER TO "MetaCodeX"; + +-- +-- Name: ShipmentItems; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."ShipmentItems" ( + "Id" uuid NOT NULL, + "ShipmentId" uuid NOT NULL, + "Sku" character varying(50), + "Description" character varying(500) NOT NULL, + "PackagingType" integer NOT NULL, + "Quantity" integer NOT NULL, + "WeightKg" numeric(10,2) NOT NULL, + "WidthCm" numeric(10,2) NOT NULL, + "HeightCm" numeric(10,2) NOT NULL, + "LengthCm" numeric(10,2) NOT NULL, + "DeclaredValue" numeric(18,2) NOT NULL, + "IsFragile" boolean NOT NULL, + "IsHazardous" boolean NOT NULL, + "RequiresRefrigeration" boolean NOT NULL, + "StackingInstructions" character varying(500), + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid, + "ProductId" uuid +); + + +ALTER TABLE public."ShipmentItems" OWNER TO "MetaCodeX"; + +-- +-- Name: Shipments; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Shipments" ( + "Id" uuid NOT NULL, + "TrackingNumber" character varying(20) NOT NULL, + "QrCodeData" character varying(100) NOT NULL, + "OriginLocationId" uuid NOT NULL, + "DestinationLocationId" uuid NOT NULL, + "AssignedRouteId" uuid, + "CurrentStepOrder" integer, + "RecipientName" character varying(200) NOT NULL, + "RecipientPhone" character varying(20), + "TotalWeightKg" numeric(10,2) NOT NULL, + "TotalVolumeM3" numeric(10,3) NOT NULL, + "DeclaredValue" numeric(18,2), + "SatMerchandiseCode" character varying(20), + "DeliveryInstructions" character varying(1000), + "RecipientSignatureUrl" character varying(500), + "Priority" integer NOT NULL, + "Status" integer NOT NULL, + "TruckId" uuid, + "DriverId" uuid, + "WasQrScanned" boolean NOT NULL, + "IsDelayed" boolean NOT NULL, + "ScheduledDeparture" timestamp with time zone, + "PickupWindowStart" timestamp with time zone, + "PickupWindowEnd" timestamp with time zone, + "EstimatedArrival" timestamp with time zone, + "AssignedAt" timestamp with time zone, + "DeliveredAt" timestamp with time zone, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "RecipientClientId" uuid, + "SenderId" uuid, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Shipments" OWNER TO "MetaCodeX"; + +-- +-- Name: Tenants; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Tenants" ( + "Id" uuid NOT NULL, + "CompanyName" character varying(200) NOT NULL, + "ContactEmail" character varying(256) NOT NULL, + "FleetSize" integer NOT NULL, + "DriverCount" integer NOT NULL, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Tenants" OWNER TO "MetaCodeX"; + +-- +-- Name: Trucks; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Trucks" ( + "Id" uuid NOT NULL, + "Plate" character varying(20) NOT NULL, + "Model" character varying(100) NOT NULL, + "Type" integer NOT NULL, + "MaxCapacityKg" numeric(10,2) NOT NULL, + "MaxVolumeM3" numeric(10,2) NOT NULL, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "Color" text, + "CurrentOdometerKm" numeric, + "EngineNumber" text, + "InsuranceExpiration" timestamp with time zone, + "InsurancePolicy" text, + "LastMaintenanceDate" timestamp with time zone, + "NextMaintenanceDate" timestamp with time zone, + "VerificationExpiration" timestamp with time zone, + "VerificationNumber" text, + "Vin" text, + "Year" integer, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Trucks" OWNER TO "MetaCodeX"; + +-- +-- Name: Users; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."Users" ( + "Id" uuid NOT NULL, + "Email" character varying(256) NOT NULL, + "PasswordHash" character varying(500) NOT NULL, + "FullName" character varying(200) NOT NULL, + "RoleId" uuid NOT NULL, + "IsDemoUser" boolean NOT NULL, + "UsesArgon2" boolean NOT NULL, + "LastLogin" timestamp with time zone, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "TenantId" uuid NOT NULL, + "IsSuperAdmin" boolean DEFAULT false NOT NULL, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."Users" OWNER TO "MetaCodeX"; + +-- +-- Name: WarehouseOperators; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."WarehouseOperators" ( + "Id" uuid NOT NULL, + "EmployeeId" uuid NOT NULL, + "AssignedLocationId" uuid NOT NULL, + "PrimaryZoneId" uuid, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."WarehouseOperators" OWNER TO "MetaCodeX"; + +-- +-- Name: WarehouseZones; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."WarehouseZones" ( + "Id" uuid NOT NULL, + "LocationId" uuid NOT NULL, + "Code" character varying(20) NOT NULL, + "Name" character varying(100) NOT NULL, + "Type" integer NOT NULL, + "IsActive" boolean NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + "UpdatedAt" timestamp with time zone, + "IsDeleted" boolean NOT NULL, + "DeletedAt" timestamp with time zone, + "CreatedByUserId" uuid, + "LastModifiedByUserId" uuid +); + + +ALTER TABLE public."WarehouseZones" OWNER TO "MetaCodeX"; + +-- +-- Name: __EFMigrationsHistory; Type: TABLE; Schema: public; Owner: MetaCodeX +-- + +CREATE TABLE public."__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL +); + + +ALTER TABLE public."__EFMigrationsHistory" OWNER TO "MetaCodeX"; + +-- +-- Data for Name: CatalogItems; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."CatalogItems" ("Id", "Sku", "Name", "Description", "BaseUom", "DefaultWeightKg", "DefaultWidthCm", "DefaultHeightCm", "DefaultLengthCm", "RequiresRefrigeration", "IsHazardous", "IsFragile", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId", "TenantId") FROM stdin; +\. + + +-- +-- Data for Name: Clients; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Clients" ("Id", "CompanyName", "TradeName", "ContactName", "Email", "Phone", "TaxId", "LegalName", "BillingAddress", "ShippingAddress", "PreferredProductTypes", "Priority", "IsActive", "Notes", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: Drivers; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Drivers" ("Id", "EmployeeId", "LicenseNumber", "DefaultTruckId", "CurrentTruckId", "Status", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "LicenseExpiration", "LicenseType", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: Employees; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Employees" ("Id", "UserId", "Phone", "Rfc", "Nss", "Curp", "EmergencyContact", "EmergencyPhone", "HireDate", "ShiftId", "Department", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: FleetLogs; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."FleetLogs" ("Id", "DriverId", "OldTruckId", "NewTruckId", "Reason", "Timestamp", "CreatedByUserId", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: InventoryStocks; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."InventoryStocks" ("Id", "ZoneId", "ProductId", "Quantity", "QuantityReserved", "BatchNumber", "ExpiryDate", "LastCountDate", "UnitCost", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId", "TenantId") FROM stdin; +\. + + +-- +-- Data for Name: InventoryTransactions; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."InventoryTransactions" ("Id", "ProductId", "OriginZoneId", "DestinationZoneId", "Quantity", "TransactionType", "PerformedByUserId", "ShipmentId", "BatchNumber", "Remarks", "Timestamp", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId", "TenantId") FROM stdin; +\. + + +-- +-- Data for Name: Locations; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Locations" ("Id", "Code", "Name", "Type", "FullAddress", "CanReceive", "CanDispatch", "IsInternal", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "CreatedByUserId", "LastModifiedByUserId", "Latitude", "Longitude") FROM stdin; +\. + + +-- +-- Data for Name: NetworkLinks; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."NetworkLinks" ("Id", "OriginLocationId", "DestinationLocationId", "LinkType", "TransitTime", "IsBidirectional", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: RefreshTokens; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."RefreshTokens" ("Id", "UserId", "TokenHash", "ExpiresAt", "IsRevoked", "RevokedAt", "RevokedReason", "CreatedFromIp", "UserAgent", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +ef919193-8cb7-4f60-bc7c-6d240983b1eb 00000000-0000-0000-0000-000000000001 b3KSGCViUaxxaMF2duWSAltYtUxSrBSOjkTq+8axLuc= 2025-12-22 01:45:20.060723+00 f \N \N 127.0.0.1 node 2025-12-15 01:45:20.231994+00 \N f \N \N \N +4fb00dab-0bd4-4419-a8ee-3b610489e0a0 00000000-0000-0000-0000-000000000001 36WZ5Vm29xcS3wkZsZvLB7dT4BKAEPDLEgfM/oISGJY= 2025-12-22 01:46:18.594664+00 f \N \N 127.0.0.1 node 2025-12-15 01:46:18.60054+00 \N f \N \N \N +e6502b63-f8e3-41d2-9b54-c88f5fb787a6 00000000-0000-0000-0000-000000000001 ZJHwla8b3fgW18CHviD2FbSEkQEMcwixjrHUzJCtQM0= 2025-12-22 01:50:49.93796+00 f \N \N 127.0.0.1 node 2025-12-15 01:50:49.940207+00 \N f \N \N \N +5ae661fa-f1ef-4b3d-8f2f-fadab41009fa 00000000-0000-0000-0000-000000000001 t//Mr7dPMKQxrb08CwKi01+m7V5KSiN1yUZZBrwKKOw= 2025-12-22 04:09:00.00483+00 f \N \N 127.0.0.1 node 2025-12-15 04:09:00.234369+00 \N f \N \N \N +\. + + +-- +-- Data for Name: Roles; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Roles" ("Id", "Name", "Description", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +11111111-1111-1111-1111-111111111111 Admin Gerente de Tráfico - Acceso total al sistema 2025-12-13 00:20:55.508718+00 \N f \N \N \N +22222222-2222-2222-2222-222222222222 Driver Chofer - Solo ve sus envíos asignados 2025-12-13 00:20:55.508718+00 \N f \N \N \N +33333333-3333-3333-3333-333333333333 DemoUser Usuario de demostración temporal (24-48h) 2025-12-13 00:20:55.508718+00 \N f \N \N \N +44444444-4444-4444-4444-444444444444 Warehouse Almacenista - Gestiona carga y descarga de camiones 2025-12-13 00:20:55.508718+00 \N f \N \N \N +55555555-5555-5555-5555-555555555555 SystemAdmin Super Admin - Gestiona tenants y administradores (v0.4.3) 2025-12-14 02:04:53.167361+00 \N f \N \N \N +00000000-0000-0000-0000-000000000001 SuperAdmin Super Administrator with full system access 2025-12-15 01:44:01.669271+00 \N f \N \N \N +\. + + +-- +-- Data for Name: RouteBlueprints; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."RouteBlueprints" ("Id", "Name", "Description", "TotalSteps", "TotalTransitTime", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: RouteSteps; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."RouteSteps" ("Id", "RouteBlueprintId", "LocationId", "StepOrder", "StandardTransitTime", "StepType", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: Shifts; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Shifts" ("Id", "Name", "StartTime", "EndTime", "DaysOfWeek", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: ShipmentCheckpoints; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."ShipmentCheckpoints" ("Id", "ShipmentId", "LocationId", "StatusCode", "Remarks", "Timestamp", "CreatedByUserId", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "ActionType", "HandledByDriverId", "LoadedOntoTruckId", "NewCustodian", "PreviousCustodian", "HandledByWarehouseOperatorId", "LastModifiedByUserId", "Latitude", "Longitude") FROM stdin; +\. + + +-- +-- Data for Name: ShipmentDocuments; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."ShipmentDocuments" ("Id", "ShipmentId", "DocumentType", "FileUrl", "GeneratedBy", "GeneratedAt", "ExpiresAt", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: ShipmentItems; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."ShipmentItems" ("Id", "ShipmentId", "Sku", "Description", "PackagingType", "Quantity", "WeightKg", "WidthCm", "HeightCm", "LengthCm", "DeclaredValue", "IsFragile", "IsHazardous", "RequiresRefrigeration", "StackingInstructions", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId", "ProductId") FROM stdin; +\. + + +-- +-- Data for Name: Shipments; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Shipments" ("Id", "TrackingNumber", "QrCodeData", "OriginLocationId", "DestinationLocationId", "AssignedRouteId", "CurrentStepOrder", "RecipientName", "RecipientPhone", "TotalWeightKg", "TotalVolumeM3", "DeclaredValue", "SatMerchandiseCode", "DeliveryInstructions", "RecipientSignatureUrl", "Priority", "Status", "TruckId", "DriverId", "WasQrScanned", "IsDelayed", "ScheduledDeparture", "PickupWindowStart", "PickupWindowEnd", "EstimatedArrival", "AssignedAt", "DeliveredAt", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "RecipientClientId", "SenderId", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: Tenants; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Tenants" ("Id", "CompanyName", "ContactEmail", "FleetSize", "DriverCount", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +00000000-0000-0000-0000-000000000001 Parhelion Logistics admin@parhelion.com 0 0 t 2025-12-15 01:44:39.806809+00 \N f \N \N \N +\. + + +-- +-- Data for Name: Trucks; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Trucks" ("Id", "Plate", "Model", "Type", "MaxCapacityKg", "MaxVolumeM3", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "Color", "CurrentOdometerKm", "EngineNumber", "InsuranceExpiration", "InsurancePolicy", "LastMaintenanceDate", "NextMaintenanceDate", "VerificationExpiration", "VerificationNumber", "Vin", "Year", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: Users; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."Users" ("Id", "Email", "PasswordHash", "FullName", "RoleId", "IsDemoUser", "UsesArgon2", "LastLogin", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "TenantId", "IsSuperAdmin", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +00000000-0000-0000-0000-000000000001 metacodex@parhelion.com $2b$14$biNEvC.Y.mAhfgWvgM5SyugH3xOIEI2oDAuqKpvcktwy9KsBvQCxK MetaCodeX CEO 00000000-0000-0000-0000-000000000001 f t 2025-12-15 04:08:59.962106+00 t 2025-12-15 01:44:59.445268+00 2025-12-15 04:09:00.234369+00 f \N 00000000-0000-0000-0000-000000000001 t \N \N +\. + + +-- +-- Data for Name: WarehouseOperators; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."WarehouseOperators" ("Id", "EmployeeId", "AssignedLocationId", "PrimaryZoneId", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: WarehouseZones; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."WarehouseZones" ("Id", "LocationId", "Code", "Name", "Type", "IsActive", "CreatedAt", "UpdatedAt", "IsDeleted", "DeletedAt", "CreatedByUserId", "LastModifiedByUserId") FROM stdin; +\. + + +-- +-- Data for Name: __EFMigrationsHistory; Type: TABLE DATA; Schema: public; Owner: MetaCodeX +-- + +COPY public."__EFMigrationsHistory" ("MigrationId", "ProductVersion") FROM stdin; +20251213001913_InitialCreate 8.0.10 +20251213030538_AddAuthAndClients 8.0.10 +20251213194319_AddEmployeeLayerV043 8.0.10 +20251214153448_WmsEnhancement044 8.0.10 +\. + + +-- +-- Name: CatalogItems PK_CatalogItems; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."CatalogItems" + ADD CONSTRAINT "PK_CatalogItems" PRIMARY KEY ("Id"); + + +-- +-- Name: Clients PK_Clients; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Clients" + ADD CONSTRAINT "PK_Clients" PRIMARY KEY ("Id"); + + +-- +-- Name: Drivers PK_Drivers; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Drivers" + ADD CONSTRAINT "PK_Drivers" PRIMARY KEY ("Id"); + + +-- +-- Name: Employees PK_Employees; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Employees" + ADD CONSTRAINT "PK_Employees" PRIMARY KEY ("Id"); + + +-- +-- Name: FleetLogs PK_FleetLogs; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."FleetLogs" + ADD CONSTRAINT "PK_FleetLogs" PRIMARY KEY ("Id"); + + +-- +-- Name: InventoryStocks PK_InventoryStocks; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryStocks" + ADD CONSTRAINT "PK_InventoryStocks" PRIMARY KEY ("Id"); + + +-- +-- Name: InventoryTransactions PK_InventoryTransactions; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryTransactions" + ADD CONSTRAINT "PK_InventoryTransactions" PRIMARY KEY ("Id"); + + +-- +-- Name: Locations PK_Locations; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Locations" + ADD CONSTRAINT "PK_Locations" PRIMARY KEY ("Id"); + + +-- +-- Name: NetworkLinks PK_NetworkLinks; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."NetworkLinks" + ADD CONSTRAINT "PK_NetworkLinks" PRIMARY KEY ("Id"); + + +-- +-- Name: RefreshTokens PK_RefreshTokens; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."RefreshTokens" + ADD CONSTRAINT "PK_RefreshTokens" PRIMARY KEY ("Id"); + + +-- +-- Name: Roles PK_Roles; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Roles" + ADD CONSTRAINT "PK_Roles" PRIMARY KEY ("Id"); + + +-- +-- Name: RouteBlueprints PK_RouteBlueprints; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."RouteBlueprints" + ADD CONSTRAINT "PK_RouteBlueprints" PRIMARY KEY ("Id"); + + +-- +-- Name: RouteSteps PK_RouteSteps; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."RouteSteps" + ADD CONSTRAINT "PK_RouteSteps" PRIMARY KEY ("Id"); + + +-- +-- Name: Shifts PK_Shifts; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shifts" + ADD CONSTRAINT "PK_Shifts" PRIMARY KEY ("Id"); + + +-- +-- Name: ShipmentCheckpoints PK_ShipmentCheckpoints; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentCheckpoints" + ADD CONSTRAINT "PK_ShipmentCheckpoints" PRIMARY KEY ("Id"); + + +-- +-- Name: ShipmentDocuments PK_ShipmentDocuments; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentDocuments" + ADD CONSTRAINT "PK_ShipmentDocuments" PRIMARY KEY ("Id"); + + +-- +-- Name: ShipmentItems PK_ShipmentItems; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentItems" + ADD CONSTRAINT "PK_ShipmentItems" PRIMARY KEY ("Id"); + + +-- +-- Name: Shipments PK_Shipments; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "PK_Shipments" PRIMARY KEY ("Id"); + + +-- +-- Name: Tenants PK_Tenants; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Tenants" + ADD CONSTRAINT "PK_Tenants" PRIMARY KEY ("Id"); + + +-- +-- Name: Trucks PK_Trucks; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Trucks" + ADD CONSTRAINT "PK_Trucks" PRIMARY KEY ("Id"); + + +-- +-- Name: Users PK_Users; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Users" + ADD CONSTRAINT "PK_Users" PRIMARY KEY ("Id"); + + +-- +-- Name: WarehouseOperators PK_WarehouseOperators; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."WarehouseOperators" + ADD CONSTRAINT "PK_WarehouseOperators" PRIMARY KEY ("Id"); + + +-- +-- Name: WarehouseZones PK_WarehouseZones; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."WarehouseZones" + ADD CONSTRAINT "PK_WarehouseZones" PRIMARY KEY ("Id"); + + +-- +-- Name: __EFMigrationsHistory PK___EFMigrationsHistory; Type: CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."__EFMigrationsHistory" + ADD CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId"); + + +-- +-- Name: IX_CatalogItems_TenantId_Sku; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_CatalogItems_TenantId_Sku" ON public."CatalogItems" USING btree ("TenantId", "Sku"); + + +-- +-- Name: IX_Clients_Email; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Clients_Email" ON public."Clients" USING btree ("Email"); + + +-- +-- Name: IX_Clients_TenantId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Clients_TenantId" ON public."Clients" USING btree ("TenantId"); + + +-- +-- Name: IX_Clients_TenantId_CompanyName; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Clients_TenantId_CompanyName" ON public."Clients" USING btree ("TenantId", "CompanyName"); + + +-- +-- Name: IX_Drivers_CurrentTruckId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Drivers_CurrentTruckId" ON public."Drivers" USING btree ("CurrentTruckId"); + + +-- +-- Name: IX_Drivers_DefaultTruckId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Drivers_DefaultTruckId" ON public."Drivers" USING btree ("DefaultTruckId"); + + +-- +-- Name: IX_Drivers_EmployeeId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_Drivers_EmployeeId" ON public."Drivers" USING btree ("EmployeeId"); + + +-- +-- Name: IX_Drivers_Status; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Drivers_Status" ON public."Drivers" USING btree ("Status"); + + +-- +-- Name: IX_Drivers_TenantId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Drivers_TenantId" ON public."Drivers" USING btree ("TenantId"); + + +-- +-- Name: IX_Employees_ShiftId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Employees_ShiftId" ON public."Employees" USING btree ("ShiftId"); + + +-- +-- Name: IX_Employees_TenantId_Department; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Employees_TenantId_Department" ON public."Employees" USING btree ("TenantId", "Department"); + + +-- +-- Name: IX_Employees_UserId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_Employees_UserId" ON public."Employees" USING btree ("UserId"); + + +-- +-- Name: IX_FleetLogs_CreatedByUserId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_FleetLogs_CreatedByUserId" ON public."FleetLogs" USING btree ("CreatedByUserId"); + + +-- +-- Name: IX_FleetLogs_DriverId_Timestamp; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_FleetLogs_DriverId_Timestamp" ON public."FleetLogs" USING btree ("DriverId", "Timestamp"); + + +-- +-- Name: IX_FleetLogs_NewTruckId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_FleetLogs_NewTruckId" ON public."FleetLogs" USING btree ("NewTruckId"); + + +-- +-- Name: IX_FleetLogs_OldTruckId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_FleetLogs_OldTruckId" ON public."FleetLogs" USING btree ("OldTruckId"); + + +-- +-- Name: IX_FleetLogs_TenantId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_FleetLogs_TenantId" ON public."FleetLogs" USING btree ("TenantId"); + + +-- +-- Name: IX_InventoryStocks_ProductId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryStocks_ProductId" ON public."InventoryStocks" USING btree ("ProductId"); + + +-- +-- Name: IX_InventoryStocks_TenantId_ExpiryDate; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryStocks_TenantId_ExpiryDate" ON public."InventoryStocks" USING btree ("TenantId", "ExpiryDate") WHERE ("ExpiryDate" IS NOT NULL); + + +-- +-- Name: IX_InventoryStocks_TenantId_ProductId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryStocks_TenantId_ProductId" ON public."InventoryStocks" USING btree ("TenantId", "ProductId"); + + +-- +-- Name: IX_InventoryStocks_ZoneId_ProductId_BatchNumber; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_InventoryStocks_ZoneId_ProductId_BatchNumber" ON public."InventoryStocks" USING btree ("ZoneId", "ProductId", "BatchNumber"); + + +-- +-- Name: IX_InventoryTransactions_DestinationZoneId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryTransactions_DestinationZoneId" ON public."InventoryTransactions" USING btree ("DestinationZoneId"); + + +-- +-- Name: IX_InventoryTransactions_OriginZoneId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryTransactions_OriginZoneId" ON public."InventoryTransactions" USING btree ("OriginZoneId"); + + +-- +-- Name: IX_InventoryTransactions_PerformedByUserId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryTransactions_PerformedByUserId" ON public."InventoryTransactions" USING btree ("PerformedByUserId"); + + +-- +-- Name: IX_InventoryTransactions_ProductId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryTransactions_ProductId" ON public."InventoryTransactions" USING btree ("ProductId"); + + +-- +-- Name: IX_InventoryTransactions_ShipmentId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryTransactions_ShipmentId" ON public."InventoryTransactions" USING btree ("ShipmentId") WHERE ("ShipmentId" IS NOT NULL); + + +-- +-- Name: IX_InventoryTransactions_TenantId_ProductId_Timestamp; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryTransactions_TenantId_ProductId_Timestamp" ON public."InventoryTransactions" USING btree ("TenantId", "ProductId", "Timestamp"); + + +-- +-- Name: IX_InventoryTransactions_TenantId_Timestamp; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_InventoryTransactions_TenantId_Timestamp" ON public."InventoryTransactions" USING btree ("TenantId", "Timestamp"); + + +-- +-- Name: IX_Locations_TenantId_Code; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_Locations_TenantId_Code" ON public."Locations" USING btree ("TenantId", "Code"); + + +-- +-- Name: IX_Locations_TenantId_Type; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Locations_TenantId_Type" ON public."Locations" USING btree ("TenantId", "Type"); + + +-- +-- Name: IX_NetworkLinks_DestinationLocationId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_NetworkLinks_DestinationLocationId" ON public."NetworkLinks" USING btree ("DestinationLocationId"); + + +-- +-- Name: IX_NetworkLinks_OriginLocationId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_NetworkLinks_OriginLocationId" ON public."NetworkLinks" USING btree ("OriginLocationId"); + + +-- +-- Name: IX_NetworkLinks_TenantId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_NetworkLinks_TenantId" ON public."NetworkLinks" USING btree ("TenantId"); + + +-- +-- Name: IX_RefreshTokens_ExpiresAt; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_RefreshTokens_ExpiresAt" ON public."RefreshTokens" USING btree ("ExpiresAt"); + + +-- +-- Name: IX_RefreshTokens_TokenHash; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_RefreshTokens_TokenHash" ON public."RefreshTokens" USING btree ("TokenHash"); + + +-- +-- Name: IX_RefreshTokens_UserId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_RefreshTokens_UserId" ON public."RefreshTokens" USING btree ("UserId"); + + +-- +-- Name: IX_Roles_Name; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_Roles_Name" ON public."Roles" USING btree ("Name"); + + +-- +-- Name: IX_RouteBlueprints_TenantId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_RouteBlueprints_TenantId" ON public."RouteBlueprints" USING btree ("TenantId"); + + +-- +-- Name: IX_RouteSteps_LocationId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_RouteSteps_LocationId" ON public."RouteSteps" USING btree ("LocationId"); + + +-- +-- Name: IX_RouteSteps_RouteBlueprintId_StepOrder; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_RouteSteps_RouteBlueprintId_StepOrder" ON public."RouteSteps" USING btree ("RouteBlueprintId", "StepOrder"); + + +-- +-- Name: IX_Shifts_TenantId_IsActive; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shifts_TenantId_IsActive" ON public."Shifts" USING btree ("TenantId", "IsActive"); + + +-- +-- Name: IX_ShipmentCheckpoints_CreatedByUserId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentCheckpoints_CreatedByUserId" ON public."ShipmentCheckpoints" USING btree ("CreatedByUserId"); + + +-- +-- Name: IX_ShipmentCheckpoints_HandledByDriverId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentCheckpoints_HandledByDriverId" ON public."ShipmentCheckpoints" USING btree ("HandledByDriverId"); + + +-- +-- Name: IX_ShipmentCheckpoints_HandledByWarehouseOperatorId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentCheckpoints_HandledByWarehouseOperatorId" ON public."ShipmentCheckpoints" USING btree ("HandledByWarehouseOperatorId"); + + +-- +-- Name: IX_ShipmentCheckpoints_LoadedOntoTruckId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentCheckpoints_LoadedOntoTruckId" ON public."ShipmentCheckpoints" USING btree ("LoadedOntoTruckId"); + + +-- +-- Name: IX_ShipmentCheckpoints_LocationId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentCheckpoints_LocationId" ON public."ShipmentCheckpoints" USING btree ("LocationId"); + + +-- +-- Name: IX_ShipmentCheckpoints_ShipmentId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentCheckpoints_ShipmentId" ON public."ShipmentCheckpoints" USING btree ("ShipmentId"); + + +-- +-- Name: IX_ShipmentCheckpoints_Timestamp; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentCheckpoints_Timestamp" ON public."ShipmentCheckpoints" USING btree ("Timestamp"); + + +-- +-- Name: IX_ShipmentDocuments_ShipmentId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentDocuments_ShipmentId" ON public."ShipmentDocuments" USING btree ("ShipmentId"); + + +-- +-- Name: IX_ShipmentItems_ProductId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentItems_ProductId" ON public."ShipmentItems" USING btree ("ProductId"); + + +-- +-- Name: IX_ShipmentItems_ShipmentId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_ShipmentItems_ShipmentId" ON public."ShipmentItems" USING btree ("ShipmentId"); + + +-- +-- Name: IX_Shipments_AssignedRouteId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_AssignedRouteId" ON public."Shipments" USING btree ("AssignedRouteId"); + + +-- +-- Name: IX_Shipments_DestinationLocationId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_DestinationLocationId" ON public."Shipments" USING btree ("DestinationLocationId"); + + +-- +-- Name: IX_Shipments_DriverId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_DriverId" ON public."Shipments" USING btree ("DriverId"); + + +-- +-- Name: IX_Shipments_OriginLocationId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_OriginLocationId" ON public."Shipments" USING btree ("OriginLocationId"); + + +-- +-- Name: IX_Shipments_RecipientClientId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_RecipientClientId" ON public."Shipments" USING btree ("RecipientClientId"); + + +-- +-- Name: IX_Shipments_SenderId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_SenderId" ON public."Shipments" USING btree ("SenderId"); + + +-- +-- Name: IX_Shipments_TenantId_CreatedAt; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_TenantId_CreatedAt" ON public."Shipments" USING btree ("TenantId", "CreatedAt"); + + +-- +-- Name: IX_Shipments_TenantId_IsDelayed; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_TenantId_IsDelayed" ON public."Shipments" USING btree ("TenantId", "IsDelayed") WHERE ("IsDelayed" = true); + + +-- +-- Name: IX_Shipments_TenantId_Status; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_TenantId_Status" ON public."Shipments" USING btree ("TenantId", "Status"); + + +-- +-- Name: IX_Shipments_TrackingNumber; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_Shipments_TrackingNumber" ON public."Shipments" USING btree ("TrackingNumber"); + + +-- +-- Name: IX_Shipments_TruckId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Shipments_TruckId" ON public."Shipments" USING btree ("TruckId"); + + +-- +-- Name: IX_Tenants_IsActive; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Tenants_IsActive" ON public."Tenants" USING btree ("IsActive"); + + +-- +-- Name: IX_Trucks_TenantId_Plate; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_Trucks_TenantId_Plate" ON public."Trucks" USING btree ("TenantId", "Plate"); + + +-- +-- Name: IX_Trucks_TenantId_Type; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Trucks_TenantId_Type" ON public."Trucks" USING btree ("TenantId", "Type"); + + +-- +-- Name: IX_Users_Email; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_Users_Email" ON public."Users" USING btree ("Email"); + + +-- +-- Name: IX_Users_RoleId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Users_RoleId" ON public."Users" USING btree ("RoleId"); + + +-- +-- Name: IX_Users_TenantId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_Users_TenantId" ON public."Users" USING btree ("TenantId"); + + +-- +-- Name: IX_WarehouseOperators_AssignedLocationId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_WarehouseOperators_AssignedLocationId" ON public."WarehouseOperators" USING btree ("AssignedLocationId"); + + +-- +-- Name: IX_WarehouseOperators_EmployeeId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_WarehouseOperators_EmployeeId" ON public."WarehouseOperators" USING btree ("EmployeeId"); + + +-- +-- Name: IX_WarehouseOperators_PrimaryZoneId; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE INDEX "IX_WarehouseOperators_PrimaryZoneId" ON public."WarehouseOperators" USING btree ("PrimaryZoneId"); + + +-- +-- Name: IX_WarehouseZones_LocationId_Code; Type: INDEX; Schema: public; Owner: MetaCodeX +-- + +CREATE UNIQUE INDEX "IX_WarehouseZones_LocationId_Code" ON public."WarehouseZones" USING btree ("LocationId", "Code"); + + +-- +-- Name: CatalogItems FK_CatalogItems_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."CatalogItems" + ADD CONSTRAINT "FK_CatalogItems_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: Clients FK_Clients_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Clients" + ADD CONSTRAINT "FK_Clients_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: Drivers FK_Drivers_Employees_EmployeeId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Drivers" + ADD CONSTRAINT "FK_Drivers_Employees_EmployeeId" FOREIGN KEY ("EmployeeId") REFERENCES public."Employees"("Id") ON DELETE CASCADE; + + +-- +-- Name: Drivers FK_Drivers_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Drivers" + ADD CONSTRAINT "FK_Drivers_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id"); + + +-- +-- Name: Drivers FK_Drivers_Trucks_CurrentTruckId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Drivers" + ADD CONSTRAINT "FK_Drivers_Trucks_CurrentTruckId" FOREIGN KEY ("CurrentTruckId") REFERENCES public."Trucks"("Id") ON DELETE SET NULL; + + +-- +-- Name: Drivers FK_Drivers_Trucks_DefaultTruckId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Drivers" + ADD CONSTRAINT "FK_Drivers_Trucks_DefaultTruckId" FOREIGN KEY ("DefaultTruckId") REFERENCES public."Trucks"("Id") ON DELETE SET NULL; + + +-- +-- Name: Employees FK_Employees_Shifts_ShiftId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Employees" + ADD CONSTRAINT "FK_Employees_Shifts_ShiftId" FOREIGN KEY ("ShiftId") REFERENCES public."Shifts"("Id") ON DELETE SET NULL; + + +-- +-- Name: Employees FK_Employees_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Employees" + ADD CONSTRAINT "FK_Employees_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: Employees FK_Employees_Users_UserId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Employees" + ADD CONSTRAINT "FK_Employees_Users_UserId" FOREIGN KEY ("UserId") REFERENCES public."Users"("Id") ON DELETE RESTRICT; + + +-- +-- Name: FleetLogs FK_FleetLogs_Drivers_DriverId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."FleetLogs" + ADD CONSTRAINT "FK_FleetLogs_Drivers_DriverId" FOREIGN KEY ("DriverId") REFERENCES public."Drivers"("Id") ON DELETE RESTRICT; + + +-- +-- Name: FleetLogs FK_FleetLogs_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."FleetLogs" + ADD CONSTRAINT "FK_FleetLogs_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: FleetLogs FK_FleetLogs_Trucks_NewTruckId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."FleetLogs" + ADD CONSTRAINT "FK_FleetLogs_Trucks_NewTruckId" FOREIGN KEY ("NewTruckId") REFERENCES public."Trucks"("Id") ON DELETE RESTRICT; + + +-- +-- Name: FleetLogs FK_FleetLogs_Trucks_OldTruckId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."FleetLogs" + ADD CONSTRAINT "FK_FleetLogs_Trucks_OldTruckId" FOREIGN KEY ("OldTruckId") REFERENCES public."Trucks"("Id") ON DELETE SET NULL; + + +-- +-- Name: FleetLogs FK_FleetLogs_Users_CreatedByUserId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."FleetLogs" + ADD CONSTRAINT "FK_FleetLogs_Users_CreatedByUserId" FOREIGN KEY ("CreatedByUserId") REFERENCES public."Users"("Id") ON DELETE RESTRICT; + + +-- +-- Name: InventoryStocks FK_InventoryStocks_CatalogItems_ProductId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryStocks" + ADD CONSTRAINT "FK_InventoryStocks_CatalogItems_ProductId" FOREIGN KEY ("ProductId") REFERENCES public."CatalogItems"("Id") ON DELETE RESTRICT; + + +-- +-- Name: InventoryStocks FK_InventoryStocks_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryStocks" + ADD CONSTRAINT "FK_InventoryStocks_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: InventoryStocks FK_InventoryStocks_WarehouseZones_ZoneId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryStocks" + ADD CONSTRAINT "FK_InventoryStocks_WarehouseZones_ZoneId" FOREIGN KEY ("ZoneId") REFERENCES public."WarehouseZones"("Id") ON DELETE RESTRICT; + + +-- +-- Name: InventoryTransactions FK_InventoryTransactions_CatalogItems_ProductId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryTransactions" + ADD CONSTRAINT "FK_InventoryTransactions_CatalogItems_ProductId" FOREIGN KEY ("ProductId") REFERENCES public."CatalogItems"("Id") ON DELETE RESTRICT; + + +-- +-- Name: InventoryTransactions FK_InventoryTransactions_Shipments_ShipmentId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryTransactions" + ADD CONSTRAINT "FK_InventoryTransactions_Shipments_ShipmentId" FOREIGN KEY ("ShipmentId") REFERENCES public."Shipments"("Id") ON DELETE SET NULL; + + +-- +-- Name: InventoryTransactions FK_InventoryTransactions_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryTransactions" + ADD CONSTRAINT "FK_InventoryTransactions_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: InventoryTransactions FK_InventoryTransactions_Users_PerformedByUserId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryTransactions" + ADD CONSTRAINT "FK_InventoryTransactions_Users_PerformedByUserId" FOREIGN KEY ("PerformedByUserId") REFERENCES public."Users"("Id") ON DELETE RESTRICT; + + +-- +-- Name: InventoryTransactions FK_InventoryTransactions_WarehouseZones_DestinationZoneId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryTransactions" + ADD CONSTRAINT "FK_InventoryTransactions_WarehouseZones_DestinationZoneId" FOREIGN KEY ("DestinationZoneId") REFERENCES public."WarehouseZones"("Id") ON DELETE SET NULL; + + +-- +-- Name: InventoryTransactions FK_InventoryTransactions_WarehouseZones_OriginZoneId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."InventoryTransactions" + ADD CONSTRAINT "FK_InventoryTransactions_WarehouseZones_OriginZoneId" FOREIGN KEY ("OriginZoneId") REFERENCES public."WarehouseZones"("Id") ON DELETE SET NULL; + + +-- +-- Name: Locations FK_Locations_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Locations" + ADD CONSTRAINT "FK_Locations_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: NetworkLinks FK_NetworkLinks_Locations_DestinationLocationId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."NetworkLinks" + ADD CONSTRAINT "FK_NetworkLinks_Locations_DestinationLocationId" FOREIGN KEY ("DestinationLocationId") REFERENCES public."Locations"("Id") ON DELETE RESTRICT; + + +-- +-- Name: NetworkLinks FK_NetworkLinks_Locations_OriginLocationId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."NetworkLinks" + ADD CONSTRAINT "FK_NetworkLinks_Locations_OriginLocationId" FOREIGN KEY ("OriginLocationId") REFERENCES public."Locations"("Id") ON DELETE RESTRICT; + + +-- +-- Name: NetworkLinks FK_NetworkLinks_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."NetworkLinks" + ADD CONSTRAINT "FK_NetworkLinks_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: RefreshTokens FK_RefreshTokens_Users_UserId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."RefreshTokens" + ADD CONSTRAINT "FK_RefreshTokens_Users_UserId" FOREIGN KEY ("UserId") REFERENCES public."Users"("Id") ON DELETE CASCADE; + + +-- +-- Name: RouteBlueprints FK_RouteBlueprints_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."RouteBlueprints" + ADD CONSTRAINT "FK_RouteBlueprints_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: RouteSteps FK_RouteSteps_Locations_LocationId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."RouteSteps" + ADD CONSTRAINT "FK_RouteSteps_Locations_LocationId" FOREIGN KEY ("LocationId") REFERENCES public."Locations"("Id") ON DELETE RESTRICT; + + +-- +-- Name: RouteSteps FK_RouteSteps_RouteBlueprints_RouteBlueprintId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."RouteSteps" + ADD CONSTRAINT "FK_RouteSteps_RouteBlueprints_RouteBlueprintId" FOREIGN KEY ("RouteBlueprintId") REFERENCES public."RouteBlueprints"("Id") ON DELETE CASCADE; + + +-- +-- Name: Shifts FK_Shifts_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shifts" + ADD CONSTRAINT "FK_Shifts_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: ShipmentCheckpoints FK_ShipmentCheckpoints_Drivers_HandledByDriverId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentCheckpoints" + ADD CONSTRAINT "FK_ShipmentCheckpoints_Drivers_HandledByDriverId" FOREIGN KEY ("HandledByDriverId") REFERENCES public."Drivers"("Id") ON DELETE SET NULL; + + +-- +-- Name: ShipmentCheckpoints FK_ShipmentCheckpoints_Locations_LocationId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentCheckpoints" + ADD CONSTRAINT "FK_ShipmentCheckpoints_Locations_LocationId" FOREIGN KEY ("LocationId") REFERENCES public."Locations"("Id") ON DELETE SET NULL; + + +-- +-- Name: ShipmentCheckpoints FK_ShipmentCheckpoints_Shipments_ShipmentId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentCheckpoints" + ADD CONSTRAINT "FK_ShipmentCheckpoints_Shipments_ShipmentId" FOREIGN KEY ("ShipmentId") REFERENCES public."Shipments"("Id") ON DELETE CASCADE; + + +-- +-- Name: ShipmentCheckpoints FK_ShipmentCheckpoints_Trucks_LoadedOntoTruckId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentCheckpoints" + ADD CONSTRAINT "FK_ShipmentCheckpoints_Trucks_LoadedOntoTruckId" FOREIGN KEY ("LoadedOntoTruckId") REFERENCES public."Trucks"("Id") ON DELETE SET NULL; + + +-- +-- Name: ShipmentCheckpoints FK_ShipmentCheckpoints_Users_CreatedByUserId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentCheckpoints" + ADD CONSTRAINT "FK_ShipmentCheckpoints_Users_CreatedByUserId" FOREIGN KEY ("CreatedByUserId") REFERENCES public."Users"("Id") ON DELETE RESTRICT; + + +-- +-- Name: ShipmentCheckpoints FK_ShipmentCheckpoints_WarehouseOperators_HandledByWarehouseOp~; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentCheckpoints" + ADD CONSTRAINT "FK_ShipmentCheckpoints_WarehouseOperators_HandledByWarehouseOp~" FOREIGN KEY ("HandledByWarehouseOperatorId") REFERENCES public."WarehouseOperators"("Id"); + + +-- +-- Name: ShipmentDocuments FK_ShipmentDocuments_Shipments_ShipmentId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentDocuments" + ADD CONSTRAINT "FK_ShipmentDocuments_Shipments_ShipmentId" FOREIGN KEY ("ShipmentId") REFERENCES public."Shipments"("Id") ON DELETE CASCADE; + + +-- +-- Name: ShipmentItems FK_ShipmentItems_CatalogItems_ProductId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentItems" + ADD CONSTRAINT "FK_ShipmentItems_CatalogItems_ProductId" FOREIGN KEY ("ProductId") REFERENCES public."CatalogItems"("Id") ON DELETE SET NULL; + + +-- +-- Name: ShipmentItems FK_ShipmentItems_Shipments_ShipmentId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."ShipmentItems" + ADD CONSTRAINT "FK_ShipmentItems_Shipments_ShipmentId" FOREIGN KEY ("ShipmentId") REFERENCES public."Shipments"("Id") ON DELETE CASCADE; + + +-- +-- Name: Shipments FK_Shipments_Clients_RecipientClientId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "FK_Shipments_Clients_RecipientClientId" FOREIGN KEY ("RecipientClientId") REFERENCES public."Clients"("Id") ON DELETE SET NULL; + + +-- +-- Name: Shipments FK_Shipments_Clients_SenderId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "FK_Shipments_Clients_SenderId" FOREIGN KEY ("SenderId") REFERENCES public."Clients"("Id") ON DELETE SET NULL; + + +-- +-- Name: Shipments FK_Shipments_Drivers_DriverId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "FK_Shipments_Drivers_DriverId" FOREIGN KEY ("DriverId") REFERENCES public."Drivers"("Id") ON DELETE SET NULL; + + +-- +-- Name: Shipments FK_Shipments_Locations_DestinationLocationId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "FK_Shipments_Locations_DestinationLocationId" FOREIGN KEY ("DestinationLocationId") REFERENCES public."Locations"("Id") ON DELETE RESTRICT; + + +-- +-- Name: Shipments FK_Shipments_Locations_OriginLocationId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "FK_Shipments_Locations_OriginLocationId" FOREIGN KEY ("OriginLocationId") REFERENCES public."Locations"("Id") ON DELETE RESTRICT; + + +-- +-- Name: Shipments FK_Shipments_RouteBlueprints_AssignedRouteId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "FK_Shipments_RouteBlueprints_AssignedRouteId" FOREIGN KEY ("AssignedRouteId") REFERENCES public."RouteBlueprints"("Id") ON DELETE SET NULL; + + +-- +-- Name: Shipments FK_Shipments_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "FK_Shipments_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: Shipments FK_Shipments_Trucks_TruckId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Shipments" + ADD CONSTRAINT "FK_Shipments_Trucks_TruckId" FOREIGN KEY ("TruckId") REFERENCES public."Trucks"("Id") ON DELETE SET NULL; + + +-- +-- Name: Trucks FK_Trucks_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Trucks" + ADD CONSTRAINT "FK_Trucks_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: Users FK_Users_Roles_RoleId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Users" + ADD CONSTRAINT "FK_Users_Roles_RoleId" FOREIGN KEY ("RoleId") REFERENCES public."Roles"("Id") ON DELETE RESTRICT; + + +-- +-- Name: Users FK_Users_Tenants_TenantId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."Users" + ADD CONSTRAINT "FK_Users_Tenants_TenantId" FOREIGN KEY ("TenantId") REFERENCES public."Tenants"("Id") ON DELETE RESTRICT; + + +-- +-- Name: WarehouseOperators FK_WarehouseOperators_Employees_EmployeeId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."WarehouseOperators" + ADD CONSTRAINT "FK_WarehouseOperators_Employees_EmployeeId" FOREIGN KEY ("EmployeeId") REFERENCES public."Employees"("Id") ON DELETE CASCADE; + + +-- +-- Name: WarehouseOperators FK_WarehouseOperators_Locations_AssignedLocationId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."WarehouseOperators" + ADD CONSTRAINT "FK_WarehouseOperators_Locations_AssignedLocationId" FOREIGN KEY ("AssignedLocationId") REFERENCES public."Locations"("Id") ON DELETE RESTRICT; + + +-- +-- Name: WarehouseOperators FK_WarehouseOperators_WarehouseZones_PrimaryZoneId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."WarehouseOperators" + ADD CONSTRAINT "FK_WarehouseOperators_WarehouseZones_PrimaryZoneId" FOREIGN KEY ("PrimaryZoneId") REFERENCES public."WarehouseZones"("Id") ON DELETE SET NULL; + + +-- +-- Name: WarehouseZones FK_WarehouseZones_Locations_LocationId; Type: FK CONSTRAINT; Schema: public; Owner: MetaCodeX +-- + +ALTER TABLE ONLY public."WarehouseZones" + ADD CONSTRAINT "FK_WarehouseZones_Locations_LocationId" FOREIGN KEY ("LocationId") REFERENCES public."Locations"("Id") ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict vPbXrNWUlquOeEJ5QzhfD8JnUNQKiT4WuaC9WX29tPKWN2DF69yiKlPnA6Wcagr + diff --git a/docker-compose.yml b/docker-compose.yml index c901201..56bf981 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,21 +6,21 @@ services: # ===== BASE DE DATOS ===== postgres: - image: postgres:16-alpine + image: postgres:17 container_name: parhelion-db restart: unless-stopped environment: - POSTGRES_DB: parhelion_db - POSTGRES_USER: parhelion_user - POSTGRES_PASSWORD: ${DB_PASSWORD:-parhelion_dev_123} + POSTGRES_DB: parhelion_dev + POSTGRES_USER: ${DB_USER:-MetaCodeX} + POSTGRES_PASSWORD: ${DB_PASSWORD:-H4NZC0D3X1521} volumes: - - postgres_data:/var/lib/postgresql/data + - postgres_pgdata:/var/lib/postgresql/data ports: - "5432:5432" networks: - parhelion-network healthcheck: - test: ["CMD-SHELL", "pg_isready -U parhelion_user -d parhelion_db"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-MetaCodeX} -d parhelion_dev"] interval: 10s timeout: 5s retries: 5 @@ -33,10 +33,12 @@ services: container_name: parhelion-api restart: unless-stopped ports: - - "5000:5000" + - "${BACKEND_PORT:-5100}:5000" environment: - ASPNETCORE_ENVIRONMENT=Production - - ConnectionStrings__DefaultConnection=Host=postgres;Database=parhelion_db;Username=parhelion_user;Password=${DB_PASSWORD:-parhelion_dev_123} + - ASPNETCORE_URLS=http://+:5000 + - ConnectionStrings__DefaultConnection=Host=postgres;Database=parhelion_dev;Username=${DB_USER:-MetaCodeX};Password=${DB_PASSWORD:-H4NZC0D3X1521} + - Jwt__Secret=${JWT_SECRET} depends_on: postgres: condition: service_healthy @@ -57,7 +59,7 @@ services: container_name: parhelion-admin restart: unless-stopped ports: - - "4200:80" + - "${ADMIN_PORT:-4100}:80" networks: - parhelion-network healthcheck: @@ -74,7 +76,7 @@ services: container_name: parhelion-operaciones restart: unless-stopped ports: - - "5173:80" + - "${OPERACIONES_PORT:-5101}:80" networks: - parhelion-network healthcheck: @@ -91,7 +93,7 @@ services: container_name: parhelion-campo restart: unless-stopped ports: - - "5174:80" + - "${CAMPO_PORT:-5102}:80" networks: - parhelion-network healthcheck: @@ -122,8 +124,9 @@ services: # ===== VOLUMES ===== volumes: - postgres_data: - name: parhelion-postgres-data + postgres_pgdata: + external: true + name: postgres_pgdata # ===== NETWORK ===== networks: From a32c903bd2ea07244a23432b6c2a3ad80aae07c2 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Wed, 17 Dec 2025 22:03:28 +0000 Subject: [PATCH 21/34] v0.5.2: Implementacion del Services Layer Implementacion de capa de Services para Clean Architecture. Agregado: - IGenericService como interfaz base con operaciones CRUD - Core Services: TenantService, UserService, RoleService, EmployeeService, ClientService - Shipment Services: ShipmentService, ShipmentItemService, ShipmentCheckpointService, ShipmentDocumentService, CatalogItemService - Fleet Services: DriverService, TruckService, FleetLogService - Network Services: LocationService, RouteService Refactorizacion: - 6 Controllers refactorizados para usar interfaces de Service - Registro de 15 Services en Dependency Injection (Program.cs) - Controllers reducidos a thin wrappers delegando a Services Documentacion: - CHANGELOG.md actualizado con detalles de v0.5.2 - README.md actualizado a v0.5.2 con checklist de Services Layer - api-architecture.md actualizado con tablas de Services por endpoint Verificacion: - Build exitoso sin errores ni warnings - 28/28 tests unitarios pasando --- v0.5.2: Services Layer Implementation Implementation of Services layer for Clean Architecture compliance. Added: - IGenericService as base interface with CRUD operations - Core Services: TenantService, UserService, RoleService, EmployeeService, ClientService - Shipment Services: ShipmentService, ShipmentItemService, ShipmentCheckpointService, ShipmentDocumentService, CatalogItemService - Fleet Services: DriverService, TruckService, FleetLogService - Network Services: LocationService, RouteService Refactored: - 6 Controllers refactored to use Service interfaces - 15 Services registered in Dependency Injection (Program.cs) - Controllers reduced to thin wrappers delegating to Services Documentation: - CHANGELOG.md updated with v0.5.2 details - README.md updated to v0.5.2 with Services Layer checklist - api-architecture.md updated with Service tables per endpoint Verification: - Build successful with no errors or warnings - 28/28 unit tests passing --- CHANGELOG.md | 41 +++ README.md | 3 +- api-architecture.md | 101 +++--- .../Controllers/ClientsController.cs | 273 +++++++++------- .../Controllers/EmployeesController.cs | 232 ++++++++------ .../Controllers/RolesController.cs | 177 +++++++---- .../Controllers/ShipmentsController.cs | 293 +++++++++-------- .../Controllers/TenantsController.cs | 189 ++++++----- .../Controllers/UsersController.cs | 227 ++++++++------ backend/src/Parhelion.API/Program.cs | 42 +++ .../Services/ICatalogItemService.cs | 74 +++++ .../Interfaces/Services/IClientService.cs | 73 +++++ .../Interfaces/Services/IDriverService.cs | 42 +++ .../Interfaces/Services/IEmployeeService.cs | 58 ++++ .../Interfaces/Services/IFleetLogService.cs | 46 +++ .../Interfaces/Services/IGenericService.cs | 78 +++++ .../Interfaces/Services/ILocationService.cs | 32 ++ .../Interfaces/Services/IRoleService.cs | 39 +++ .../Interfaces/Services/IRouteService.cs | 31 ++ .../Services/IShipmentCheckpointService.cs | 71 +++++ .../Services/IShipmentDocumentService.cs | 66 ++++ .../Services/IShipmentItemService.cs | 59 ++++ .../Interfaces/Services/IShipmentService.cs | 99 ++++++ .../Interfaces/Services/ITenantService.cs | 44 +++ .../Interfaces/Services/ITruckService.cs | 42 +++ .../Interfaces/Services/IUserService.cs | 69 ++++ .../Services/Core/ClientService.cs | 294 ++++++++++++++++++ .../Services/Core/EmployeeService.cs | 282 +++++++++++++++++ .../Services/Core/RoleService.cs | 183 +++++++++++ .../Services/Core/TenantService.cs | 206 ++++++++++++ .../Services/Core/UserService.cs | 291 +++++++++++++++++ .../Services/Fleet/DriverService.cs | 166 ++++++++++ .../Services/Fleet/FleetLogService.cs | 117 +++++++ .../Services/Fleet/TruckService.cs | 156 ++++++++++ .../Services/Network/LocationService.cs | 120 +++++++ .../Services/Network/RouteService.cs | 111 +++++++ .../Services/Shipment/CatalogItemService.cs | 127 ++++++++ .../Shipment/ShipmentCheckpointService.cs | 117 +++++++ .../Shipment/ShipmentDocumentService.cs | 79 +++++ .../Services/Shipment/ShipmentItemService.cs | 142 +++++++++ .../Services/Shipment/ShipmentService.cs | 215 +++++++++++++ 41 files changed, 4493 insertions(+), 614 deletions(-) create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/ICatalogItemService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IClientService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IDriverService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IEmployeeService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IFleetLogService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IGenericService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/ILocationService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IRoleService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IRouteService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IShipmentCheckpointService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IShipmentDocumentService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IShipmentItemService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IShipmentService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/ITenantService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/ITruckService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IUserService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Core/ClientService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Core/EmployeeService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Core/RoleService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Core/TenantService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Core/UserService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Fleet/FleetLogService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Fleet/TruckService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Network/LocationService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Network/RouteService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Shipment/CatalogItemService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentCheckpointService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentDocumentService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentItemService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d40c18d..879fa78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,47 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.5.2] - 2025-12-17 + +### Agregado + +- **Services Layer (16 interfaces, 15 implementaciones)**: + + - `IGenericService` - Interfaz base con operaciones CRUD genericas + - **Core Services**: TenantService, UserService, RoleService, EmployeeService, ClientService + - **Shipment Services**: ShipmentService, ShipmentItemService, ShipmentCheckpointService, ShipmentDocumentService, CatalogItemService + - **Fleet Services**: DriverService, TruckService, FleetLogService + - **Network Services**: LocationService, RouteService + +- **Refactorizacion de Controllers**: + + - TenantsController, UsersController, RolesController, EmployeesController, ClientsController, ShipmentsController + - Controllers ahora usan Service interfaces en lugar de acceso directo a DbContext + - Cumplimiento estricto de Clean Architecture + +- **Nuevos Endpoints en ShipmentsController**: + + - `GET /api/shipments/by-tracking/{trackingNumber}` - Busqueda por tracking number + - `GET /api/shipments/by-status/{status}` - Filtrado por estatus + - `GET /api/shipments/by-driver/{driverId}` - Envios por chofer + - `GET /api/shipments/by-location/{locationId}` - Envios por ubicacion + - `PATCH /api/shipments/{id}/assign` - Asignacion de chofer y camion + - `PATCH /api/shipments/{id}/status` - Actualizacion de estatus + +### Modificado + +- **Dependency Injection**: Registro de 15 Services en Program.cs organizado por capas +- **Program.cs**: Estructura clara con secciones Core, Shipment, Fleet, Network + +### Notas Tecnicas + +- Services Layer encapsula logica de negocio y validaciones +- Controllers reducidos a thin wrappers (delegacion a Services) +- IUnitOfWork se inyecta en Services para coordinacion de repositorios +- Preparacion para implementacion de tests de integracion en v0.5.3 + +--- + ## [0.5.1] - 2025-12-16 ### Agregado diff --git a/README.md b/README.md index a783eee..248db8e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. -> **Estado:** Development Preview v0.5.1 - Foundation Layer + Repository Pattern +> **Estado:** Development Preview v0.5.2 - Services Layer Implementation --- @@ -37,6 +37,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in - [x] **Autenticacion:** JWT con roles SuperAdmin/Admin/Driver/Warehouse - [x] **Repository Pattern:** GenericRepository + UnitOfWork + Soft Delete - [x] **xUnit Tests:** 28 tests de foundation (paginacion, CRUD) +- [x] **Services Layer:** 16 interfaces, 15 implementaciones (Core, Shipment, Fleet, Network) ### Gestion de Flotilla diff --git a/api-architecture.md b/api-architecture.md index e65da9f..2ffc1ad 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -4,8 +4,8 @@ Documentacion tecnica de la estructura API-First del backend Parhelion. ## Estado Actual -**Version:** 0.5.1 -**Enfoque:** API-First (Skeleton + Foundation Layer) +**Version:** 0.5.2 +**Enfoque:** Services Layer Implementation **Arquitectura:** Clean Architecture + Domain-Driven Design --- @@ -18,58 +18,82 @@ El backend esta organizado en 5 capas logicas que agrupan los endpoints segun su Gestion de identidad, usuarios y estructura organizacional. -| Endpoint | Entidad | Estado | -| ---------------- | -------- | -------- | -| `/api/tenants` | Tenant | Skeleton | -| `/api/users` | User | Skeleton | -| `/api/roles` | Role | Skeleton | -| `/api/employees` | Employee | Skeleton | -| `/api/clients` | Client | Skeleton | +| Endpoint | Entidad | Estado | Service | +| ---------------- | -------- | -------- | --------------- | +| `/api/tenants` | Tenant | Services | TenantService | +| `/api/users` | User | Services | UserService | +| `/api/roles` | Role | Services | RoleService | +| `/api/employees` | Employee | Services | EmployeeService | +| `/api/clients` | Client | Services | ClientService | ### Warehouse Layer Gestion de almacenes, zonas e inventario. -| Endpoint | Entidad | Estado | -| ----------------------------- | -------------------- | -------- | -| `/api/locations` | Location | Skeleton | -| `/api/warehouse-zones` | WarehouseZone | Skeleton | -| `/api/warehouse-operators` | WarehouseOperator | Skeleton | -| `/api/inventory-stocks` | InventoryStock | Skeleton | -| `/api/inventory-transactions` | InventoryTransaction | Skeleton | +| Endpoint | Entidad | Estado | Service | +| ----------------------------- | -------------------- | -------- | --------------- | +| `/api/locations` | Location | Services | LocationService | +| `/api/warehouse-zones` | WarehouseZone | Skeleton | - | +| `/api/warehouse-operators` | WarehouseOperator | Skeleton | - | +| `/api/inventory-stocks` | InventoryStock | Skeleton | - | +| `/api/inventory-transactions` | InventoryTransaction | Skeleton | - | ### Fleet Layer Gestion de flotilla, choferes y turnos. -| Endpoint | Entidad | Estado | -| ----------------- | -------- | -------- | -| `/api/trucks` | Truck | Skeleton | -| `/api/drivers` | Driver | Skeleton | -| `/api/shifts` | Shift | Skeleton | -| `/api/fleet-logs` | FleetLog | Skeleton | +| Endpoint | Entidad | Estado | Service | +| ----------------- | -------- | -------- | --------------- | +| `/api/trucks` | Truck | Services | TruckService | +| `/api/drivers` | Driver | Services | DriverService | +| `/api/shifts` | Shift | Skeleton | - | +| `/api/fleet-logs` | FleetLog | Services | FleetLogService | ### Shipment Layer Gestion de envios, items y trazabilidad. -| Endpoint | Entidad | Estado | -| --------------------------- | ------------------ | -------- | -| `/api/shipments` | Shipment | Skeleton | -| `/api/shipment-items` | ShipmentItem | Skeleton | -| `/api/shipment-checkpoints` | ShipmentCheckpoint | Skeleton | -| `/api/shipment-documents` | ShipmentDocument | Skeleton | -| `/api/catalog-items` | CatalogItem | Skeleton | +| Endpoint | Entidad | Estado | Service | +| --------------------------- | ------------------ | -------- | ------------------------- | +| `/api/shipments` | Shipment | Services | ShipmentService | +| `/api/shipment-items` | ShipmentItem | Services | ShipmentItemService | +| `/api/shipment-checkpoints` | ShipmentCheckpoint | Services | ShipmentCheckpointService | +| `/api/shipment-documents` | ShipmentDocument | Services | ShipmentDocumentService | +| `/api/catalog-items` | CatalogItem | Services | CatalogItemService | ### Network Layer Gestion de red logistica Hub and Spoke. -| Endpoint | Entidad | Estado | -| ----------------------- | -------------- | -------- | -| `/api/network-links` | NetworkLink | Skeleton | -| `/api/route-blueprints` | RouteBlueprint | Skeleton | -| `/api/route-steps` | RouteStep | Skeleton | +| Endpoint | Entidad | Estado | Service | +| ----------------------- | -------------- | -------- | ------------ | +| `/api/network-links` | NetworkLink | Skeleton | - | +| `/api/route-blueprints` | RouteBlueprint | Services | RouteService | +| `/api/route-steps` | RouteStep | Skeleton | - | + +--- + +## Services Layer (v0.5.2) + +Capa de servicios que encapsula logica de negocio. + +### Interfaces Base + +| Interfaz | Descripcion | +| -------------------- | ----------------------------------------- | +| `IGenericService` | CRUD generico con paginacion y DTOs | +| `ITenantService` | Extiende IGenericService para Tenants | +| `IUserService` | Validacion de credenciales, cambio passwd | +| `IShipmentService` | Tracking, asignacion, estatus | + +### Implementaciones por Capa + +| Capa | Services | +| -------- | ----------------------------------------------------- | +| Core | Tenant, User, Role, Employee, Client | +| Shipment | Shipment, ShipmentItem, Checkpoint, Document, Catalog | +| Fleet | Driver, Truck, FleetLog | +| Network | Location, Route | --- @@ -85,15 +109,6 @@ Infraestructura base para operaciones CRUD y transacciones. | `ITenantRepository` | `TenantRepository` | Filtrado automatico por TenantId | | `IUnitOfWork` | `UnitOfWork` | Coordinacion de transacciones | -### DTOs Comunes - -| DTO | Uso | -| ----------------- | ----------------------------------------- | -| `PagedRequest` | Paginacion con Sort, Search, ActiveOnly | -| `PagedResult` | Respuesta con TotalPages, HasNext, etc | -| `BaseDto` | Campos comunes (Id, CreatedAt, UpdatedAt) | -| `OperationResult` | Respuestas estandarizadas | - --- ## Autenticacion diff --git a/backend/src/Parhelion.API/Controllers/ClientsController.cs b/backend/src/Parhelion.API/Controllers/ClientsController.cs index 00e2949..050cf54 100644 --- a/backend/src/Parhelion.API/Controllers/ClientsController.cs +++ b/backend/src/Parhelion.API/Controllers/ClientsController.cs @@ -1,10 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Core; -using Parhelion.Domain.Entities; +using Parhelion.Application.Interfaces.Services; using Parhelion.Domain.Enums; -using Parhelion.Infrastructure.Data; namespace Parhelion.API.Controllers; @@ -16,157 +15,221 @@ namespace Parhelion.API.Controllers; [Authorize] public class ClientsController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IClientService _clientService; - public ClientsController(ParhelionDbContext context) + /// + /// Inicializa el controlador con el servicio de Clients. + /// + /// Servicio de gestión de clientes. + public ClientsController(IClientService clientService) { - _context = context; + _clientService = clientService; } /// - /// Obtiene todos los clientes del tenant. + /// Obtiene todos los clientes con paginación. /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de clientes. [HttpGet] - public async Task>> GetAll() + public async Task>> GetAll( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var items = await _context.Clients - .Where(x => !x.IsDeleted) - .OrderBy(x => x.CompanyName) - .Select(x => MapToResponse(x)) - .ToListAsync(); - - return Ok(items); + var result = await _clientService.GetAllAsync(request, cancellationToken); + return Ok(result); } /// /// Obtiene un cliente por ID. /// + /// ID del cliente. + /// Token de cancelación. + /// Cliente encontrado. [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task> GetById( + Guid id, + CancellationToken cancellationToken = default) + { + var item = await _clientService.GetByIdAsync(id, cancellationToken); + if (item == null) + return NotFound(new { error = "Cliente no encontrado" }); + + return Ok(item); + } + + /// + /// Busca un cliente por email. + /// + /// Email del cliente. + /// Token de cancelación. + /// Cliente encontrado. + [HttpGet("by-email")] + public async Task> GetByEmail( + [FromQuery] string email, + CancellationToken cancellationToken = default) { - var item = await _context.Clients - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + if (string.IsNullOrWhiteSpace(email)) + return BadRequest(new { error = "El parámetro 'email' es requerido" }); + var item = await _clientService.GetByEmailAsync(email, cancellationToken); if (item == null) return NotFound(new { error = "Cliente no encontrado" }); - return Ok(MapToResponse(item)); + return Ok(item); } /// - /// Busca clientes por nombre o email. + /// Busca un cliente por Tax ID (RFC). /// - [HttpGet("search")] - public async Task>> Search([FromQuery] string q) + /// RFC del cliente. + /// Token de cancelación. + /// Cliente encontrado. + [HttpGet("by-tax-id/{taxId}")] + public async Task> GetByTaxId( + string taxId, + CancellationToken cancellationToken = default) + { + var item = await _clientService.GetByTaxIdAsync(taxId, cancellationToken); + if (item == null) + return NotFound(new { error = "Cliente no encontrado" }); + + return Ok(item); + } + + /// + /// Obtiene clientes del tenant actual. + /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de clientes del tenant. + [HttpGet("current-tenant")] + public async Task>> GetByCurrentTenant( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - if (string.IsNullOrWhiteSpace(q)) - return BadRequest(new { error = "El parámetro 'q' es requerido" }); - - var query = q.ToLower(); - var items = await _context.Clients - .Where(x => !x.IsDeleted && - (x.CompanyName.ToLower().Contains(query) || - x.ContactName.ToLower().Contains(query) || - x.Email.ToLower().Contains(query))) - .OrderBy(x => x.CompanyName) - .Select(x => MapToResponse(x)) - .ToListAsync(); - - return Ok(items); + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _clientService.GetByTenantAsync(tenantId, request, cancellationToken); + return Ok(result); } /// - /// Crea un nuevo cliente. + /// Obtiene clientes por prioridad del tenant actual. /// - [HttpPost] - public async Task> Create([FromBody] CreateClientRequest request) + /// Prioridad del cliente (Normal, Low, High, Urgent). + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de clientes con la prioridad especificada. + [HttpGet("by-priority/{priority}")] + public async Task>> GetByPriority( + string priority, + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { + if (!Enum.TryParse(priority, out var clientPriority)) + return BadRequest(new { error = "Prioridad inválida" }); + var tenantIdClaim = User.FindFirst("tenant_id"); if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) return Unauthorized(new { error = "No se pudo determinar el tenant" }); - var item = new Client - { - Id = Guid.NewGuid(), - TenantId = tenantId, - CompanyName = request.CompanyName, - TradeName = request.TradeName, - ContactName = request.ContactName, - Email = request.Email, - Phone = request.Phone, - TaxId = request.TaxId, - LegalName = request.LegalName, - BillingAddress = request.BillingAddress, - ShippingAddress = request.ShippingAddress, - PreferredProductTypes = request.PreferredProductTypes, - Priority = Enum.TryParse(request.Priority, out var priority) - ? priority : ClientPriority.Normal, - Notes = request.Notes, - IsActive = true, - CreatedAt = DateTime.UtcNow - }; - - _context.Clients.Add(item); - await _context.SaveChangesAsync(); - - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + var result = await _clientService.GetByPriorityAsync( + tenantId, clientPriority, request, cancellationToken); + return Ok(result); + } + + /// + /// Busca clientes por nombre de empresa. + /// + /// Nombre parcial de la empresa. + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de clientes que coinciden. + [HttpGet("search")] + public async Task>> Search( + [FromQuery] string companyName, + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(companyName)) + return BadRequest(new { error = "El parámetro 'companyName' es requerido" }); + + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _clientService.SearchByCompanyNameAsync( + tenantId, companyName, request, cancellationToken); + return Ok(result); + } + + /// + /// Crea un nuevo cliente. + /// + /// Datos del nuevo cliente. + /// Token de cancelación. + /// Cliente creado. + [HttpPost] + public async Task> Create( + [FromBody] CreateClientRequest request, + CancellationToken cancellationToken = default) + { + var result = await _clientService.CreateAsync(request, cancellationToken); + + if (!result.Success) + return Conflict(new { error = result.Message }); + + return CreatedAtAction( + nameof(GetById), + new { id = result.Data!.Id }, + result.Data); } /// /// Actualiza un cliente existente. /// + /// ID del cliente. + /// Datos de actualización. + /// Token de cancelación. + /// Cliente actualizado. [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateClientRequest request) + public async Task> Update( + Guid id, + [FromBody] UpdateClientRequest request, + CancellationToken cancellationToken = default) { - var item = await _context.Clients - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - - if (item == null) - return NotFound(new { error = "Cliente no encontrado" }); + var result = await _clientService.UpdateAsync(id, request, cancellationToken); + + if (!result.Success) + { + if (result.Message?.Contains("no encontrado") == true) + return NotFound(new { error = result.Message }); + return Conflict(new { error = result.Message }); + } - item.CompanyName = request.CompanyName; - item.TradeName = request.TradeName; - item.ContactName = request.ContactName; - item.Email = request.Email; - item.Phone = request.Phone; - item.TaxId = request.TaxId; - item.LegalName = request.LegalName; - item.BillingAddress = request.BillingAddress; - item.ShippingAddress = request.ShippingAddress; - item.PreferredProductTypes = request.PreferredProductTypes; - item.Priority = Enum.TryParse(request.Priority, out var priority) - ? priority : ClientPriority.Normal; - item.IsActive = request.IsActive; - item.Notes = request.Notes; - item.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - return Ok(MapToResponse(item)); + return Ok(result.Data); } /// /// Elimina (soft-delete) un cliente. /// + /// ID del cliente. + /// Token de cancelación. + /// 204 No Content. [HttpDelete("{id:guid}")] - public async Task Delete(Guid id) + public async Task Delete( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Clients - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - - if (item == null) - return NotFound(new { error = "Cliente no encontrado" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _clientService.DeleteAsync(id, cancellationToken); + + if (!result.Success) + return NotFound(new { error = result.Message }); return NoContent(); } - - private static ClientResponse MapToResponse(Client x) => new( - x.Id, x.CompanyName, x.TradeName, x.ContactName, x.Email, x.Phone, - x.TaxId, x.LegalName, x.BillingAddress, x.ShippingAddress, - x.PreferredProductTypes, x.Priority.ToString(), x.IsActive, x.Notes, - x.CreatedAt, x.UpdatedAt - ); } diff --git a/backend/src/Parhelion.API/Controllers/EmployeesController.cs b/backend/src/Parhelion.API/Controllers/EmployeesController.cs index e4126fa..25cb322 100644 --- a/backend/src/Parhelion.API/Controllers/EmployeesController.cs +++ b/backend/src/Parhelion.API/Controllers/EmployeesController.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Core; -using Parhelion.Domain.Entities; -using Parhelion.Infrastructure.Data; +using Parhelion.Application.Interfaces.Services; namespace Parhelion.API.Controllers; @@ -15,151 +14,190 @@ namespace Parhelion.API.Controllers; [Authorize] public class EmployeesController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IEmployeeService _employeeService; - public EmployeesController(ParhelionDbContext context) + /// + /// Inicializa el controlador con el servicio de Employees. + /// + /// Servicio de gestión de empleados. + public EmployeesController(IEmployeeService employeeService) { - _context = context; + _employeeService = employeeService; } /// - /// Obtiene todos los empleados del tenant. + /// Obtiene todos los empleados con paginación. /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de empleados. [HttpGet] - public async Task>> GetAll() + public async Task>> GetAll( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var items = await _context.Employees - .Include(x => x.User) - .Where(x => !x.IsDeleted) - .OrderBy(x => x.User.FullName) - .Select(x => MapToResponse(x)) - .ToListAsync(); - - return Ok(items); + var result = await _employeeService.GetAllAsync(request, cancellationToken); + return Ok(result); } /// /// Obtiene un empleado por ID. /// + /// ID del empleado. + /// Token de cancelación. + /// Empleado encontrado. [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task> GetById( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Employees - .Include(x => x.User) - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + var item = await _employeeService.GetByIdAsync(id, cancellationToken); + if (item == null) + return NotFound(new { error = "Empleado no encontrado" }); + + return Ok(item); + } + /// + /// Obtiene un empleado por su User ID. + /// + /// ID del usuario asociado. + /// Token de cancelación. + /// Empleado encontrado. + [HttpGet("by-user/{userId:guid}")] + public async Task> GetByUserId( + Guid userId, + CancellationToken cancellationToken = default) + { + var item = await _employeeService.GetByUserIdAsync(userId, cancellationToken); if (item == null) return NotFound(new { error = "Empleado no encontrado" }); - return Ok(MapToResponse(item)); + return Ok(item); } /// - /// Obtiene empleados por departamento. + /// Busca un empleado por RFC. /// - [HttpGet("by-department/{department}")] - public async Task>> ByDepartment(string department) + /// RFC del empleado. + /// Token de cancelación. + /// Empleado encontrado. + [HttpGet("by-rfc/{rfc}")] + public async Task> GetByRfc( + string rfc, + CancellationToken cancellationToken = default) { - var items = await _context.Employees - .Include(x => x.User) - .Where(x => !x.IsDeleted && x.Department == department) - .OrderBy(x => x.User.FullName) - .Select(x => MapToResponse(x)) - .ToListAsync(); - - return Ok(items); + var item = await _employeeService.GetByRfcAsync(rfc, cancellationToken); + if (item == null) + return NotFound(new { error = "Empleado no encontrado" }); + + return Ok(item); } /// - /// Crea un nuevo empleado. + /// Obtiene empleados por departamento del tenant actual. /// - [HttpPost] - public async Task> Create([FromBody] CreateEmployeeRequest request) + /// Nombre del departamento. + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de empleados del departamento. + [HttpGet("by-department/{department}")] + public async Task>> ByDepartment( + string department, + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var existingUser = await _context.Employees - .AnyAsync(x => x.UserId == request.UserId && !x.IsDeleted); + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); - if (existingUser) - return Conflict(new { error = "Este usuario ya tiene un registro de empleado" }); + var result = await _employeeService.GetByDepartmentAsync( + tenantId, department, request, cancellationToken); + return Ok(result); + } + /// + /// Obtiene empleados del tenant actual. + /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de empleados del tenant. + [HttpGet("current-tenant")] + public async Task>> GetByCurrentTenant( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) + { var tenantIdClaim = User.FindFirst("tenant_id"); if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) return Unauthorized(new { error = "No se pudo determinar el tenant" }); - var item = new Employee - { - Id = Guid.NewGuid(), - TenantId = tenantId, - UserId = request.UserId, - Phone = request.Phone, - Rfc = request.Rfc, - Nss = request.Nss, - Curp = request.Curp, - EmergencyContact = request.EmergencyContact, - EmergencyPhone = request.EmergencyPhone, - HireDate = request.HireDate, - ShiftId = request.ShiftId, - Department = request.Department, - CreatedAt = DateTime.UtcNow - }; - - _context.Employees.Add(item); - await _context.SaveChangesAsync(); - - item = await _context.Employees.Include(x => x.User).FirstAsync(x => x.Id == item.Id); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + var result = await _employeeService.GetByTenantAsync(tenantId, request, cancellationToken); + return Ok(result); + } + + /// + /// Crea un nuevo empleado. + /// + /// Datos del nuevo empleado. + /// Token de cancelación. + /// Empleado creado. + [HttpPost] + public async Task> Create( + [FromBody] CreateEmployeeRequest request, + CancellationToken cancellationToken = default) + { + var result = await _employeeService.CreateAsync(request, cancellationToken); + + if (!result.Success) + return Conflict(new { error = result.Message }); + + return CreatedAtAction( + nameof(GetById), + new { id = result.Data!.Id }, + result.Data); } /// /// Actualiza un empleado existente. /// + /// ID del empleado. + /// Datos de actualización. + /// Token de cancelación. + /// Empleado actualizado. [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateEmployeeRequest request) + public async Task> Update( + Guid id, + [FromBody] UpdateEmployeeRequest request, + CancellationToken cancellationToken = default) { - var item = await _context.Employees - .Include(x => x.User) - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - - if (item == null) - return NotFound(new { error = "Empleado no encontrado" }); + var result = await _employeeService.UpdateAsync(id, request, cancellationToken); + + if (!result.Success) + { + if (result.Message?.Contains("no encontrado") == true) + return NotFound(new { error = result.Message }); + return Conflict(new { error = result.Message }); + } - item.Phone = request.Phone; - item.Rfc = request.Rfc; - item.Nss = request.Nss; - item.Curp = request.Curp; - item.EmergencyContact = request.EmergencyContact; - item.EmergencyPhone = request.EmergencyPhone; - item.HireDate = request.HireDate; - item.ShiftId = request.ShiftId; - item.Department = request.Department; - item.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - return Ok(MapToResponse(item)); + return Ok(result.Data); } /// /// Elimina (soft-delete) un empleado. /// + /// ID del empleado. + /// Token de cancelación. + /// 204 No Content. [HttpDelete("{id:guid}")] - public async Task Delete(Guid id) + public async Task Delete( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Employees - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - - if (item == null) - return NotFound(new { error = "Empleado no encontrado" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _employeeService.DeleteAsync(id, cancellationToken); + + if (!result.Success) + return NotFound(new { error = result.Message }); return NoContent(); } - - private static EmployeeResponse MapToResponse(Employee x) => new( - x.Id, x.UserId, x.User?.FullName ?? "", x.User?.Email ?? "", - x.Phone, x.Rfc, x.Nss, x.Curp, x.EmergencyContact, x.EmergencyPhone, - x.HireDate, x.ShiftId, x.Department, x.CreatedAt, x.UpdatedAt - ); } diff --git a/backend/src/Parhelion.API/Controllers/RolesController.cs b/backend/src/Parhelion.API/Controllers/RolesController.cs index 82e8e39..411d92a 100644 --- a/backend/src/Parhelion.API/Controllers/RolesController.cs +++ b/backend/src/Parhelion.API/Controllers/RolesController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Core; -using Parhelion.Domain.Entities; -using Parhelion.Infrastructure.Data; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Enums; namespace Parhelion.API.Controllers; @@ -16,105 +16,162 @@ namespace Parhelion.API.Controllers; [Authorize] public class RolesController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IRoleService _roleService; - public RolesController(ParhelionDbContext context) + /// + /// Inicializa el controlador con el servicio de Roles. + /// + /// Servicio de gestión de roles. + public RolesController(IRoleService roleService) { - _context = context; + _roleService = roleService; } /// - /// Obtiene todos los roles. + /// Obtiene todos los roles con paginación. /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de roles. [HttpGet] - public async Task>> GetAll() + public async Task>> GetAll( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var items = await _context.Roles - .Where(x => !x.IsDeleted) - .OrderBy(x => x.Name) - .Select(x => new RoleResponse(x.Id, x.Name, x.Description, x.CreatedAt)) - .ToListAsync(); - - return Ok(items); + var result = await _roleService.GetAllAsync(request, cancellationToken); + return Ok(result); } /// /// Obtiene un rol por ID. /// + /// ID del rol. + /// Token de cancelación. + /// Rol encontrado. [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task> GetById( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Roles - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - + var item = await _roleService.GetByIdAsync(id, cancellationToken); if (item == null) return NotFound(new { error = "Rol no encontrado" }); - return Ok(new RoleResponse(item.Id, item.Name, item.Description, item.CreatedAt)); + return Ok(item); } /// - /// Crea un nuevo rol. + /// Busca un rol por nombre. /// - [HttpPost] - public async Task> Create([FromBody] CreateRoleRequest request) + /// Nombre del rol. + /// Token de cancelación. + /// Rol encontrado. + [HttpGet("by-name/{name}")] + public async Task> GetByName( + string name, + CancellationToken cancellationToken = default) { - var existing = await _context.Roles - .AnyAsync(x => x.Name == request.Name && !x.IsDeleted); + var item = await _roleService.GetByNameAsync(name, cancellationToken); + if (item == null) + return NotFound(new { error = "Rol no encontrado" }); - if (existing) - return Conflict(new { error = "Ya existe un rol con ese nombre" }); + return Ok(item); + } - var item = new Role - { - Id = Guid.NewGuid(), - Name = request.Name, - Description = request.Description, - CreatedAt = DateTime.UtcNow - }; + /// + /// Obtiene los permisos de un rol. + /// + /// Nombre del rol. + /// Lista de permisos del rol. + [HttpGet("{name}/permissions")] + public ActionResult> GetPermissions(string name) + { + var permissions = _roleService.GetPermissions(name); + return Ok(permissions.Select(p => p.ToString())); + } + + /// + /// Verifica si un rol tiene un permiso específico. + /// + /// Nombre del rol. + /// Permiso a verificar. + /// True si el rol tiene el permiso. + [HttpGet("{name}/has-permission/{permission}")] + public ActionResult HasPermission(string name, string permission) + { + if (!Enum.TryParse(permission, out var perm)) + return BadRequest(new { error = "Permiso inválido" }); - _context.Roles.Add(item); - await _context.SaveChangesAsync(); + var hasPermission = _roleService.HasPermission(name, perm); + return Ok(new { hasPermission }); + } - return CreatedAtAction(nameof(GetById), new { id = item.Id }, - new RoleResponse(item.Id, item.Name, item.Description, item.CreatedAt)); + /// + /// Crea un nuevo rol. + /// + /// Datos del nuevo rol. + /// Token de cancelación. + /// Rol creado. + [HttpPost] + public async Task> Create( + [FromBody] CreateRoleRequest request, + CancellationToken cancellationToken = default) + { + var result = await _roleService.CreateAsync(request, cancellationToken); + + if (!result.Success) + return Conflict(new { error = result.Message }); + + return CreatedAtAction( + nameof(GetById), + new { id = result.Data!.Id }, + result.Data); } /// /// Actualiza un rol existente. /// + /// ID del rol. + /// Datos de actualización. + /// Token de cancelación. + /// Rol actualizado. [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateRoleRequest request) + public async Task> Update( + Guid id, + [FromBody] UpdateRoleRequest request, + CancellationToken cancellationToken = default) { - var item = await _context.Roles - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - - if (item == null) - return NotFound(new { error = "Rol no encontrado" }); - - item.Name = request.Name; - item.Description = request.Description; - item.UpdatedAt = DateTime.UtcNow; + var result = await _roleService.UpdateAsync(id, request, cancellationToken); + + if (!result.Success) + { + if (result.Message?.Contains("no encontrado") == true) + return NotFound(new { error = result.Message }); + return Conflict(new { error = result.Message }); + } - await _context.SaveChangesAsync(); - return Ok(new RoleResponse(item.Id, item.Name, item.Description, item.CreatedAt)); + return Ok(result.Data); } /// /// Elimina (soft-delete) un rol. /// + /// ID del rol. + /// Token de cancelación. + /// 204 No Content. [HttpDelete("{id:guid}")] - public async Task Delete(Guid id) + public async Task Delete( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Roles - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - - if (item == null) - return NotFound(new { error = "Rol no encontrado" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _roleService.DeleteAsync(id, cancellationToken); + + if (!result.Success) + { + if (result.Message?.Contains("usuarios asignados") == true) + return Conflict(new { error = result.Message }); + return NotFound(new { error = result.Message }); + } return NoContent(); } diff --git a/backend/src/Parhelion.API/Controllers/ShipmentsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentsController.cs index 0cf2028..bab2d3a 100644 --- a/backend/src/Parhelion.API/Controllers/ShipmentsController.cs +++ b/backend/src/Parhelion.API/Controllers/ShipmentsController.cs @@ -1,10 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Shipment; -using Parhelion.Domain.Entities; +using Parhelion.Application.Interfaces.Services; using Parhelion.Domain.Enums; -using Parhelion.Infrastructure.Data; namespace Parhelion.API.Controllers; @@ -16,167 +15,211 @@ namespace Parhelion.API.Controllers; [Authorize] public class ShipmentsController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IShipmentService _shipmentService; - public ShipmentsController(ParhelionDbContext context) + /// + /// Inicializa el controlador con el servicio de Shipments. + /// + public ShipmentsController(IShipmentService shipmentService) { - _context = context; + _shipmentService = shipmentService; } + /// + /// Obtiene todos los envíos con paginación. + /// [HttpGet] - public async Task>> GetAll() + public async Task>> GetAll( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var items = await _context.Shipments - .Include(x => x.OriginLocation) - .Include(x => x.DestinationLocation) - .Include(x => x.Sender) - .Include(x => x.RecipientClient) - .Include(x => x.Truck) - .Include(x => x.Driver).ThenInclude(d => d!.Employee).ThenInclude(e => e.User) - .Where(x => !x.IsDeleted) - .OrderByDescending(x => x.CreatedAt) - .Take(100) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _shipmentService.GetAllAsync(request, cancellationToken); + return Ok(result); } + /// + /// Obtiene un envío por ID. + /// [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task> GetById( + Guid id, + CancellationToken cancellationToken = default) { - var item = await GetShipmentWithIncludes().FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Envío no encontrado" }); - return Ok(MapToResponse(item)); + var item = await _shipmentService.GetByIdAsync(id, cancellationToken); + if (item == null) + return NotFound(new { error = "Envío no encontrado" }); + + return Ok(item); } + /// + /// Busca un envío por número de tracking. + /// [HttpGet("by-tracking/{trackingNumber}")] - public async Task> ByTracking(string trackingNumber) + public async Task> ByTracking( + string trackingNumber, + CancellationToken cancellationToken = default) { - var item = await GetShipmentWithIncludes() - .FirstOrDefaultAsync(x => x.TrackingNumber == trackingNumber && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Envío no encontrado" }); - return Ok(MapToResponse(item)); + var item = await _shipmentService.GetByTrackingNumberAsync(trackingNumber, cancellationToken); + if (item == null) + return NotFound(new { error = "Envío no encontrado" }); + + return Ok(item); } + /// + /// Obtiene envíos por estatus. + /// [HttpGet("by-status/{status}")] - public async Task>> ByStatus(string status) + public async Task>> ByStatus( + string status, + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { if (!Enum.TryParse(status, out var shipmentStatus)) return BadRequest(new { error = "Estatus inválido" }); - var items = await GetShipmentWithIncludes() - .Where(x => !x.IsDeleted && x.Status == shipmentStatus) - .OrderByDescending(x => x.CreatedAt) - .Take(100) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _shipmentService.GetByStatusAsync(tenantId, shipmentStatus, request, cancellationToken); + return Ok(result); } - [HttpPost] - public async Task> Create([FromBody] CreateShipmentRequest request) + /// + /// Obtiene envíos del tenant actual. + /// + [HttpGet("current-tenant")] + public async Task>> GetByCurrentTenant( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { var tenantIdClaim = User.FindFirst("tenant_id"); if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) return Unauthorized(new { error = "No se pudo determinar el tenant" }); - var trackingNumber = GenerateTrackingNumber(); - var item = new Domain.Entities.Shipment - { - Id = Guid.NewGuid(), - TenantId = tenantId, - TrackingNumber = trackingNumber, - QrCodeData = $"PAR:{trackingNumber}", - OriginLocationId = request.OriginLocationId, - DestinationLocationId = request.DestinationLocationId, - SenderId = request.SenderId, - RecipientClientId = request.RecipientClientId, - RecipientName = request.RecipientName, - RecipientPhone = request.RecipientPhone, - TotalWeightKg = request.TotalWeightKg, - TotalVolumeM3 = request.TotalVolumeM3, - DeclaredValue = request.DeclaredValue, - SatMerchandiseCode = request.SatMerchandiseCode, - DeliveryInstructions = request.DeliveryInstructions, - Priority = Enum.TryParse(request.Priority, out var p) ? p : ShipmentPriority.Normal, - Status = ShipmentStatus.PendingApproval, - CreatedAt = DateTime.UtcNow - }; - - _context.Shipments.Add(item); - await _context.SaveChangesAsync(); - - item = await GetShipmentWithIncludes().FirstAsync(x => x.Id == item.Id); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + var result = await _shipmentService.GetByTenantAsync(tenantId, request, cancellationToken); + return Ok(result); } - [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateShipmentRequest request) + /// + /// Obtiene envíos asignados a un chofer. + /// + [HttpGet("by-driver/{driverId:guid}")] + public async Task>> ByDriver( + Guid driverId, + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var item = await GetShipmentWithIncludes().FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Envío no encontrado" }); - - item.AssignedRouteId = request.AssignedRouteId; - item.CurrentStepOrder = request.CurrentStepOrder; - item.DeliveryInstructions = request.DeliveryInstructions; - item.Priority = Enum.TryParse(request.Priority, out var p) ? p : item.Priority; - item.Status = Enum.TryParse(request.Status, out var s) ? s : item.Status; - item.TruckId = request.TruckId; - item.DriverId = request.DriverId; - item.WasQrScanned = request.WasQrScanned; - item.IsDelayed = request.IsDelayed; - item.ScheduledDeparture = request.ScheduledDeparture; - item.PickupWindowStart = request.PickupWindowStart; - item.PickupWindowEnd = request.PickupWindowEnd; - item.EstimatedArrival = request.EstimatedArrival; - item.AssignedAt = request.AssignedAt; - item.DeliveredAt = request.DeliveredAt; - item.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - return Ok(MapToResponse(item)); + var result = await _shipmentService.GetByDriverAsync(driverId, request, cancellationToken); + return Ok(result); } - [HttpDelete("{id:guid}")] - public async Task Delete(Guid id) + /// + /// Obtiene envíos por ubicación. + /// + [HttpGet("by-location/{locationId:guid}")] + public async Task>> ByLocation( + Guid locationId, + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var item = await _context.Shipments.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Envío no encontrado" }); + var result = await _shipmentService.GetByLocationAsync(locationId, request, cancellationToken); + return Ok(result); + } - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); - return NoContent(); + /// + /// Crea un nuevo envío. + /// + [HttpPost] + public async Task> Create( + [FromBody] CreateShipmentRequest request, + CancellationToken cancellationToken = default) + { + var result = await _shipmentService.CreateAsync(request, cancellationToken); + + if (!result.Success) + return Conflict(new { error = result.Message }); + + return CreatedAtAction( + nameof(GetById), + new { id = result.Data!.Id }, + result.Data); } - private IQueryable GetShipmentWithIncludes() => _context.Shipments - .Include(x => x.OriginLocation) - .Include(x => x.DestinationLocation) - .Include(x => x.Sender) - .Include(x => x.RecipientClient) - .Include(x => x.Truck) - .Include(x => x.Driver).ThenInclude(d => d!.Employee).ThenInclude(e => e.User); + /// + /// Actualiza un envío existente. + /// + [HttpPut("{id:guid}")] + public async Task> Update( + Guid id, + [FromBody] UpdateShipmentRequest request, + CancellationToken cancellationToken = default) + { + var result = await _shipmentService.UpdateAsync(id, request, cancellationToken); + + if (!result.Success) + { + if (result.Message?.Contains("no encontrado") == true) + return NotFound(new { error = result.Message }); + return Conflict(new { error = result.Message }); + } + + return Ok(result.Data); + } - private static string GenerateTrackingNumber() + /// + /// Asigna un envío a un chofer y camión. + /// + [HttpPatch("{id:guid}/assign")] + public async Task> AssignToDriver( + Guid id, + [FromQuery] Guid driverId, + [FromQuery] Guid truckId, + CancellationToken cancellationToken = default) { - var random = new Random(); - return $"PAR-{random.Next(100000, 999999)}"; + var result = await _shipmentService.AssignToDriverAsync(id, driverId, truckId, cancellationToken); + + if (!result.Success) + return BadRequest(new { error = result.Message }); + + return Ok(result.Data); } - private static ShipmentResponse MapToResponse(Domain.Entities.Shipment x) => new( - x.Id, x.TrackingNumber, x.QrCodeData, - x.OriginLocationId, x.OriginLocation?.Name ?? "", - x.DestinationLocationId, x.DestinationLocation?.Name ?? "", - x.SenderId, x.Sender?.CompanyName, - x.RecipientClientId, x.RecipientClient?.CompanyName, - x.RecipientName, x.RecipientPhone, - x.TotalWeightKg, x.TotalVolumeM3, x.DeclaredValue, - x.SatMerchandiseCode, x.DeliveryInstructions, - x.Priority.ToString(), x.Status.ToString(), - x.TruckId, x.Truck?.Plate, - x.DriverId, x.Driver?.Employee?.User?.FullName, - x.WasQrScanned, x.IsDelayed, - x.ScheduledDeparture, x.EstimatedArrival, x.DeliveredAt, - x.CreatedAt, x.UpdatedAt - ); + /// + /// Actualiza el estatus de un envío. + /// + [HttpPatch("{id:guid}/status")] + public async Task> UpdateStatus( + Guid id, + [FromQuery] string status, + CancellationToken cancellationToken = default) + { + if (!Enum.TryParse(status, out var newStatus)) + return BadRequest(new { error = "Estatus inválido" }); + + var result = await _shipmentService.UpdateStatusAsync(id, newStatus, cancellationToken); + + if (!result.Success) + return BadRequest(new { error = result.Message }); + + return Ok(result.Data); + } + + /// + /// Elimina (soft-delete) un envío. + /// + [HttpDelete("{id:guid}")] + public async Task Delete( + Guid id, + CancellationToken cancellationToken = default) + { + var result = await _shipmentService.DeleteAsync(id, cancellationToken); + + if (!result.Success) + return NotFound(new { error = result.Message }); + + return NoContent(); + } } diff --git a/backend/src/Parhelion.API/Controllers/TenantsController.cs b/backend/src/Parhelion.API/Controllers/TenantsController.cs index 1b7bd07..05758ba 100644 --- a/backend/src/Parhelion.API/Controllers/TenantsController.cs +++ b/backend/src/Parhelion.API/Controllers/TenantsController.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Core; -using Parhelion.Domain.Entities; -using Parhelion.Infrastructure.Data; +using Parhelion.Application.Interfaces.Services; namespace Parhelion.API.Controllers; @@ -16,34 +15,40 @@ namespace Parhelion.API.Controllers; [Authorize] public class TenantsController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly ITenantService _tenantService; - public TenantsController(ParhelionDbContext context) + /// + /// Inicializa el controlador con el servicio de Tenants. + /// + /// Servicio de gestión de tenants. + public TenantsController(ITenantService tenantService) { - _context = context; + _tenantService = tenantService; } /// - /// Obtiene todos los tenants (solo Super Admin). + /// Obtiene todos los tenants con paginación. /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de tenants. [HttpGet] - public async Task>> GetAll() + public async Task>> GetAll( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var items = await _context.Tenants - .IgnoreQueryFilters() - .Where(x => !x.IsDeleted) - .OrderBy(x => x.CompanyName) - .Select(x => MapToResponse(x)) - .ToListAsync(); - - return Ok(items); + var result = await _tenantService.GetAllAsync(request, cancellationToken); + return Ok(result); } /// /// Obtiene el tenant actual del usuario logueado. /// + /// Token de cancelación. + /// Tenant actual. [HttpGet("current")] - public async Task> GetCurrent() + public async Task> GetCurrent( + CancellationToken cancellationToken = default) { var tenantIdClaim = User.FindFirst("tenant_id"); if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) @@ -51,108 +56,130 @@ public async Task> GetCurrent() return Unauthorized(new { error = "No se pudo determinar el tenant" }); } - var tenant = await _context.Tenants - .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.Id == tenantId && !x.IsDeleted); - + var tenant = await _tenantService.GetByIdAsync(tenantId, cancellationToken); if (tenant == null) return NotFound(new { error = "Tenant no encontrado" }); - return Ok(MapToResponse(tenant)); + return Ok(tenant); } /// /// Obtiene un tenant por ID. /// + /// ID del tenant. + /// Token de cancelación. + /// Tenant encontrado. [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task> GetById( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Tenants - .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - + var item = await _tenantService.GetByIdAsync(id, cancellationToken); if (item == null) return NotFound(new { error = "Tenant no encontrado" }); - return Ok(MapToResponse(item)); + return Ok(item); + } + + /// + /// Obtiene solo los tenants activos. + /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de tenants activos. + [HttpGet("active")] + public async Task>> GetActive( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) + { + var result = await _tenantService.GetActiveAsync(request, cancellationToken); + return Ok(result); } /// /// Crea un nuevo tenant. /// + /// Datos del nuevo tenant. + /// Token de cancelación. + /// Tenant creado. [HttpPost] - public async Task> Create([FromBody] CreateTenantRequest request) + public async Task> Create( + [FromBody] CreateTenantRequest request, + CancellationToken cancellationToken = default) { - var existing = await _context.Tenants - .IgnoreQueryFilters() - .AnyAsync(x => x.ContactEmail == request.ContactEmail && !x.IsDeleted); - - if (existing) - return Conflict(new { error = "Ya existe un tenant con ese email" }); - - var item = new Tenant - { - Id = Guid.NewGuid(), - CompanyName = request.CompanyName, - ContactEmail = request.ContactEmail, - FleetSize = request.FleetSize, - DriverCount = request.DriverCount, - IsActive = true, - CreatedAt = DateTime.UtcNow - }; - - _context.Tenants.Add(item); - await _context.SaveChangesAsync(); - - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + var result = await _tenantService.CreateAsync(request, cancellationToken); + + if (!result.Success) + return Conflict(new { error = result.Message }); + + return CreatedAtAction( + nameof(GetById), + new { id = result.Data!.Id }, + result.Data); } /// /// Actualiza un tenant existente. /// + /// ID del tenant. + /// Datos de actualización. + /// Token de cancelación. + /// Tenant actualizado. [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateTenantRequest request) + public async Task> Update( + Guid id, + [FromBody] UpdateTenantRequest request, + CancellationToken cancellationToken = default) { - var item = await _context.Tenants - .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + var result = await _tenantService.UpdateAsync(id, request, cancellationToken); + + if (!result.Success) + { + if (result.Message?.Contains("no encontrado") == true) + return NotFound(new { error = result.Message }); + return Conflict(new { error = result.Message }); + } - if (item == null) - return NotFound(new { error = "Tenant no encontrado" }); + return Ok(result.Data); + } - item.CompanyName = request.CompanyName; - item.ContactEmail = request.ContactEmail; - item.FleetSize = request.FleetSize; - item.DriverCount = request.DriverCount; - item.IsActive = request.IsActive; - item.UpdatedAt = DateTime.UtcNow; + /// + /// Activa o desactiva un tenant. + /// + /// ID del tenant. + /// Estado deseado. + /// Token de cancelación. + /// Resultado de la operación. + [HttpPatch("{id:guid}/status")] + public async Task SetStatus( + Guid id, + [FromQuery] bool isActive, + CancellationToken cancellationToken = default) + { + var result = await _tenantService.SetActiveStatusAsync(id, isActive, cancellationToken); + + if (!result.Success) + return NotFound(new { error = result.Message }); - await _context.SaveChangesAsync(); - return Ok(MapToResponse(item)); + return Ok(new { message = result.Message }); } /// /// Elimina (soft-delete) un tenant. /// + /// ID del tenant. + /// Token de cancelación. + /// 204 No Content. [HttpDelete("{id:guid}")] - public async Task Delete(Guid id) + public async Task Delete( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Tenants - .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - - if (item == null) - return NotFound(new { error = "Tenant no encontrado" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _tenantService.DeleteAsync(id, cancellationToken); + + if (!result.Success) + return NotFound(new { error = result.Message }); return NoContent(); } - - private static TenantResponse MapToResponse(Tenant x) => new( - x.Id, x.CompanyName, x.ContactEmail, x.FleetSize, x.DriverCount, - x.IsActive, x.CreatedAt, x.UpdatedAt - ); } diff --git a/backend/src/Parhelion.API/Controllers/UsersController.cs b/backend/src/Parhelion.API/Controllers/UsersController.cs index 9d97c02..8ef7c66 100644 --- a/backend/src/Parhelion.API/Controllers/UsersController.cs +++ b/backend/src/Parhelion.API/Controllers/UsersController.cs @@ -1,10 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Parhelion.Application.Auth; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Core; -using Parhelion.Domain.Entities; -using Parhelion.Infrastructure.Data; +using Parhelion.Application.Interfaces.Services; namespace Parhelion.API.Controllers; @@ -16,154 +14,179 @@ namespace Parhelion.API.Controllers; [Authorize] public class UsersController : ControllerBase { - private readonly ParhelionDbContext _context; - private readonly IPasswordHasher _passwordHasher; + private readonly IUserService _userService; - public UsersController(ParhelionDbContext context, IPasswordHasher passwordHasher) + /// + /// Inicializa el controlador con el servicio de Users. + /// + /// Servicio de gestión de usuarios. + public UsersController(IUserService userService) { - _context = context; - _passwordHasher = passwordHasher; + _userService = userService; } /// - /// Obtiene todos los usuarios del tenant. + /// Obtiene todos los usuarios con paginación. /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de usuarios. [HttpGet] - public async Task>> GetAll() + public async Task>> GetAll( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var items = await _context.Users - .Include(x => x.Role) - .Where(x => !x.IsDeleted) - .OrderBy(x => x.FullName) - .Select(x => MapToResponse(x)) - .ToListAsync(); - - return Ok(items); + var result = await _userService.GetAllAsync(request, cancellationToken); + return Ok(result); } /// /// Obtiene un usuario por ID. /// + /// ID del usuario. + /// Token de cancelación. + /// Usuario encontrado. [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task> GetById( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Users - .Include(x => x.Role) - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - + var item = await _userService.GetByIdAsync(id, cancellationToken); if (item == null) return NotFound(new { error = "Usuario no encontrado" }); - return Ok(MapToResponse(item)); + return Ok(item); } /// - /// Busca usuarios por nombre o email. + /// Obtiene usuarios del tenant actual. /// - [HttpGet("search")] - public async Task>> Search([FromQuery] string q) + /// Parámetros de paginación. + /// Token de cancelación. + /// Lista paginada de usuarios del tenant. + [HttpGet("current-tenant")] + public async Task>> GetByCurrentTenant( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - if (string.IsNullOrWhiteSpace(q)) - return BadRequest(new { error = "El parámetro 'q' es requerido" }); - - var query = q.ToLower(); - var items = await _context.Users - .Include(x => x.Role) - .Where(x => !x.IsDeleted && - (x.FullName.ToLower().Contains(query) || x.Email.ToLower().Contains(query))) - .OrderBy(x => x.FullName) - .Select(x => MapToResponse(x)) - .ToListAsync(); - - return Ok(items); + var tenantIdClaim = User.FindFirst("tenant_id"); + if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _userService.GetByTenantAsync(tenantId, request, cancellationToken); + return Ok(result); } /// - /// Crea un nuevo usuario. + /// Busca un usuario por email. /// - [HttpPost] - public async Task> Create([FromBody] CreateUserRequest request) + /// Email del usuario. + /// Token de cancelación. + /// Usuario encontrado. + [HttpGet("by-email")] + public async Task> GetByEmail( + [FromQuery] string email, + CancellationToken cancellationToken = default) { - var existingEmail = await _context.Users - .AnyAsync(x => x.Email == request.Email && !x.IsDeleted); + if (string.IsNullOrWhiteSpace(email)) + return BadRequest(new { error = "El parámetro 'email' es requerido" }); - if (existingEmail) - return Conflict(new { error = "Ya existe un usuario con ese email" }); + var item = await _userService.GetByEmailAsync(email, cancellationToken); + if (item == null) + return NotFound(new { error = "Usuario no encontrado" }); - var tenantIdClaim = User.FindFirst("tenant_id"); - if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) - return Unauthorized(new { error = "No se pudo determinar el tenant" }); + return Ok(item); + } - var item = new User - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Email = request.Email, - PasswordHash = _passwordHasher.HashPassword(request.Password), - FullName = request.FullName, - RoleId = request.RoleId, - IsDemoUser = request.IsDemoUser, - UsesArgon2 = false, - IsSuperAdmin = false, - IsActive = true, - CreatedAt = DateTime.UtcNow - }; - - _context.Users.Add(item); - await _context.SaveChangesAsync(); - - // Reload with Role - item = await _context.Users.Include(x => x.Role).FirstAsync(x => x.Id == item.Id); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + /// + /// Crea un nuevo usuario. + /// + /// Datos del nuevo usuario. + /// Token de cancelación. + /// Usuario creado. + [HttpPost] + public async Task> Create( + [FromBody] CreateUserRequest request, + CancellationToken cancellationToken = default) + { + var result = await _userService.CreateAsync(request, cancellationToken); + + if (!result.Success) + return Conflict(new { error = result.Message }); + + return CreatedAtAction( + nameof(GetById), + new { id = result.Data!.Id }, + result.Data); } /// /// Actualiza un usuario existente. /// + /// ID del usuario. + /// Datos de actualización. + /// Token de cancelación. + /// Usuario actualizado. [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateUserRequest request) + public async Task> Update( + Guid id, + [FromBody] UpdateUserRequest request, + CancellationToken cancellationToken = default) { - var item = await _context.Users - .Include(x => x.Role) - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); + var result = await _userService.UpdateAsync(id, request, cancellationToken); + + if (!result.Success) + { + if (result.Message?.Contains("no encontrado") == true) + return NotFound(new { error = result.Message }); + return Conflict(new { error = result.Message }); + } - if (item == null) - return NotFound(new { error = "Usuario no encontrado" }); + return Ok(result.Data); + } - item.FullName = request.FullName; - item.RoleId = request.RoleId; - item.IsActive = request.IsActive; - item.UpdatedAt = DateTime.UtcNow; + /// + /// Cambia el password del usuario actual. + /// + /// Password actual. + /// Nuevo password. + /// Token de cancelación. + /// Resultado de la operación. + [HttpPatch("change-password")] + public async Task ChangePassword( + [FromQuery] string currentPassword, + [FromQuery] string newPassword, + CancellationToken cancellationToken = default) + { + var userIdClaim = User.FindFirst("sub") ?? User.FindFirst("user_id"); + if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId)) + return Unauthorized(new { error = "No se pudo determinar el usuario" }); - await _context.SaveChangesAsync(); + var result = await _userService.ChangePasswordAsync( + userId, currentPassword, newPassword, cancellationToken); + + if (!result.Success) + return BadRequest(new { error = result.Message }); - // Reload with updated Role - item = await _context.Users.Include(x => x.Role).FirstAsync(x => x.Id == item.Id); - return Ok(MapToResponse(item)); + return Ok(new { message = result.Message }); } /// /// Elimina (soft-delete) un usuario. /// + /// ID del usuario. + /// Token de cancelación. + /// 204 No Content. [HttpDelete("{id:guid}")] - public async Task Delete(Guid id) + public async Task Delete( + Guid id, + CancellationToken cancellationToken = default) { - var item = await _context.Users - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - - if (item == null) - return NotFound(new { error = "Usuario no encontrado" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _userService.DeleteAsync(id, cancellationToken); + + if (!result.Success) + return NotFound(new { error = result.Message }); return NoContent(); } - - private static UserResponse MapToResponse(User x) => new( - x.Id, x.Email, x.FullName, x.RoleId, x.Role?.Name ?? "", - x.IsDemoUser, x.IsSuperAdmin, x.LastLogin, x.IsActive, - x.CreatedAt, x.UpdatedAt - ); } diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index 7c25627..dc96f45 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -41,6 +41,48 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// ========== REPOSITORY PATTERN ========== +builder.Services.AddScoped(); + +// ========== CORE LAYER SERVICES ========== +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// ========== SHIPMENT LAYER SERVICES ========== +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// ========== FLEET LAYER SERVICES ========== +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// ========== NETWORK LAYER SERVICES ========== +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // ========== JWT AUTHENTICATION ========== var jwtSecretKey = builder.Configuration["Jwt:SecretKey"] ?? "ParhelionLogisticsDefaultSecretKey2024!"; diff --git a/backend/src/Parhelion.Application/Interfaces/Services/ICatalogItemService.cs b/backend/src/Parhelion.Application/Interfaces/Services/ICatalogItemService.cs new file mode 100644 index 0000000..74bcff7 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/ICatalogItemService.cs @@ -0,0 +1,74 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Catalog; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de CatalogItems (catálogo maestro de productos). +/// Maneja productos con SKU, dimensiones y características especiales. +/// +public interface ICatalogItemService : IGenericService +{ + /// + /// Busca un producto por SKU. + /// + /// ID del tenant. + /// SKU del producto. + /// Token de cancelación. + /// Producto encontrado o null. + Task GetBySkuAsync( + Guid tenantId, + string sku, + CancellationToken cancellationToken = default); + + /// + /// Obtiene productos por tenant. + /// + /// ID del tenant. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de productos del tenant. + Task> GetByTenantAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Busca productos por nombre o descripción. + /// + /// ID del tenant. + /// Término de búsqueda. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de productos que coinciden. + Task> SearchAsync( + Guid tenantId, + string searchTerm, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene productos que requieren refrigeración. + /// + /// ID del tenant. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de productos refrigerados. + Task> GetRefrigeratedAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene productos peligrosos (HAZMAT). + /// + /// ID del tenant. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de productos peligrosos. + Task> GetHazardousAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IClientService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IClientService.cs new file mode 100644 index 0000000..8e2d670 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IClientService.cs @@ -0,0 +1,73 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Clients (remitentes/destinatarios de envíos). +/// Los Clients son empresas externas con las que se interactúa comercialmente. +/// +public interface IClientService : IGenericService +{ + /// + /// Busca un cliente por su email. + /// + /// Email del cliente. + /// Token de cancelación. + /// Cliente encontrado o null. + Task GetByEmailAsync( + string email, + CancellationToken cancellationToken = default); + + /// + /// Busca un cliente por su Tax ID (RFC). + /// + /// RFC del cliente. + /// Token de cancelación. + /// Cliente encontrado o null. + Task GetByTaxIdAsync( + string taxId, + CancellationToken cancellationToken = default); + + /// + /// Obtiene clientes por tenant. + /// + /// ID del tenant. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de clientes del tenant. + Task> GetByTenantAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene clientes por prioridad. + /// + /// ID del tenant. + /// Prioridad del cliente. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de clientes con la prioridad especificada. + Task> GetByPriorityAsync( + Guid tenantId, + ClientPriority priority, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Busca clientes por nombre de empresa (búsqueda parcial). + /// + /// ID del tenant. + /// Nombre parcial de la empresa. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de clientes que coinciden. + Task> SearchByCompanyNameAsync( + Guid tenantId, + string companyName, + PagedRequest request, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IDriverService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IDriverService.cs new file mode 100644 index 0000000..9126b90 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IDriverService.cs @@ -0,0 +1,42 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Drivers (choferes). +/// +public interface IDriverService : IGenericService +{ + /// + /// Obtiene un chofer por su Employee ID. + /// + Task GetByEmployeeIdAsync(Guid employeeId, CancellationToken cancellationToken = default); + + /// + /// Obtiene choferes por tenant. + /// + Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene choferes por estatus. + /// + Task> GetByStatusAsync(Guid tenantId, DriverStatus status, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene choferes disponibles. + /// + Task> GetAvailableAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Actualiza el estatus de un chofer. + /// + Task> UpdateStatusAsync(Guid id, DriverStatus status, CancellationToken cancellationToken = default); + + /// + /// Asigna un camión al chofer. + /// + Task> AssignTruckAsync(Guid driverId, Guid truckId, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IEmployeeService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IEmployeeService.cs new file mode 100644 index 0000000..5131187 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IEmployeeService.cs @@ -0,0 +1,58 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Employees (datos laborales de usuarios). +/// Un Employee está vinculado 1:1 con un User y contiene datos legales (RFC, NSS, CURP). +/// +public interface IEmployeeService : IGenericService +{ + /// + /// Obtiene un empleado por su User ID. + /// + /// ID del usuario asociado. + /// Token de cancelación. + /// Empleado encontrado o null. + Task GetByUserIdAsync( + Guid userId, + CancellationToken cancellationToken = default); + + /// + /// Obtiene empleados por tenant. + /// + /// ID del tenant. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de empleados del tenant. + Task> GetByTenantAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Busca un empleado por RFC. + /// + /// RFC del empleado. + /// Token de cancelación. + /// Empleado encontrado o null. + Task GetByRfcAsync( + string rfc, + CancellationToken cancellationToken = default); + + /// + /// Obtiene empleados por departamento. + /// + /// ID del tenant. + /// Nombre del departamento. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de empleados del departamento. + Task> GetByDepartmentAsync( + Guid tenantId, + string department, + PagedRequest request, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IFleetLogService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IFleetLogService.cs new file mode 100644 index 0000000..f05d31c --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IFleetLogService.cs @@ -0,0 +1,46 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de FleetLogs (bitácora de asignaciones). +/// +public interface IFleetLogService +{ + /// + /// Obtiene logs con paginación. + /// + Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene un log por ID. + /// + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// Obtiene historial de un chofer. + /// + Task> GetByDriverAsync(Guid driverId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene historial de un camión. + /// + Task> GetByTruckAsync(Guid truckId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Registra inicio de uso de un camión por un chofer. + /// + Task> StartUsageAsync(Guid driverId, Guid truckId, CancellationToken cancellationToken = default); + + /// + /// Registra fin de uso de un camión. + /// + Task> EndUsageAsync(Guid logId, decimal? endOdometer, CancellationToken cancellationToken = default); + + /// + /// Obtiene el log activo de un chofer (sin EndAt). + /// + Task GetActiveLogForDriverAsync(Guid driverId, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IGenericService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IGenericService.cs new file mode 100644 index 0000000..9ceba30 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IGenericService.cs @@ -0,0 +1,78 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Domain.Common; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Interface base genérica para servicios CRUD. +/// Proporciona operaciones estándar para todas las entidades del dominio. +/// +/// Tipo de entidad del dominio. +/// DTO de respuesta. +/// DTO de creación. +/// DTO de actualización. +public interface IGenericService + where TEntity : BaseEntity +{ + /// + /// Obtiene entidades paginadas. + /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado con DTOs de respuesta. + Task> GetAllAsync( + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene una entidad por su ID. + /// + /// ID de la entidad. + /// Token de cancelación. + /// DTO de respuesta o null si no existe. + Task GetByIdAsync( + Guid id, + CancellationToken cancellationToken = default); + + /// + /// Crea una nueva entidad. + /// + /// DTO con datos de creación. + /// Token de cancelación. + /// Resultado de la operación con el ID creado. + Task> CreateAsync( + TCreateRequest request, + CancellationToken cancellationToken = default); + + /// + /// Actualiza una entidad existente. + /// + /// ID de la entidad a actualizar. + /// DTO con datos de actualización. + /// Token de cancelación. + /// Resultado de la operación. + Task> UpdateAsync( + Guid id, + TUpdateRequest request, + CancellationToken cancellationToken = default); + + /// + /// Elimina una entidad (soft delete). + /// + /// ID de la entidad a eliminar. + /// Token de cancelación. + /// Resultado de la operación. + Task DeleteAsync( + Guid id, + CancellationToken cancellationToken = default); + + /// + /// Verifica si existe una entidad con el ID especificado. + /// + /// ID a verificar. + /// Token de cancelación. + /// True si existe, false en caso contrario. + Task ExistsAsync( + Guid id, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/ILocationService.cs b/backend/src/Parhelion.Application/Interfaces/Services/ILocationService.cs new file mode 100644 index 0000000..a41b3ab --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/ILocationService.cs @@ -0,0 +1,32 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Locations (ubicaciones/nodos de la red). +/// +public interface ILocationService : IGenericService +{ + /// + /// Obtiene ubicaciones por tenant. + /// + Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene ubicaciones por tipo. + /// + Task> GetByTypeAsync(Guid tenantId, LocationType locationType, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene ubicaciones activas. + /// + Task> GetActiveAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Busca ubicaciones por nombre. + /// + Task> SearchByNameAsync(Guid tenantId, string name, PagedRequest request, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IRoleService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IRoleService.cs new file mode 100644 index 0000000..bf3990e --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IRoleService.cs @@ -0,0 +1,39 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Roles del sistema. +/// Los roles definen permisos inmutables en código (RolePermissions.cs). +/// +public interface IRoleService : IGenericService +{ + /// + /// Busca un rol por su nombre. + /// + /// Nombre del rol (ej: "Admin", "Driver"). + /// Token de cancelación. + /// Rol encontrado o null. + Task GetByNameAsync( + string name, + CancellationToken cancellationToken = default); + + /// + /// Obtiene los permisos asociados a un rol. + /// Los permisos están definidos en RolePermissions.cs (inmutables). + /// + /// Nombre del rol. + /// Lista de permisos del rol. + IEnumerable GetPermissions(string roleName); + + /// + /// Verifica si un rol tiene un permiso específico. + /// + /// Nombre del rol. + /// Permiso a verificar. + /// True si el rol tiene el permiso. + bool HasPermission(string roleName, Permission permission); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IRouteService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IRouteService.cs new file mode 100644 index 0000000..0f5f779 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IRouteService.cs @@ -0,0 +1,31 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Routes (rutas de transporte). +/// +public interface IRouteService : IGenericService +{ + /// + /// Obtiene rutas por tenant. + /// + Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene rutas activas. + /// + Task> GetActiveAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Busca rutas por nombre. + /// + Task> SearchByNameAsync(Guid tenantId, string name, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene los pasos de una ruta. + /// + Task> GetStepsAsync(Guid routeId, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IShipmentCheckpointService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentCheckpointService.cs new file mode 100644 index 0000000..ff9a7da --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentCheckpointService.cs @@ -0,0 +1,71 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de ShipmentCheckpoints (eventos de trazabilidad). +/// Maneja el registro de eventos durante el ciclo de vida del envío. +/// +public interface IShipmentCheckpointService +{ + /// + /// Obtiene checkpoints con paginación. + /// + Task> GetAllAsync( + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene un checkpoint por ID. + /// + Task GetByIdAsync( + Guid id, + CancellationToken cancellationToken = default); + + /// + /// Obtiene todos los checkpoints de un envío ordenados por timestamp. + /// + /// ID del envío. + /// Token de cancelación. + /// Lista de checkpoints ordenada cronológicamente. + Task> GetByShipmentAsync( + Guid shipmentId, + CancellationToken cancellationToken = default); + + /// + /// Crea un nuevo checkpoint de trazabilidad. + /// + /// Datos del checkpoint. + /// ID del usuario que crea el checkpoint. + /// Token de cancelación. + /// Resultado de la operación. + Task> CreateAsync( + CreateShipmentCheckpointRequest request, + Guid createdByUserId, + CancellationToken cancellationToken = default); + + /// + /// Obtiene checkpoints por estatus. + /// + /// ID del envío. + /// Código de estatus. + /// Token de cancelación. + /// Lista de checkpoints con el estatus especificado. + Task> GetByStatusCodeAsync( + Guid shipmentId, + string statusCode, + CancellationToken cancellationToken = default); + + /// + /// Obtiene el último checkpoint de un envío. + /// + /// ID del envío. + /// Token de cancelación. + /// Último checkpoint o null. + Task GetLastCheckpointAsync( + Guid shipmentId, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IShipmentDocumentService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentDocumentService.cs new file mode 100644 index 0000000..e4aff20 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentDocumentService.cs @@ -0,0 +1,66 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de ShipmentDocuments (documentos de envío). +/// Maneja documentos legales como Carta Porte, POD, Manifiestos, etc. +/// +public interface IShipmentDocumentService +{ + /// + /// Obtiene documentos con paginación. + /// + Task> GetAllAsync( + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene un documento por ID. + /// + Task GetByIdAsync( + Guid id, + CancellationToken cancellationToken = default); + + /// + /// Obtiene todos los documentos de un envío. + /// + /// ID del envío. + /// Token de cancelación. + /// Lista de documentos del envío. + Task> GetByShipmentAsync( + Guid shipmentId, + CancellationToken cancellationToken = default); + + /// + /// Crea un nuevo documento de envío. + /// + /// Datos del documento. + /// Token de cancelación. + /// Resultado de la operación. + Task> CreateAsync( + CreateShipmentDocumentRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene documentos por tipo. + /// + /// ID del envío. + /// Tipo de documento. + /// Token de cancelación. + /// Lista de documentos del tipo especificado. + Task> GetByTypeAsync( + Guid shipmentId, + DocumentType documentType, + CancellationToken cancellationToken = default); + + /// + /// Elimina un documento. + /// + Task DeleteAsync( + Guid id, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IShipmentItemService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentItemService.cs new file mode 100644 index 0000000..bd05e95 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentItemService.cs @@ -0,0 +1,59 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de ShipmentItems (partidas del manifiesto de carga). +/// Maneja items individuales dentro de un envío con sus características y restricciones. +/// +public interface IShipmentItemService : IGenericService +{ + /// + /// Obtiene todos los items de un envío. + /// + /// ID del envío. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de items del envío. + Task> GetByShipmentAsync( + Guid shipmentId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Calcula el peso volumétrico de un item. + /// Fórmula: (Alto x Ancho x Largo) / Factor Dimensional + /// + /// Ancho en centímetros. + /// Alto en centímetros. + /// Largo en centímetros. + /// Factor dimensional (default: 5000). + /// Peso volumétrico en kilogramos. + decimal CalculateVolumetricWeight( + decimal widthCm, + decimal heightCm, + decimal lengthCm, + decimal factorDimensional = 5000); + + /// + /// Obtiene items que requieren refrigeración de un envío. + /// + /// ID del envío. + /// Token de cancelación. + /// Lista de items con RequiresRefrigeration = true. + Task> GetRefrigeratedItemsAsync( + Guid shipmentId, + CancellationToken cancellationToken = default); + + /// + /// Obtiene items peligrosos (HAZMAT) de un envío. + /// + /// ID del envío. + /// Token de cancelación. + /// Lista de items con IsHazardous = true. + Task> GetHazardousItemsAsync( + Guid shipmentId, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IShipmentService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentService.cs new file mode 100644 index 0000000..a96ae6d --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentService.cs @@ -0,0 +1,99 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Shipments (envíos). +/// Maneja el ciclo de vida completo de un envío desde creación hasta entrega. +/// +public interface IShipmentService : IGenericService +{ + /// + /// Busca un envío por su número de tracking. + /// + /// Número de tracking (ej: PAR-123456). + /// Token de cancelación. + /// Envío encontrado o null. + Task GetByTrackingNumberAsync( + string trackingNumber, + CancellationToken cancellationToken = default); + + /// + /// Obtiene envíos por tenant. + /// + /// ID del tenant. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de envíos del tenant. + Task> GetByTenantAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene envíos por estatus. + /// + /// ID del tenant. + /// Estatus del envío. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de envíos con el estatus especificado. + Task> GetByStatusAsync( + Guid tenantId, + ShipmentStatus status, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene envíos asignados a un chofer. + /// + /// ID del chofer. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de envíos del chofer. + Task> GetByDriverAsync( + Guid driverId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene envíos por ubicación de origen o destino. + /// + /// ID de la ubicación. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de envíos de la ubicación. + Task> GetByLocationAsync( + Guid locationId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Asigna un envío a un chofer y camión. + /// + /// ID del envío. + /// ID del chofer. + /// ID del camión. + /// Token de cancelación. + /// Resultado de la operación. + Task> AssignToDriverAsync( + Guid shipmentId, + Guid driverId, + Guid truckId, + CancellationToken cancellationToken = default); + + /// + /// Actualiza el estatus de un envío. + /// + /// ID del envío. + /// Nuevo estatus. + /// Token de cancelación. + /// Resultado de la operación. + Task> UpdateStatusAsync( + Guid shipmentId, + ShipmentStatus newStatus, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/ITenantService.cs b/backend/src/Parhelion.Application/Interfaces/Services/ITenantService.cs new file mode 100644 index 0000000..149fded --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/ITenantService.cs @@ -0,0 +1,44 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Tenants (empresas clientes). +/// Los Tenants son entidades raíz del sistema multi-tenant. +/// +public interface ITenantService : IGenericService +{ + /// + /// Busca un tenant por su email de contacto. + /// + /// Email de contacto del tenant. + /// Token de cancelación. + /// Tenant encontrado o null. + Task GetByEmailAsync( + string email, + CancellationToken cancellationToken = default); + + /// + /// Obtiene solo los tenants activos. + /// + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de tenants activos. + Task> GetActiveAsync( + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Activa o desactiva un tenant. + /// + /// ID del tenant. + /// Estado deseado. + /// Token de cancelación. + /// Resultado de la operación. + Task SetActiveStatusAsync( + Guid id, + bool isActive, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/ITruckService.cs b/backend/src/Parhelion.Application/Interfaces/Services/ITruckService.cs new file mode 100644 index 0000000..9be6d4a --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/ITruckService.cs @@ -0,0 +1,42 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Trucks (camiones/unidades). +/// +public interface ITruckService : IGenericService +{ + /// + /// Busca un camión por placa. + /// + Task GetByPlateAsync(string plate, CancellationToken cancellationToken = default); + + /// + /// Obtiene camiones por tenant. + /// + Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene camiones activos o inactivos. + /// + Task> GetByActiveStatusAsync(Guid tenantId, bool isActive, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene camiones por tipo. + /// + Task> GetByTypeAsync(Guid tenantId, TruckType truckType, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Actualiza el estatus activo de un camión. + /// + Task> SetActiveStatusAsync(Guid id, bool isActive, CancellationToken cancellationToken = default); + + /// + /// Obtiene camiones disponibles (activos). + /// + Task> GetAvailableAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IUserService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IUserService.cs new file mode 100644 index 0000000..9bdb6ed --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IUserService.cs @@ -0,0 +1,69 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de Users. +/// Maneja autenticación, roles y estado de usuarios. +/// +public interface IUserService : IGenericService +{ + /// + /// Busca un usuario por su email. + /// + /// Email del usuario. + /// Token de cancelación. + /// Usuario encontrado o null. + Task GetByEmailAsync( + string email, + CancellationToken cancellationToken = default); + + /// + /// Obtiene usuarios por tenant. + /// + /// ID del tenant. + /// Parámetros de paginación. + /// Token de cancelación. + /// Resultado paginado de usuarios del tenant. + Task> GetByTenantAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Verifica credenciales de usuario (para login). + /// + /// Email del usuario. + /// Password en texto plano. + /// Token de cancelación. + /// Usuario si credenciales válidas, null si no. + Task ValidateCredentialsAsync( + string email, + string password, + CancellationToken cancellationToken = default); + + /// + /// Actualiza el último login del usuario. + /// + /// ID del usuario. + /// Token de cancelación. + Task UpdateLastLoginAsync( + Guid userId, + CancellationToken cancellationToken = default); + + /// + /// Cambia el password de un usuario. + /// + /// ID del usuario. + /// Password actual. + /// Nuevo password. + /// Token de cancelación. + /// Resultado de la operación. + Task ChangePasswordAsync( + Guid userId, + string currentPassword, + string newPassword, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Core/ClientService.cs b/backend/src/Parhelion.Infrastructure/Services/Core/ClientService.cs new file mode 100644 index 0000000..953d42e --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Core/ClientService.cs @@ -0,0 +1,294 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Core; + +/// +/// Implementación del servicio de Clients. +/// Gestiona clientes B2B (remitentes/destinatarios de envíos). +/// +public class ClientService : IClientService +{ + private readonly IUnitOfWork _unitOfWork; + + /// + /// Inicializa una nueva instancia del servicio de Clients. + /// + /// Unit of Work para coordinación de repositorios. + public ClientService(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + /// + public async Task> GetAllAsync( + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Clients.GetPagedAsync( + request, + filter: null, + orderBy: q => q.OrderByDescending(c => c.CreatedAt), + cancellationToken); + + var dtos = items.Select(MapToResponse); + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task GetByIdAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Clients.GetByIdAsync(id, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + /// + public async Task> CreateAsync( + CreateClientRequest request, + CancellationToken cancellationToken = default) + { + // Validar email único + var existingByEmail = await _unitOfWork.Clients.FirstOrDefaultAsync( + c => c.Email == request.Email, cancellationToken); + + if (existingByEmail != null) + { + return OperationResult.Fail( + $"Ya existe un cliente con el email '{request.Email}'"); + } + + // Validar Tax ID único si se proporciona + if (!string.IsNullOrEmpty(request.TaxId)) + { + var existingByTaxId = await _unitOfWork.Clients.FirstOrDefaultAsync( + c => c.TaxId == request.TaxId, cancellationToken); + + if (existingByTaxId != null) + { + return OperationResult.Fail( + $"Ya existe un cliente con el RFC '{request.TaxId}'"); + } + } + + // Parsear prioridad + if (!Enum.TryParse(request.Priority, out var priority)) + { + priority = ClientPriority.Normal; + } + + var entity = new Client + { + Id = Guid.NewGuid(), + CompanyName = request.CompanyName, + TradeName = request.TradeName, + ContactName = request.ContactName, + Email = request.Email, + Phone = request.Phone, + TaxId = request.TaxId, + LegalName = request.LegalName, + BillingAddress = request.BillingAddress, + ShippingAddress = request.ShippingAddress, + PreferredProductTypes = request.PreferredProductTypes, + Priority = priority, + IsActive = true, + Notes = request.Notes, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Clients.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity), + "Cliente creado exitosamente"); + } + + /// + public async Task> UpdateAsync( + Guid id, + UpdateClientRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Clients.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Cliente no encontrado"); + } + + // Validar email único (excluyendo el actual) + var existingByEmail = await _unitOfWork.Clients.FirstOrDefaultAsync( + c => c.Email == request.Email && c.Id != id, cancellationToken); + + if (existingByEmail != null) + { + return OperationResult.Fail( + $"Ya existe otro cliente con el email '{request.Email}'"); + } + + // Validar Tax ID único (excluyendo el actual) + if (!string.IsNullOrEmpty(request.TaxId)) + { + var existingByTaxId = await _unitOfWork.Clients.FirstOrDefaultAsync( + c => c.TaxId == request.TaxId && c.Id != id, cancellationToken); + + if (existingByTaxId != null) + { + return OperationResult.Fail( + $"Ya existe otro cliente con el RFC '{request.TaxId}'"); + } + } + + // Parsear prioridad + if (!Enum.TryParse(request.Priority, out var priority)) + { + priority = entity.Priority; + } + + entity.CompanyName = request.CompanyName; + entity.TradeName = request.TradeName; + entity.ContactName = request.ContactName; + entity.Email = request.Email; + entity.Phone = request.Phone; + entity.TaxId = request.TaxId; + entity.LegalName = request.LegalName; + entity.BillingAddress = request.BillingAddress; + entity.ShippingAddress = request.ShippingAddress; + entity.PreferredProductTypes = request.PreferredProductTypes; + entity.Priority = priority; + entity.IsActive = request.IsActive; + entity.Notes = request.Notes; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Clients.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity), + "Cliente actualizado exitosamente"); + } + + /// + public async Task DeleteAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Clients.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Cliente no encontrado"); + } + + _unitOfWork.Clients.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok("Cliente eliminado exitosamente"); + } + + /// + public async Task ExistsAsync( + Guid id, + CancellationToken cancellationToken = default) + { + return await _unitOfWork.Clients.AnyAsync(c => c.Id == id, cancellationToken); + } + + /// + public async Task GetByEmailAsync( + string email, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Clients.FirstOrDefaultAsync( + c => c.Email == email, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + /// + public async Task GetByTaxIdAsync( + string taxId, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Clients.FirstOrDefaultAsync( + c => c.TaxId == taxId, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + /// + public async Task> GetByTenantAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Clients.GetPagedAsync( + request, + filter: c => c.TenantId == tenantId, + orderBy: q => q.OrderByDescending(c => c.CreatedAt), + cancellationToken); + + var dtos = items.Select(MapToResponse); + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task> GetByPriorityAsync( + Guid tenantId, + ClientPriority priority, + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Clients.GetPagedAsync( + request, + filter: c => c.TenantId == tenantId && c.Priority == priority, + orderBy: q => q.OrderByDescending(c => c.CreatedAt), + cancellationToken); + + var dtos = items.Select(MapToResponse); + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task> SearchByCompanyNameAsync( + Guid tenantId, + string companyName, + PagedRequest request, + CancellationToken cancellationToken = default) + { + var searchTerm = companyName.ToLower(); + var (items, totalCount) = await _unitOfWork.Clients.GetPagedAsync( + request, + filter: c => c.TenantId == tenantId && + c.CompanyName.ToLower().Contains(searchTerm), + orderBy: q => q.OrderByDescending(c => c.CreatedAt), + cancellationToken); + + var dtos = items.Select(MapToResponse); + return PagedResult.From(dtos, totalCount, request); + } + + /// + /// Mapea una entidad Client a su DTO de respuesta. + /// + private static ClientResponse MapToResponse(Client entity) => new( + entity.Id, + entity.CompanyName, + entity.TradeName, + entity.ContactName, + entity.Email, + entity.Phone, + entity.TaxId, + entity.LegalName, + entity.BillingAddress, + entity.ShippingAddress, + entity.PreferredProductTypes, + entity.Priority.ToString(), + entity.IsActive, + entity.Notes, + entity.CreatedAt, + entity.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Core/EmployeeService.cs b/backend/src/Parhelion.Infrastructure/Services/Core/EmployeeService.cs new file mode 100644 index 0000000..226ff34 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Core/EmployeeService.cs @@ -0,0 +1,282 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Services.Core; + +/// +/// Implementación del servicio de Employees. +/// Gestiona datos laborales de empleados (RFC, NSS, CURP, contacto de emergencia). +/// +public class EmployeeService : IEmployeeService +{ + private readonly IUnitOfWork _unitOfWork; + + /// + /// Inicializa una nueva instancia del servicio de Employees. + /// + /// Unit of Work para coordinación de repositorios. + public EmployeeService(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + /// + public async Task> GetAllAsync( + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Employees.GetPagedAsync( + request, + filter: null, + orderBy: q => q.OrderByDescending(e => e.CreatedAt), + cancellationToken); + + var dtos = new List(); + foreach (var employee in items) + { + var user = await _unitOfWork.Users.GetByIdAsync(employee.UserId, cancellationToken); + dtos.Add(MapToResponse(employee, user)); + } + + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task GetByIdAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Employees.GetByIdAsync(id, cancellationToken); + if (entity == null) return null; + + var user = await _unitOfWork.Users.GetByIdAsync(entity.UserId, cancellationToken); + return MapToResponse(entity, user); + } + + /// + public async Task> CreateAsync( + CreateEmployeeRequest request, + CancellationToken cancellationToken = default) + { + // Validar que el usuario exista + var user = await _unitOfWork.Users.GetByIdAsync(request.UserId, cancellationToken); + if (user == null) + { + return OperationResult.Fail("Usuario no encontrado"); + } + + // Validar que el usuario no tenga ya un empleado + var existingByUser = await _unitOfWork.Employees.FirstOrDefaultAsync( + e => e.UserId == request.UserId, cancellationToken); + + if (existingByUser != null) + { + return OperationResult.Fail( + "El usuario ya tiene un registro de empleado"); + } + + // Validar RFC único si se proporciona + if (!string.IsNullOrEmpty(request.Rfc)) + { + var existingByRfc = await _unitOfWork.Employees.FirstOrDefaultAsync( + e => e.Rfc == request.Rfc, cancellationToken); + + if (existingByRfc != null) + { + return OperationResult.Fail( + $"Ya existe un empleado con el RFC '{request.Rfc}'"); + } + } + + var entity = new Employee + { + Id = Guid.NewGuid(), + TenantId = user.TenantId, + UserId = request.UserId, + Phone = request.Phone, + Rfc = request.Rfc, + Nss = request.Nss, + Curp = request.Curp, + EmergencyContact = request.EmergencyContact, + EmergencyPhone = request.EmergencyPhone, + HireDate = request.HireDate, + ShiftId = request.ShiftId, + Department = request.Department, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Employees.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity, user), + "Empleado creado exitosamente"); + } + + /// + public async Task> UpdateAsync( + Guid id, + UpdateEmployeeRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Employees.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Empleado no encontrado"); + } + + // Validar RFC único (excluyendo el actual) + if (!string.IsNullOrEmpty(request.Rfc)) + { + var existingByRfc = await _unitOfWork.Employees.FirstOrDefaultAsync( + e => e.Rfc == request.Rfc && e.Id != id, cancellationToken); + + if (existingByRfc != null) + { + return OperationResult.Fail( + $"Ya existe otro empleado con el RFC '{request.Rfc}'"); + } + } + + entity.Phone = request.Phone; + entity.Rfc = request.Rfc; + entity.Nss = request.Nss; + entity.Curp = request.Curp; + entity.EmergencyContact = request.EmergencyContact; + entity.EmergencyPhone = request.EmergencyPhone; + entity.HireDate = request.HireDate; + entity.ShiftId = request.ShiftId; + entity.Department = request.Department; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Employees.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + var user = await _unitOfWork.Users.GetByIdAsync(entity.UserId, cancellationToken); + return OperationResult.Ok( + MapToResponse(entity, user), + "Empleado actualizado exitosamente"); + } + + /// + public async Task DeleteAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Employees.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Empleado no encontrado"); + } + + _unitOfWork.Employees.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok("Empleado eliminado exitosamente"); + } + + /// + public async Task ExistsAsync( + Guid id, + CancellationToken cancellationToken = default) + { + return await _unitOfWork.Employees.AnyAsync(e => e.Id == id, cancellationToken); + } + + /// + public async Task GetByUserIdAsync( + Guid userId, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Employees.FirstOrDefaultAsync( + e => e.UserId == userId, cancellationToken); + + if (entity == null) return null; + + var user = await _unitOfWork.Users.GetByIdAsync(userId, cancellationToken); + return MapToResponse(entity, user); + } + + /// + public async Task> GetByTenantAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Employees.GetPagedAsync( + request, + filter: e => e.TenantId == tenantId, + orderBy: q => q.OrderByDescending(e => e.CreatedAt), + cancellationToken); + + var dtos = new List(); + foreach (var employee in items) + { + var user = await _unitOfWork.Users.GetByIdAsync(employee.UserId, cancellationToken); + dtos.Add(MapToResponse(employee, user)); + } + + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task GetByRfcAsync( + string rfc, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Employees.FirstOrDefaultAsync( + e => e.Rfc == rfc, cancellationToken); + + if (entity == null) return null; + + var user = await _unitOfWork.Users.GetByIdAsync(entity.UserId, cancellationToken); + return MapToResponse(entity, user); + } + + /// + public async Task> GetByDepartmentAsync( + Guid tenantId, + string department, + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Employees.GetPagedAsync( + request, + filter: e => e.TenantId == tenantId && e.Department == department, + orderBy: q => q.OrderByDescending(e => e.CreatedAt), + cancellationToken); + + var dtos = new List(); + foreach (var employee in items) + { + var user = await _unitOfWork.Users.GetByIdAsync(employee.UserId, cancellationToken); + dtos.Add(MapToResponse(employee, user)); + } + + return PagedResult.From(dtos, totalCount, request); + } + + /// + /// Mapea una entidad Employee a su DTO de respuesta. + /// + private static EmployeeResponse MapToResponse(Employee entity, User? user) => new( + entity.Id, + entity.UserId, + user?.FullName ?? "Unknown", + user?.Email ?? "Unknown", + entity.Phone, + entity.Rfc, + entity.Nss, + entity.Curp, + entity.EmergencyContact, + entity.EmergencyPhone, + entity.HireDate, + entity.ShiftId, + entity.Department, + entity.CreatedAt, + entity.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Core/RoleService.cs b/backend/src/Parhelion.Infrastructure/Services/Core/RoleService.cs new file mode 100644 index 0000000..44ba822 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Core/RoleService.cs @@ -0,0 +1,183 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Application.Auth; + +namespace Parhelion.Infrastructure.Services.Core; + +/// +/// Implementación del servicio de Roles. +/// Gestiona roles del sistema con permisos inmutables definidos en RolePermissions. +/// +public class RoleService : IRoleService +{ + private readonly IUnitOfWork _unitOfWork; + + /// + /// Inicializa una nueva instancia del servicio de Roles. + /// + /// Unit of Work para coordinación de repositorios. + public RoleService(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + /// + public async Task> GetAllAsync( + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Roles.GetPagedAsync( + request, + filter: null, + orderBy: q => q.OrderBy(r => r.Name), + cancellationToken); + + var dtos = items.Select(MapToResponse); + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task GetByIdAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Roles.GetByIdAsync(id, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + /// + public async Task> CreateAsync( + CreateRoleRequest request, + CancellationToken cancellationToken = default) + { + // Validar nombre único + var existingByName = await _unitOfWork.Roles.FirstOrDefaultAsync( + r => r.Name == request.Name, cancellationToken); + + if (existingByName != null) + { + return OperationResult.Fail( + $"Ya existe un rol con el nombre '{request.Name}'"); + } + + var entity = new Role + { + Id = Guid.NewGuid(), + Name = request.Name, + Description = request.Description, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Roles.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity), + "Rol creado exitosamente"); + } + + /// + public async Task> UpdateAsync( + Guid id, + UpdateRoleRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Roles.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Rol no encontrado"); + } + + // Validar nombre único (excluyendo el actual) + var existingByName = await _unitOfWork.Roles.FirstOrDefaultAsync( + r => r.Name == request.Name && r.Id != id, cancellationToken); + + if (existingByName != null) + { + return OperationResult.Fail( + $"Ya existe otro rol con el nombre '{request.Name}'"); + } + + entity.Name = request.Name; + entity.Description = request.Description; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Roles.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity), + "Rol actualizado exitosamente"); + } + + /// + public async Task DeleteAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Roles.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Rol no encontrado"); + } + + // Verificar si hay usuarios con este rol + var usersWithRole = await _unitOfWork.Users.AnyAsync( + u => u.RoleId == id, cancellationToken); + + if (usersWithRole) + { + return OperationResult.Fail( + "No se puede eliminar el rol porque tiene usuarios asignados"); + } + + _unitOfWork.Roles.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok("Rol eliminado exitosamente"); + } + + /// + public async Task ExistsAsync( + Guid id, + CancellationToken cancellationToken = default) + { + return await _unitOfWork.Roles.AnyAsync(r => r.Id == id, cancellationToken); + } + + /// + public async Task GetByNameAsync( + string name, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Roles.FirstOrDefaultAsync( + r => r.Name == name, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + /// + public IEnumerable GetPermissions(string roleName) + { + return RolePermissions.GetPermissions(roleName); + } + + /// + public bool HasPermission(string roleName, Permission permission) + { + return RolePermissions.HasPermission(roleName, permission); + } + + /// + /// Mapea una entidad Role a su DTO de respuesta. + /// + private static RoleResponse MapToResponse(Role entity) => new( + entity.Id, + entity.Name, + entity.Description, + entity.CreatedAt + ); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Core/TenantService.cs b/backend/src/Parhelion.Infrastructure/Services/Core/TenantService.cs new file mode 100644 index 0000000..baae4b2 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Core/TenantService.cs @@ -0,0 +1,206 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Services.Core; + +/// +/// Implementación del servicio de Tenants. +/// Gestiona operaciones CRUD para empresas clientes del sistema multi-tenant. +/// +public class TenantService : ITenantService +{ + private readonly IUnitOfWork _unitOfWork; + + /// + /// Inicializa una nueva instancia del servicio de Tenants. + /// + /// Unit of Work para coordinación de repositorios. + public TenantService(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + /// + public async Task> GetAllAsync( + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Tenants.GetPagedAsync( + request, + filter: null, + orderBy: q => q.OrderByDescending(t => t.CreatedAt), + cancellationToken); + + var dtos = items.Select(MapToResponse); + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task GetByIdAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Tenants.GetByIdAsync(id, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + /// + public async Task> CreateAsync( + CreateTenantRequest request, + CancellationToken cancellationToken = default) + { + // Validar email único + var existingByEmail = await _unitOfWork.Tenants.FirstOrDefaultAsync( + t => t.ContactEmail == request.ContactEmail, cancellationToken); + + if (existingByEmail != null) + { + return OperationResult.Fail( + $"Ya existe un tenant con el email '{request.ContactEmail}'"); + } + + var entity = new Tenant + { + Id = Guid.NewGuid(), + CompanyName = request.CompanyName, + ContactEmail = request.ContactEmail, + FleetSize = request.FleetSize, + DriverCount = request.DriverCount, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Tenants.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity), + "Tenant creado exitosamente"); + } + + /// + public async Task> UpdateAsync( + Guid id, + UpdateTenantRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Tenants.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Tenant no encontrado"); + } + + // Validar email único (excluyendo el actual) + var existingByEmail = await _unitOfWork.Tenants.FirstOrDefaultAsync( + t => t.ContactEmail == request.ContactEmail && t.Id != id, cancellationToken); + + if (existingByEmail != null) + { + return OperationResult.Fail( + $"Ya existe otro tenant con el email '{request.ContactEmail}'"); + } + + entity.CompanyName = request.CompanyName; + entity.ContactEmail = request.ContactEmail; + entity.FleetSize = request.FleetSize; + entity.DriverCount = request.DriverCount; + entity.IsActive = request.IsActive; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Tenants.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity), + "Tenant actualizado exitosamente"); + } + + /// + public async Task DeleteAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Tenants.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Tenant no encontrado"); + } + + _unitOfWork.Tenants.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok("Tenant eliminado exitosamente"); + } + + /// + public async Task ExistsAsync( + Guid id, + CancellationToken cancellationToken = default) + { + return await _unitOfWork.Tenants.AnyAsync(t => t.Id == id, cancellationToken); + } + + /// + public async Task GetByEmailAsync( + string email, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Tenants.FirstOrDefaultAsync( + t => t.ContactEmail == email, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + /// + public async Task> GetActiveAsync( + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Tenants.GetPagedAsync( + request, + filter: t => t.IsActive, + orderBy: q => q.OrderByDescending(t => t.CreatedAt), + cancellationToken); + + var dtos = items.Select(MapToResponse); + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task SetActiveStatusAsync( + Guid id, + bool isActive, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Tenants.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Tenant no encontrado"); + } + + entity.IsActive = isActive; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Tenants.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + var status = isActive ? "activado" : "desactivado"; + return OperationResult.Ok($"Tenant {status} exitosamente"); + } + + /// + /// Mapea una entidad Tenant a su DTO de respuesta. + /// + private static TenantResponse MapToResponse(Tenant entity) => new( + entity.Id, + entity.CompanyName, + entity.ContactEmail, + entity.FleetSize, + entity.DriverCount, + entity.IsActive, + entity.CreatedAt, + entity.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Core/UserService.cs b/backend/src/Parhelion.Infrastructure/Services/Core/UserService.cs new file mode 100644 index 0000000..eacc8de --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Core/UserService.cs @@ -0,0 +1,291 @@ +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.Auth; +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Services.Core; + +/// +/// Implementación del servicio de Users. +/// Gestiona usuarios del sistema incluyendo autenticación y password hashing. +/// +public class UserService : IUserService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IPasswordHasher _passwordHasher; + + /// + /// Inicializa una nueva instancia del servicio de Users. + /// + /// Unit of Work para coordinación de repositorios. + /// Servicio de hashing de passwords. + public UserService(IUnitOfWork unitOfWork, IPasswordHasher passwordHasher) + { + _unitOfWork = unitOfWork; + _passwordHasher = passwordHasher; + } + + /// + public async Task> GetAllAsync( + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Users.GetPagedAsync( + request, + filter: null, + orderBy: q => q.OrderByDescending(u => u.CreatedAt), + cancellationToken); + + var dtos = new List(); + foreach (var user in items) + { + var role = await _unitOfWork.Roles.GetByIdAsync(user.RoleId, cancellationToken); + dtos.Add(MapToResponse(user, role?.Name ?? "Unknown")); + } + + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task GetByIdAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Users.GetByIdAsync(id, cancellationToken); + if (entity == null) return null; + + var role = await _unitOfWork.Roles.GetByIdAsync(entity.RoleId, cancellationToken); + return MapToResponse(entity, role?.Name ?? "Unknown"); + } + + /// + public async Task> CreateAsync( + CreateUserRequest request, + CancellationToken cancellationToken = default) + { + // Validar email único + var existingByEmail = await _unitOfWork.Users.FirstOrDefaultAsync( + u => u.Email == request.Email, cancellationToken); + + if (existingByEmail != null) + { + return OperationResult.Fail( + $"Ya existe un usuario con el email '{request.Email}'"); + } + + // Validar que el rol exista + var role = await _unitOfWork.Roles.GetByIdAsync(request.RoleId, cancellationToken); + if (role == null) + { + return OperationResult.Fail("Rol no encontrado"); + } + + // Hash del password (Argon2id para SuperAdmins, BCrypt para resto) + var isSuperAdmin = request.Email.EndsWith("@parhelion.com"); + var passwordHash = _passwordHasher.HashPassword(request.Password, isSuperAdmin); + + var entity = new User + { + Id = Guid.NewGuid(), + Email = request.Email, + PasswordHash = passwordHash, + FullName = request.FullName, + RoleId = request.RoleId, + IsDemoUser = request.IsDemoUser, + IsSuperAdmin = isSuperAdmin, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Users.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity, role.Name), + "Usuario creado exitosamente"); + } + + /// + public async Task> UpdateAsync( + Guid id, + UpdateUserRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Users.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Usuario no encontrado"); + } + + // Validar que el rol exista + var role = await _unitOfWork.Roles.GetByIdAsync(request.RoleId, cancellationToken); + if (role == null) + { + return OperationResult.Fail("Rol no encontrado"); + } + + entity.FullName = request.FullName; + entity.RoleId = request.RoleId; + entity.IsActive = request.IsActive; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Users.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok( + MapToResponse(entity, role.Name), + "Usuario actualizado exitosamente"); + } + + /// + public async Task DeleteAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Users.GetByIdAsync(id, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Usuario no encontrado"); + } + + if (entity.IsSuperAdmin) + { + return OperationResult.Fail("No se puede eliminar un Super Admin"); + } + + _unitOfWork.Users.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok("Usuario eliminado exitosamente"); + } + + /// + public async Task ExistsAsync( + Guid id, + CancellationToken cancellationToken = default) + { + return await _unitOfWork.Users.AnyAsync(u => u.Id == id, cancellationToken); + } + + /// + public async Task GetByEmailAsync( + string email, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Users.FirstOrDefaultAsync( + u => u.Email == email, cancellationToken); + + if (entity == null) return null; + + var role = await _unitOfWork.Roles.GetByIdAsync(entity.RoleId, cancellationToken); + return MapToResponse(entity, role?.Name ?? "Unknown"); + } + + /// + public async Task> GetByTenantAsync( + Guid tenantId, + PagedRequest request, + CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Users.GetPagedAsync( + request, + filter: u => u.TenantId == tenantId, + orderBy: q => q.OrderByDescending(u => u.CreatedAt), + cancellationToken); + + var dtos = new List(); + foreach (var user in items) + { + var role = await _unitOfWork.Roles.GetByIdAsync(user.RoleId, cancellationToken); + dtos.Add(MapToResponse(user, role?.Name ?? "Unknown")); + } + + return PagedResult.From(dtos, totalCount, request); + } + + /// + public async Task ValidateCredentialsAsync( + string email, + string password, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Users.FirstOrDefaultAsync( + u => u.Email == email && u.IsActive, cancellationToken); + + if (entity == null) return null; + + // Verificar password (detectar si es Argon2id basado en SuperAdmin) + var isValid = _passwordHasher.VerifyPassword( + password, entity.PasswordHash, entity.IsSuperAdmin); + + if (!isValid) return null; + + var role = await _unitOfWork.Roles.GetByIdAsync(entity.RoleId, cancellationToken); + return MapToResponse(entity, role?.Name ?? "Unknown"); + } + + /// + public async Task UpdateLastLoginAsync( + Guid userId, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Users.GetByIdAsync(userId, cancellationToken); + if (entity == null) return; + + entity.LastLogin = DateTime.UtcNow; + _unitOfWork.Users.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + } + + /// + public async Task ChangePasswordAsync( + Guid userId, + string currentPassword, + string newPassword, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Users.GetByIdAsync(userId, cancellationToken); + if (entity == null) + { + return OperationResult.Fail("Usuario no encontrado"); + } + + // Verificar password actual + var isValid = _passwordHasher.VerifyPassword( + currentPassword, entity.PasswordHash, entity.IsSuperAdmin); + + if (!isValid) + { + return OperationResult.Fail("Password actual incorrecto"); + } + + // Hash del nuevo password + entity.PasswordHash = _passwordHasher.HashPassword(newPassword, entity.IsSuperAdmin); + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Users.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok("Password actualizado exitosamente"); + } + + /// + /// Mapea una entidad User a su DTO de respuesta. + /// + private static UserResponse MapToResponse(User entity, string roleName) => new( + entity.Id, + entity.Email, + entity.FullName, + entity.RoleId, + roleName, + entity.IsDemoUser, + entity.IsSuperAdmin, + entity.LastLogin, + entity.IsActive, + entity.CreatedAt, + entity.UpdatedAt + ); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs b/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs new file mode 100644 index 0000000..a93e826 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs @@ -0,0 +1,166 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Fleet; + +/// +/// Implementación del servicio de Drivers. +/// +public class DriverService : IDriverService +{ + private readonly IUnitOfWork _unitOfWork; + + public DriverService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Drivers.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(d => d.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var d in items) dtos.Add(await MapToResponseAsync(d, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Drivers.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> CreateAsync(CreateDriverRequest request, CancellationToken cancellationToken = default) + { + var employee = await _unitOfWork.Employees.GetByIdAsync(request.EmployeeId, cancellationToken); + if (employee == null) return OperationResult.Fail("Empleado no encontrado"); + + var existing = await _unitOfWork.Drivers.FirstOrDefaultAsync(d => d.EmployeeId == request.EmployeeId, cancellationToken); + if (existing != null) return OperationResult.Fail("Este empleado ya tiene registro de chofer"); + + if (!Enum.TryParse(request.Status, out var status)) status = DriverStatus.Available; + + var entity = new Driver + { + Id = Guid.NewGuid(), + EmployeeId = request.EmployeeId, + LicenseNumber = request.LicenseNumber, + LicenseType = request.LicenseType, + LicenseExpiration = request.LicenseExpiration, + DefaultTruckId = request.DefaultTruckId, + Status = status, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Drivers.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Chofer creado exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateDriverRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Drivers.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Chofer no encontrado"); + + entity.LicenseNumber = request.LicenseNumber; + entity.LicenseType = request.LicenseType; + entity.LicenseExpiration = request.LicenseExpiration; + entity.DefaultTruckId = request.DefaultTruckId; + entity.CurrentTruckId = request.CurrentTruckId; + if (Enum.TryParse(request.Status, out var status)) entity.Status = status; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Drivers.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Chofer actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Drivers.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Chofer no encontrado"); + _unitOfWork.Drivers.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Chofer eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.Drivers.AnyAsync(d => d.Id == id, cancellationToken); + + public async Task GetByEmployeeIdAsync(Guid employeeId, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Drivers.FirstOrDefaultAsync(d => d.EmployeeId == employeeId, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + // Driver doesn't have TenantId directly, must filter via Employee + var employees = await _unitOfWork.Employees.FindAsync(e => e.TenantId == tenantId, cancellationToken); + var employeeIds = employees.Select(e => e.Id).ToHashSet(); + + var (items, totalCount) = await _unitOfWork.Drivers.GetPagedAsync(request, + filter: d => employeeIds.Contains(d.EmployeeId), + orderBy: q => q.OrderBy(d => d.CreatedAt), cancellationToken); + + var dtos = new List(); + foreach (var d in items) dtos.Add(await MapToResponseAsync(d, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByStatusAsync(Guid tenantId, DriverStatus status, PagedRequest request, CancellationToken cancellationToken = default) + { + var employees = await _unitOfWork.Employees.FindAsync(e => e.TenantId == tenantId, cancellationToken); + var employeeIds = employees.Select(e => e.Id).ToHashSet(); + + var (items, totalCount) = await _unitOfWork.Drivers.GetPagedAsync(request, + filter: d => employeeIds.Contains(d.EmployeeId) && d.Status == status, + orderBy: q => q.OrderBy(d => d.CreatedAt), cancellationToken); + + var dtos = new List(); + foreach (var d in items) dtos.Add(await MapToResponseAsync(d, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetAvailableAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) => + await GetByStatusAsync(tenantId, DriverStatus.Available, request, cancellationToken); + + public async Task> UpdateStatusAsync(Guid id, DriverStatus status, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Drivers.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Chofer no encontrado"); + + entity.Status = status; + entity.UpdatedAt = DateTime.UtcNow; + _unitOfWork.Drivers.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), $"Estado actualizado a {status}"); + } + + public async Task> AssignTruckAsync(Guid driverId, Guid truckId, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Drivers.GetByIdAsync(driverId, cancellationToken); + if (entity == null) return OperationResult.Fail("Chofer no encontrado"); + + var truck = await _unitOfWork.Trucks.GetByIdAsync(truckId, cancellationToken); + if (truck == null) return OperationResult.Fail("Camión no encontrado"); + + entity.CurrentTruckId = truckId; + entity.UpdatedAt = DateTime.UtcNow; + _unitOfWork.Drivers.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Camión asignado exitosamente"); + } + + private async Task MapToResponseAsync(Driver e, CancellationToken ct) + { + var employee = await _unitOfWork.Employees.GetByIdAsync(e.EmployeeId, ct); + var user = employee != null ? await _unitOfWork.Users.GetByIdAsync(employee.UserId, ct) : null; + var defaultTruck = e.DefaultTruckId.HasValue ? await _unitOfWork.Trucks.GetByIdAsync(e.DefaultTruckId.Value, ct) : null; + var currentTruck = e.CurrentTruckId.HasValue ? await _unitOfWork.Trucks.GetByIdAsync(e.CurrentTruckId.Value, ct) : null; + + return new DriverResponse(e.Id, e.EmployeeId, user?.FullName ?? "Unknown", e.LicenseNumber, e.LicenseType, + e.LicenseExpiration, e.DefaultTruckId, defaultTruck?.Plate, e.CurrentTruckId, currentTruck?.Plate, + e.Status.ToString(), e.CreatedAt, e.UpdatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Fleet/FleetLogService.cs b/backend/src/Parhelion.Infrastructure/Services/Fleet/FleetLogService.cs new file mode 100644 index 0000000..fdf754f --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Fleet/FleetLogService.cs @@ -0,0 +1,117 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Fleet; + +/// +/// Implementación del servicio de FleetLogs. +/// +public class FleetLogService : IFleetLogService +{ + private readonly IUnitOfWork _unitOfWork; + + public FleetLogService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.FleetLogs.GetPagedAsync(request, filter: null, orderBy: q => q.OrderByDescending(l => l.Timestamp), cancellationToken); + var dtos = new List(); + foreach (var l in items) dtos.Add(await MapToResponseAsync(l, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.FleetLogs.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> GetByDriverAsync(Guid driverId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.FleetLogs.GetPagedAsync(request, filter: l => l.DriverId == driverId, orderBy: q => q.OrderByDescending(l => l.Timestamp), cancellationToken); + var dtos = new List(); + foreach (var l in items) dtos.Add(await MapToResponseAsync(l, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByTruckAsync(Guid truckId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.FleetLogs.GetPagedAsync(request, filter: l => l.OldTruckId == truckId || l.NewTruckId == truckId, orderBy: q => q.OrderByDescending(l => l.Timestamp), cancellationToken); + var dtos = new List(); + foreach (var l in items) dtos.Add(await MapToResponseAsync(l, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> StartUsageAsync(Guid driverId, Guid truckId, CancellationToken cancellationToken = default) + { + var driver = await _unitOfWork.Drivers.GetByIdAsync(driverId, cancellationToken); + if (driver == null) return OperationResult.Fail("Chofer no encontrado"); + + var truck = await _unitOfWork.Trucks.GetByIdAsync(truckId, cancellationToken); + if (truck == null) return OperationResult.Fail("Camión no encontrado"); + + var entity = new FleetLog + { + Id = Guid.NewGuid(), + TenantId = truck.TenantId, + DriverId = driverId, + OldTruckId = driver.CurrentTruckId, + NewTruckId = truckId, + Reason = FleetLogReason.Reassignment, + Timestamp = DateTime.UtcNow, + CreatedByUserId = Guid.Empty, // Should be injected from context + CreatedAt = DateTime.UtcNow + }; + + // Update driver's current truck + driver.CurrentTruckId = truckId; + driver.UpdatedAt = DateTime.UtcNow; + _unitOfWork.Drivers.Update(driver); + + await _unitOfWork.FleetLogs.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Uso de camión iniciado"); + } + + public async Task> EndUsageAsync(Guid logId, decimal? endOdometer, CancellationToken cancellationToken = default) + { + // FleetLog doesn't have end tracking in current model, this is for future extension + var entity = await _unitOfWork.FleetLogs.GetByIdAsync(logId, cancellationToken); + if (entity == null) return OperationResult.Fail("Log no encontrado"); + + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Uso de camión finalizado"); + } + + public async Task GetActiveLogForDriverAsync(Guid driverId, CancellationToken cancellationToken = default) + { + var logs = await _unitOfWork.FleetLogs.FindAsync(l => l.DriverId == driverId, cancellationToken); + var lastLog = logs.OrderByDescending(l => l.Timestamp).FirstOrDefault(); + return lastLog != null ? await MapToResponseAsync(lastLog, cancellationToken) : null; + } + + private async Task MapToResponseAsync(FleetLog e, CancellationToken ct) + { + var driver = await _unitOfWork.Drivers.GetByIdAsync(e.DriverId, ct); + string driverName = "Unknown"; + if (driver != null) + { + var employee = await _unitOfWork.Employees.GetByIdAsync(driver.EmployeeId, ct); + if (employee != null) + { + var user = await _unitOfWork.Users.GetByIdAsync(employee.UserId, ct); + driverName = user?.FullName ?? "Unknown"; + } + } + + var oldTruck = e.OldTruckId.HasValue ? await _unitOfWork.Trucks.GetByIdAsync(e.OldTruckId.Value, ct) : null; + var newTruck = await _unitOfWork.Trucks.GetByIdAsync(e.NewTruckId, ct); + var createdBy = await _unitOfWork.Users.GetByIdAsync(e.CreatedByUserId, ct); + + return new FleetLogResponse(e.Id, e.DriverId, driverName, e.OldTruckId, oldTruck?.Plate, e.NewTruckId, + newTruck?.Plate ?? "Unknown", e.Reason.ToString(), e.Timestamp, e.CreatedByUserId, createdBy?.FullName ?? "System", e.CreatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Fleet/TruckService.cs b/backend/src/Parhelion.Infrastructure/Services/Fleet/TruckService.cs new file mode 100644 index 0000000..2199388 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Fleet/TruckService.cs @@ -0,0 +1,156 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Fleet; + +/// +/// Implementación del servicio de Trucks. +/// +public class TruckService : ITruckService +{ + private readonly IUnitOfWork _unitOfWork; + + public TruckService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Trucks.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(t => t.Plate), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Trucks.GetByIdAsync(id, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + public async Task> CreateAsync(CreateTruckRequest request, CancellationToken cancellationToken = default) + { + var existing = await _unitOfWork.Trucks.FirstOrDefaultAsync(t => t.Plate == request.Plate, cancellationToken); + if (existing != null) return OperationResult.Fail($"Ya existe un camión con placa '{request.Plate}'"); + + if (!Enum.TryParse(request.Type, out var truckType)) + return OperationResult.Fail("Tipo de camión inválido"); + + var entity = new Truck + { + Id = Guid.NewGuid(), + Plate = request.Plate, + Model = request.Model, + Type = truckType, + MaxCapacityKg = request.MaxCapacityKg, + MaxVolumeM3 = request.MaxVolumeM3, + IsActive = true, + Vin = request.Vin, + EngineNumber = request.EngineNumber, + Year = request.Year, + Color = request.Color, + InsurancePolicy = request.InsurancePolicy, + InsuranceExpiration = request.InsuranceExpiration, + VerificationNumber = request.VerificationNumber, + VerificationExpiration = request.VerificationExpiration, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Trucks.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Camión creado exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateTruckRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Trucks.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Camión no encontrado"); + + var existingPlate = await _unitOfWork.Trucks.FirstOrDefaultAsync(t => t.Plate == request.Plate && t.Id != id, cancellationToken); + if (existingPlate != null) return OperationResult.Fail($"La placa '{request.Plate}' ya está en uso"); + + if (!Enum.TryParse(request.Type, out var truckType)) + return OperationResult.Fail("Tipo de camión inválido"); + + entity.Plate = request.Plate; + entity.Model = request.Model; + entity.Type = truckType; + entity.MaxCapacityKg = request.MaxCapacityKg; + entity.MaxVolumeM3 = request.MaxVolumeM3; + entity.IsActive = request.IsActive; + entity.Vin = request.Vin; + entity.EngineNumber = request.EngineNumber; + entity.Year = request.Year; + entity.Color = request.Color; + entity.InsurancePolicy = request.InsurancePolicy; + entity.InsuranceExpiration = request.InsuranceExpiration; + entity.VerificationNumber = request.VerificationNumber; + entity.VerificationExpiration = request.VerificationExpiration; + entity.LastMaintenanceDate = request.LastMaintenanceDate; + entity.NextMaintenanceDate = request.NextMaintenanceDate; + entity.CurrentOdometerKm = request.CurrentOdometerKm; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Trucks.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Camión actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Trucks.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Camión no encontrado"); + _unitOfWork.Trucks.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Camión eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.Trucks.AnyAsync(t => t.Id == id, cancellationToken); + + public async Task GetByPlateAsync(string plate, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Trucks.FirstOrDefaultAsync(t => t.Plate == plate, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Trucks.GetPagedAsync(request, filter: t => t.TenantId == tenantId, orderBy: q => q.OrderBy(t => t.Plate), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> GetByActiveStatusAsync(Guid tenantId, bool isActive, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Trucks.GetPagedAsync(request, filter: t => t.TenantId == tenantId && t.IsActive == isActive, orderBy: q => q.OrderBy(t => t.Plate), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> GetByTypeAsync(Guid tenantId, TruckType truckType, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Trucks.GetPagedAsync(request, filter: t => t.TenantId == tenantId && t.Type == truckType, orderBy: q => q.OrderBy(t => t.Plate), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> SetActiveStatusAsync(Guid id, bool isActive, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Trucks.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Camión no encontrado"); + + entity.IsActive = isActive; + entity.UpdatedAt = DateTime.UtcNow; + _unitOfWork.Trucks.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), $"Estado actualizado"); + } + + public async Task> GetAvailableAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Trucks.GetPagedAsync(request, filter: t => t.TenantId == tenantId && t.IsActive, orderBy: q => q.OrderBy(t => t.Plate), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + private static TruckResponse MapToResponse(Truck e) => new(e.Id, e.Plate, e.Model, e.Type.ToString(), e.MaxCapacityKg, e.MaxVolumeM3, + e.IsActive, e.Vin, e.EngineNumber, e.Year, e.Color, e.InsurancePolicy, e.InsuranceExpiration, e.VerificationNumber, + e.VerificationExpiration, e.LastMaintenanceDate, e.NextMaintenanceDate, e.CurrentOdometerKm, e.CreatedAt, e.UpdatedAt); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Network/LocationService.cs b/backend/src/Parhelion.Infrastructure/Services/Network/LocationService.cs new file mode 100644 index 0000000..49947b8 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Network/LocationService.cs @@ -0,0 +1,120 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Network; + +/// +/// Implementación del servicio de Locations. +/// +public class LocationService : ILocationService +{ + private readonly IUnitOfWork _unitOfWork; + + public LocationService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Locations.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(l => l.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Locations.GetByIdAsync(id, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + public async Task> CreateAsync(CreateLocationRequest request, CancellationToken cancellationToken = default) + { + if (!Enum.TryParse(request.Type, out var locationType)) + return OperationResult.Fail("Tipo de ubicación inválido"); + + var entity = new Location + { + Id = Guid.NewGuid(), + Code = request.Code, + Name = request.Name, + Type = locationType, + FullAddress = request.FullAddress, + Latitude = request.Latitude, + Longitude = request.Longitude, + CanReceive = request.CanReceive, + CanDispatch = request.CanDispatch, + IsInternal = request.IsInternal, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Locations.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Ubicación creada exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateLocationRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Locations.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Ubicación no encontrada"); + + if (!Enum.TryParse(request.Type, out var locationType)) + return OperationResult.Fail("Tipo de ubicación inválido"); + + entity.Code = request.Code; + entity.Name = request.Name; + entity.Type = locationType; + entity.FullAddress = request.FullAddress; + entity.Latitude = request.Latitude; + entity.Longitude = request.Longitude; + entity.CanReceive = request.CanReceive; + entity.CanDispatch = request.CanDispatch; + entity.IsInternal = request.IsInternal; + entity.IsActive = request.IsActive; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Locations.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Ubicación actualizada exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Locations.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Ubicación no encontrada"); + _unitOfWork.Locations.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Ubicación eliminada exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.Locations.AnyAsync(l => l.Id == id, cancellationToken); + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Locations.GetPagedAsync(request, filter: l => l.TenantId == tenantId, orderBy: q => q.OrderBy(l => l.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> GetByTypeAsync(Guid tenantId, LocationType locationType, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Locations.GetPagedAsync(request, filter: l => l.TenantId == tenantId && l.Type == locationType, orderBy: q => q.OrderBy(l => l.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> GetActiveAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Locations.GetPagedAsync(request, filter: l => l.TenantId == tenantId && l.IsActive, orderBy: q => q.OrderBy(l => l.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> SearchByNameAsync(Guid tenantId, string name, PagedRequest request, CancellationToken cancellationToken = default) + { + var term = name.ToLower(); + var (items, totalCount) = await _unitOfWork.Locations.GetPagedAsync(request, filter: l => l.TenantId == tenantId && (l.Name.ToLower().Contains(term) || l.Code.ToLower().Contains(term)), orderBy: q => q.OrderBy(l => l.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + private static LocationResponse MapToResponse(Location e) => new(e.Id, e.Code, e.Name, e.Type.ToString(), e.FullAddress, e.Latitude, e.Longitude, e.CanReceive, e.CanDispatch, e.IsInternal, e.IsActive, e.CreatedAt, e.UpdatedAt); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Network/RouteService.cs b/backend/src/Parhelion.Infrastructure/Services/Network/RouteService.cs new file mode 100644 index 0000000..616e188 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Network/RouteService.cs @@ -0,0 +1,111 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Services.Network; + +/// +/// Implementación del servicio de Routes. +/// +public class RouteService : IRouteService +{ + private readonly IUnitOfWork _unitOfWork; + + public RouteService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.RouteBlueprints.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(r => r.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.RouteBlueprints.GetByIdAsync(id, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + public async Task> CreateAsync(CreateRouteBlueprintRequest request, CancellationToken cancellationToken = default) + { + var entity = new RouteBlueprint + { + Id = Guid.NewGuid(), + Name = request.Name, + Description = request.Description, + TotalSteps = 0, + TotalTransitTime = TimeSpan.Zero, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.RouteBlueprints.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Ruta creada exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateRouteBlueprintRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.RouteBlueprints.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Ruta no encontrada"); + + entity.Name = request.Name; + entity.Description = request.Description; + entity.TotalSteps = request.TotalSteps; + entity.TotalTransitTime = request.TotalTransitTime; + entity.IsActive = request.IsActive; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.RouteBlueprints.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Ruta actualizada exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.RouteBlueprints.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Ruta no encontrada"); + _unitOfWork.RouteBlueprints.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Ruta eliminada exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.RouteBlueprints.AnyAsync(r => r.Id == id, cancellationToken); + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.RouteBlueprints.GetPagedAsync(request, filter: r => r.TenantId == tenantId, orderBy: q => q.OrderBy(r => r.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> GetActiveAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.RouteBlueprints.GetPagedAsync(request, filter: r => r.TenantId == tenantId && r.IsActive, orderBy: q => q.OrderBy(r => r.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> SearchByNameAsync(Guid tenantId, string name, PagedRequest request, CancellationToken cancellationToken = default) + { + var term = name.ToLower(); + var (items, totalCount) = await _unitOfWork.RouteBlueprints.GetPagedAsync(request, filter: r => r.TenantId == tenantId && r.Name.ToLower().Contains(term), orderBy: q => q.OrderBy(r => r.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> GetStepsAsync(Guid routeId, CancellationToken cancellationToken = default) + { + var steps = await _unitOfWork.RouteSteps.FindAsync(s => s.RouteBlueprintId == routeId, cancellationToken); + var orderedSteps = steps.OrderBy(s => s.StepOrder).ToList(); + var dtos = new List(); + foreach (var step in orderedSteps) + { + var route = await _unitOfWork.RouteBlueprints.GetByIdAsync(step.RouteBlueprintId, cancellationToken); + var location = await _unitOfWork.Locations.GetByIdAsync(step.LocationId, cancellationToken); + dtos.Add(new RouteStepResponse(step.Id, step.RouteBlueprintId, route?.Name ?? "", step.LocationId, location?.Name ?? "", step.StepOrder, step.StandardTransitTime, step.StepType.ToString(), step.CreatedAt, step.UpdatedAt)); + } + return dtos; + } + + private static RouteBlueprintResponse MapToResponse(RouteBlueprint e) => new(e.Id, e.Name, e.Description, e.TotalSteps, e.TotalTransitTime, e.IsActive, e.CreatedAt, e.UpdatedAt); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/CatalogItemService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/CatalogItemService.cs new file mode 100644 index 0000000..a17b113 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/CatalogItemService.cs @@ -0,0 +1,127 @@ +using Parhelion.Application.DTOs.Catalog; +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Services.Shipment; + +/// +/// Implementación del servicio de CatalogItems. +/// +public class CatalogItemService : ICatalogItemService +{ + private readonly IUnitOfWork _unitOfWork; + + public CatalogItemService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.CatalogItems.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(c => c.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.CatalogItems.GetByIdAsync(id, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + public async Task> CreateAsync(CreateCatalogItemRequest request, CancellationToken cancellationToken = default) + { + var existing = await _unitOfWork.CatalogItems.FirstOrDefaultAsync(c => c.Sku == request.Sku, cancellationToken); + if (existing != null) return OperationResult.Fail($"Ya existe un producto con SKU '{request.Sku}'"); + + var entity = new CatalogItem + { + Id = Guid.NewGuid(), + Sku = request.Sku, + Name = request.Name, + Description = request.Description, + BaseUom = request.BaseUom, + DefaultWeightKg = request.DefaultWeightKg, + DefaultWidthCm = request.DefaultWidthCm, + DefaultHeightCm = request.DefaultHeightCm, + DefaultLengthCm = request.DefaultLengthCm, + RequiresRefrigeration = request.RequiresRefrigeration, + IsHazardous = request.IsHazardous, + IsFragile = request.IsFragile, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.CatalogItems.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Producto creado exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateCatalogItemRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.CatalogItems.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Producto no encontrado"); + + entity.Name = request.Name; + entity.Description = request.Description; + entity.BaseUom = request.BaseUom; + entity.DefaultWeightKg = request.DefaultWeightKg; + entity.DefaultWidthCm = request.DefaultWidthCm; + entity.DefaultHeightCm = request.DefaultHeightCm; + entity.DefaultLengthCm = request.DefaultLengthCm; + entity.RequiresRefrigeration = request.RequiresRefrigeration; + entity.IsHazardous = request.IsHazardous; + entity.IsFragile = request.IsFragile; + entity.IsActive = request.IsActive; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.CatalogItems.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Producto actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.CatalogItems.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Producto no encontrado"); + _unitOfWork.CatalogItems.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Producto eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.CatalogItems.AnyAsync(c => c.Id == id, cancellationToken); + + public async Task GetBySkuAsync(Guid tenantId, string sku, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.CatalogItems.FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Sku == sku, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.CatalogItems.GetPagedAsync(request, filter: c => c.TenantId == tenantId, orderBy: q => q.OrderBy(c => c.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> SearchAsync(Guid tenantId, string searchTerm, PagedRequest request, CancellationToken cancellationToken = default) + { + var term = searchTerm.ToLower(); + var (items, totalCount) = await _unitOfWork.CatalogItems.GetPagedAsync(request, filter: c => c.TenantId == tenantId && (c.Name.ToLower().Contains(term) || c.Sku.ToLower().Contains(term) || (c.Description != null && c.Description.ToLower().Contains(term))), orderBy: q => q.OrderBy(c => c.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> GetRefrigeratedAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.CatalogItems.GetPagedAsync(request, filter: c => c.TenantId == tenantId && c.RequiresRefrigeration, orderBy: q => q.OrderBy(c => c.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task> GetHazardousAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.CatalogItems.GetPagedAsync(request, filter: c => c.TenantId == tenantId && c.IsHazardous, orderBy: q => q.OrderBy(c => c.Name), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + private static CatalogItemResponse MapToResponse(CatalogItem e) => new( + e.Id, e.Sku, e.Name, e.Description, e.BaseUom, e.DefaultWeightKg, e.DefaultWidthCm, e.DefaultHeightCm, e.DefaultLengthCm, + e.DefaultVolumeM3, e.RequiresRefrigeration, e.IsHazardous, e.IsFragile, e.IsActive, e.CreatedAt, e.UpdatedAt); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentCheckpointService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentCheckpointService.cs new file mode 100644 index 0000000..dd9613e --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentCheckpointService.cs @@ -0,0 +1,117 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Shipment; + +/// +/// Implementación del servicio de ShipmentCheckpoints. +/// +public class ShipmentCheckpointService : IShipmentCheckpointService +{ + private readonly IUnitOfWork _unitOfWork; + + public ShipmentCheckpointService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.ShipmentCheckpoints.GetPagedAsync(request, filter: null, orderBy: q => q.OrderByDescending(c => c.Timestamp), cancellationToken); + var dtos = new List(); + foreach (var c in items) dtos.Add(await MapToResponseAsync(c, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.ShipmentCheckpoints.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> GetByShipmentAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var checkpoints = await _unitOfWork.ShipmentCheckpoints.FindAsync(c => c.ShipmentId == shipmentId, cancellationToken); + var ordered = checkpoints.OrderBy(c => c.Timestamp).ToList(); + var dtos = new List(); + foreach (var c in ordered) dtos.Add(await MapToResponseAsync(c, cancellationToken)); + return dtos; + } + + public async Task> CreateAsync(CreateShipmentCheckpointRequest request, Guid createdByUserId, CancellationToken cancellationToken = default) + { + var shipment = await _unitOfWork.Shipments.GetByIdAsync(request.ShipmentId, cancellationToken); + if (shipment == null) return OperationResult.Fail("Envío no encontrado"); + + if (!Enum.TryParse(request.StatusCode, out var statusCode)) + return OperationResult.Fail("Código de estatus inválido"); + + var entity = new ShipmentCheckpoint + { + Id = Guid.NewGuid(), + ShipmentId = request.ShipmentId, + LocationId = request.LocationId, + StatusCode = statusCode, + Remarks = request.Remarks, + Timestamp = DateTime.UtcNow, + CreatedByUserId = createdByUserId, + HandledByDriverId = request.HandledByDriverId, + LoadedOntoTruckId = request.LoadedOntoTruckId, + ActionType = request.ActionType, + PreviousCustodian = request.PreviousCustodian, + NewCustodian = request.NewCustodian, + HandledByWarehouseOperatorId = request.HandledByWarehouseOperatorId, + Latitude = request.Latitude, + Longitude = request.Longitude, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.ShipmentCheckpoints.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Checkpoint creado exitosamente"); + } + + public async Task> GetByStatusCodeAsync(Guid shipmentId, string statusCode, CancellationToken cancellationToken = default) + { + if (!Enum.TryParse(statusCode, out var status)) return Enumerable.Empty(); + var checkpoints = await _unitOfWork.ShipmentCheckpoints.FindAsync(c => c.ShipmentId == shipmentId && c.StatusCode == status, cancellationToken); + var dtos = new List(); + foreach (var c in checkpoints) dtos.Add(await MapToResponseAsync(c, cancellationToken)); + return dtos; + } + + public async Task GetLastCheckpointAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var checkpoints = await _unitOfWork.ShipmentCheckpoints.FindAsync(c => c.ShipmentId == shipmentId, cancellationToken); + var last = checkpoints.OrderByDescending(c => c.Timestamp).FirstOrDefault(); + return last != null ? await MapToResponseAsync(last, cancellationToken) : null; + } + + private async Task MapToResponseAsync(ShipmentCheckpoint e, CancellationToken ct) + { + var location = e.LocationId.HasValue ? await _unitOfWork.Locations.GetByIdAsync(e.LocationId.Value, ct) : null; + var createdBy = await _unitOfWork.Users.GetByIdAsync(e.CreatedByUserId, ct); + + string? driverName = null; + if (e.HandledByDriverId.HasValue) + { + var driver = await _unitOfWork.Drivers.GetByIdAsync(e.HandledByDriverId.Value, ct); + if (driver != null) + { + var employee = await _unitOfWork.Employees.GetByIdAsync(driver.EmployeeId, ct); + if (employee != null) + { + var user = await _unitOfWork.Users.GetByIdAsync(employee.UserId, ct); + driverName = user?.FullName; + } + } + } + + var truck = e.LoadedOntoTruckId.HasValue ? await _unitOfWork.Trucks.GetByIdAsync(e.LoadedOntoTruckId.Value, ct) : null; + + return new ShipmentCheckpointResponse(e.Id, e.ShipmentId, e.LocationId, location?.Name, e.StatusCode.ToString(), e.Remarks, + e.Timestamp, e.CreatedByUserId, createdBy?.FullName ?? "Unknown", e.HandledByDriverId, driverName, e.LoadedOntoTruckId, + truck?.Plate, e.ActionType, e.PreviousCustodian, e.NewCustodian, e.Latitude, e.Longitude, e.CreatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentDocumentService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentDocumentService.cs new file mode 100644 index 0000000..87745c7 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentDocumentService.cs @@ -0,0 +1,79 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Shipment; + +/// +/// Implementación del servicio de ShipmentDocuments. +/// +public class ShipmentDocumentService : IShipmentDocumentService +{ + private readonly IUnitOfWork _unitOfWork; + + public ShipmentDocumentService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.ShipmentDocuments.GetPagedAsync(request, filter: null, orderBy: q => q.OrderByDescending(d => d.GeneratedAt), cancellationToken); + return PagedResult.From(items.Select(MapToResponse), totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.ShipmentDocuments.GetByIdAsync(id, cancellationToken); + return entity != null ? MapToResponse(entity) : null; + } + + public async Task> GetByShipmentAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var docs = await _unitOfWork.ShipmentDocuments.FindAsync(d => d.ShipmentId == shipmentId, cancellationToken); + return docs.Select(MapToResponse); + } + + public async Task> CreateAsync(CreateShipmentDocumentRequest request, CancellationToken cancellationToken = default) + { + var shipment = await _unitOfWork.Shipments.GetByIdAsync(request.ShipmentId, cancellationToken); + if (shipment == null) return OperationResult.Fail("Envío no encontrado"); + + if (!Enum.TryParse(request.DocumentType, out var docType)) + return OperationResult.Fail("Tipo de documento inválido"); + + var entity = new ShipmentDocument + { + Id = Guid.NewGuid(), + ShipmentId = request.ShipmentId, + DocumentType = docType, + FileUrl = request.FileUrl, + GeneratedBy = request.GeneratedBy, + GeneratedAt = DateTime.UtcNow, + ExpiresAt = request.ExpiresAt, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.ShipmentDocuments.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(MapToResponse(entity), "Documento creado exitosamente"); + } + + public async Task> GetByTypeAsync(Guid shipmentId, DocumentType documentType, CancellationToken cancellationToken = default) + { + var docs = await _unitOfWork.ShipmentDocuments.FindAsync(d => d.ShipmentId == shipmentId && d.DocumentType == documentType, cancellationToken); + return docs.Select(MapToResponse); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.ShipmentDocuments.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Documento no encontrado"); + _unitOfWork.ShipmentDocuments.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Documento eliminado exitosamente"); + } + + private static ShipmentDocumentResponse MapToResponse(ShipmentDocument e) => new( + e.Id, e.ShipmentId, e.DocumentType.ToString(), e.FileUrl, e.GeneratedBy, e.GeneratedAt, e.ExpiresAt, e.CreatedAt); +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentItemService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentItemService.cs new file mode 100644 index 0000000..322a3bd --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentItemService.cs @@ -0,0 +1,142 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Shipment; + +/// +/// Implementación del servicio de ShipmentItems. +/// +public class ShipmentItemService : IShipmentItemService +{ + private readonly IUnitOfWork _unitOfWork; + + public ShipmentItemService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.ShipmentItems.GetPagedAsync(request, filter: null, orderBy: q => q.OrderByDescending(i => i.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var i in items) dtos.Add(await MapToResponseAsync(i, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.ShipmentItems.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> CreateAsync(CreateShipmentItemRequest request, CancellationToken cancellationToken = default) + { + var shipment = await _unitOfWork.Shipments.GetByIdAsync(request.ShipmentId, cancellationToken); + if (shipment == null) return OperationResult.Fail("Envío no encontrado"); + + if (!Enum.TryParse(request.PackagingType, out var packagingType)) + return OperationResult.Fail("Tipo de empaque inválido"); + + var entity = new ShipmentItem + { + Id = Guid.NewGuid(), + ShipmentId = request.ShipmentId, + ProductId = request.ProductId, + Sku = request.Sku, + Description = request.Description, + PackagingType = packagingType, + Quantity = request.Quantity, + WeightKg = request.WeightKg, + WidthCm = request.WidthCm, + HeightCm = request.HeightCm, + LengthCm = request.LengthCm, + DeclaredValue = request.DeclaredValue, + IsFragile = request.IsFragile, + IsHazardous = request.IsHazardous, + RequiresRefrigeration = request.RequiresRefrigeration, + StackingInstructions = request.StackingInstructions, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.ShipmentItems.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Item creado exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateShipmentItemRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.ShipmentItems.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Item no encontrado"); + + if (!Enum.TryParse(request.PackagingType, out var packagingType)) + return OperationResult.Fail("Tipo de empaque inválido"); + + entity.Sku = request.Sku; + entity.Description = request.Description; + entity.PackagingType = packagingType; + entity.Quantity = request.Quantity; + entity.WeightKg = request.WeightKg; + entity.WidthCm = request.WidthCm; + entity.HeightCm = request.HeightCm; + entity.LengthCm = request.LengthCm; + entity.DeclaredValue = request.DeclaredValue; + entity.IsFragile = request.IsFragile; + entity.IsHazardous = request.IsHazardous; + entity.RequiresRefrigeration = request.RequiresRefrigeration; + entity.StackingInstructions = request.StackingInstructions; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.ShipmentItems.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Item actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.ShipmentItems.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Item no encontrado"); + _unitOfWork.ShipmentItems.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Item eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.ShipmentItems.AnyAsync(i => i.Id == id, cancellationToken); + + public async Task> GetByShipmentAsync(Guid shipmentId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.ShipmentItems.GetPagedAsync(request, filter: i => i.ShipmentId == shipmentId, orderBy: q => q.OrderBy(i => i.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var i in items) dtos.Add(await MapToResponseAsync(i, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public decimal CalculateVolumetricWeight(decimal widthCm, decimal heightCm, decimal lengthCm, decimal factorDimensional = 5000) => + (widthCm * heightCm * lengthCm) / factorDimensional; + + public async Task> GetRefrigeratedItemsAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var items = await _unitOfWork.ShipmentItems.FindAsync(i => i.ShipmentId == shipmentId && i.RequiresRefrigeration, cancellationToken); + var dtos = new List(); + foreach (var i in items) dtos.Add(await MapToResponseAsync(i, cancellationToken)); + return dtos; + } + + public async Task> GetHazardousItemsAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var items = await _unitOfWork.ShipmentItems.FindAsync(i => i.ShipmentId == shipmentId && i.IsHazardous, cancellationToken); + var dtos = new List(); + foreach (var i in items) dtos.Add(await MapToResponseAsync(i, cancellationToken)); + return dtos; + } + + private async Task MapToResponseAsync(ShipmentItem e, CancellationToken ct) + { + var product = e.ProductId.HasValue ? await _unitOfWork.CatalogItems.GetByIdAsync(e.ProductId.Value, ct) : null; + return new ShipmentItemResponse(e.Id, e.ShipmentId, e.ProductId, product?.Name, e.Sku, e.Description, + e.PackagingType.ToString(), e.Quantity, e.WeightKg, e.WidthCm, e.HeightCm, e.LengthCm, + e.VolumeM3, e.VolumetricWeightKg, e.DeclaredValue, e.IsFragile, e.IsHazardous, + e.RequiresRefrigeration, e.StackingInstructions, e.CreatedAt, e.UpdatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs new file mode 100644 index 0000000..4faf117 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs @@ -0,0 +1,215 @@ +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Shipment; + +/// +/// Implementación del servicio de Shipments. +/// Gestiona el ciclo de vida de envíos desde creación hasta entrega. +/// +public class ShipmentService : IShipmentService +{ + private readonly IUnitOfWork _unitOfWork; + + public ShipmentService(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task> GetAllAsync( + PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Shipments.GetPagedAsync( + request, filter: null, orderBy: q => q.OrderByDescending(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Shipments.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> CreateAsync( + CreateShipmentRequest request, CancellationToken cancellationToken = default) + { + var trackingNumber = GenerateTrackingNumber(); + var entity = new Domain.Entities.Shipment + { + Id = Guid.NewGuid(), + TrackingNumber = trackingNumber, + QrCodeData = $"PAR:{trackingNumber}", + OriginLocationId = request.OriginLocationId, + DestinationLocationId = request.DestinationLocationId, + SenderId = request.SenderId, + RecipientClientId = request.RecipientClientId, + RecipientName = request.RecipientName, + RecipientPhone = request.RecipientPhone, + TotalWeightKg = request.TotalWeightKg, + TotalVolumeM3 = request.TotalVolumeM3, + DeclaredValue = request.DeclaredValue, + SatMerchandiseCode = request.SatMerchandiseCode, + DeliveryInstructions = request.DeliveryInstructions, + Priority = Enum.TryParse(request.Priority, out var p) ? p : ShipmentPriority.Normal, + Status = ShipmentStatus.PendingApproval, + CreatedAt = DateTime.UtcNow + }; + await _unitOfWork.Shipments.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Envío creado exitosamente"); + } + + public async Task> UpdateAsync( + Guid id, UpdateShipmentRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Shipments.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Envío no encontrado"); + + entity.AssignedRouteId = request.AssignedRouteId; + entity.CurrentStepOrder = request.CurrentStepOrder; + entity.DeliveryInstructions = request.DeliveryInstructions; + if (Enum.TryParse(request.Priority, out var p)) entity.Priority = p; + if (Enum.TryParse(request.Status, out var s)) entity.Status = s; + entity.TruckId = request.TruckId; + entity.DriverId = request.DriverId; + entity.WasQrScanned = request.WasQrScanned; + entity.IsDelayed = request.IsDelayed; + entity.ScheduledDeparture = request.ScheduledDeparture; + entity.PickupWindowStart = request.PickupWindowStart; + entity.PickupWindowEnd = request.PickupWindowEnd; + entity.EstimatedArrival = request.EstimatedArrival; + entity.AssignedAt = request.AssignedAt; + entity.DeliveredAt = request.DeliveredAt; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Shipments.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Envío actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Shipments.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Envío no encontrado"); + _unitOfWork.Shipments.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Envío eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.Shipments.AnyAsync(s => s.Id == id, cancellationToken); + + public async Task GetByTrackingNumberAsync(string trackingNumber, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Shipments.FirstOrDefaultAsync(s => s.TrackingNumber == trackingNumber, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Shipments.GetPagedAsync(request, filter: s => s.TenantId == tenantId, orderBy: q => q.OrderByDescending(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByStatusAsync(Guid tenantId, ShipmentStatus status, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Shipments.GetPagedAsync(request, filter: s => s.TenantId == tenantId && s.Status == status, orderBy: q => q.OrderByDescending(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByDriverAsync(Guid driverId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Shipments.GetPagedAsync(request, filter: s => s.DriverId == driverId, orderBy: q => q.OrderByDescending(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByLocationAsync(Guid locationId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.Shipments.GetPagedAsync(request, filter: s => s.OriginLocationId == locationId || s.DestinationLocationId == locationId, orderBy: q => q.OrderByDescending(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> AssignToDriverAsync(Guid shipmentId, Guid driverId, Guid truckId, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Shipments.GetByIdAsync(shipmentId, cancellationToken); + if (entity == null) return OperationResult.Fail("Envío no encontrado"); + + var driver = await _unitOfWork.Drivers.GetByIdAsync(driverId, cancellationToken); + if (driver == null || driver.Status != DriverStatus.Available) + return OperationResult.Fail("Chofer no encontrado o no disponible"); + + var truck = await _unitOfWork.Trucks.GetByIdAsync(truckId, cancellationToken); + if (truck == null) return OperationResult.Fail("Camión no encontrado"); + + entity.DriverId = driverId; + entity.TruckId = truckId; + entity.AssignedAt = DateTime.UtcNow; + entity.Status = ShipmentStatus.Approved; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.Shipments.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Envío asignado exitosamente"); + } + + public async Task> UpdateStatusAsync(Guid shipmentId, ShipmentStatus newStatus, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Shipments.GetByIdAsync(shipmentId, cancellationToken); + if (entity == null) return OperationResult.Fail("Envío no encontrado"); + + entity.Status = newStatus; + entity.UpdatedAt = DateTime.UtcNow; + if (newStatus == ShipmentStatus.Delivered) entity.DeliveredAt = DateTime.UtcNow; + + _unitOfWork.Shipments.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), $"Estado actualizado a {newStatus}"); + } + + private static string GenerateTrackingNumber() => $"PAR-{DateTime.UtcNow:yyyyMMddHHmmss}-{new Random().Next(1000, 9999)}"; + + private async Task MapToResponseAsync(Domain.Entities.Shipment e, CancellationToken ct) + { + var origin = await _unitOfWork.Locations.GetByIdAsync(e.OriginLocationId, ct); + var dest = await _unitOfWork.Locations.GetByIdAsync(e.DestinationLocationId, ct); + var sender = e.SenderId.HasValue ? await _unitOfWork.Clients.GetByIdAsync(e.SenderId.Value, ct) : null; + var recipient = e.RecipientClientId.HasValue ? await _unitOfWork.Clients.GetByIdAsync(e.RecipientClientId.Value, ct) : null; + var truck = e.TruckId.HasValue ? await _unitOfWork.Trucks.GetByIdAsync(e.TruckId.Value, ct) : null; + + string? driverName = null; + if (e.DriverId.HasValue) + { + var driver = await _unitOfWork.Drivers.GetByIdAsync(e.DriverId.Value, ct); + if (driver != null) + { + var employee = await _unitOfWork.Employees.GetByIdAsync(driver.EmployeeId, ct); + if (employee != null) + { + var user = await _unitOfWork.Users.GetByIdAsync(employee.UserId, ct); + driverName = user?.FullName; + } + } + } + + return new ShipmentResponse(e.Id, e.TrackingNumber, e.QrCodeData ?? "", e.OriginLocationId, origin?.Name ?? "Unknown", + e.DestinationLocationId, dest?.Name ?? "Unknown", e.SenderId, sender?.CompanyName, e.RecipientClientId, recipient?.CompanyName, + e.RecipientName, e.RecipientPhone, e.TotalWeightKg, e.TotalVolumeM3, e.DeclaredValue, e.SatMerchandiseCode, e.DeliveryInstructions, + e.Priority.ToString(), e.Status.ToString(), e.TruckId, truck?.Plate, e.DriverId, driverName, e.WasQrScanned, e.IsDelayed, + e.ScheduledDeparture, e.EstimatedArrival, e.DeliveredAt, e.CreatedAt, e.UpdatedAt); + } +} From 41779a616056aaf213a9e3a5d94752246da6b297 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 18 Dec 2025 02:16:19 +0000 Subject: [PATCH 22/34] v0.5.3: Integration Tests para Services Layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementacion de tests de integracion para validar la calidad de los Services. Agregado: - ServiceTestFixture con UnitOfWork real e InMemory Database - TestIds para IDs conocidos y consistentes en tests - Core Services Tests: TenantServiceTests (10), RoleServiceTests (8), EmployeeServiceTests (6), ClientServiceTests (4) - Shipment Services Tests: ShipmentServiceTests (3) - Fleet Services Tests: TruckServiceTests (8) - Network Services Tests: LocationServiceTests (5) Modificado: - Total de tests: 28 → 72 (incremento de 44 tests nuevos) - Estructura de tests reorganizada en Unit/Services/{Layer} - Corregidos warnings de null reference (CS8602) Verificacion: - Build exitoso: 0 Warnings, 0 Errors - 72/72 tests pasando --- v0.5.3: Integration Tests for Services Layer Implementation of integration tests to validate Services quality. Added: - ServiceTestFixture with real UnitOfWork and InMemory Database - TestIds for known and consistent IDs in tests - Core Services Tests: TenantServiceTests (10), RoleServiceTests (8), EmployeeServiceTests (6), ClientServiceTests (4) - Shipment Services Tests: ShipmentServiceTests (3) - Fleet Services Tests: TruckServiceTests (8) - Network Services Tests: LocationServiceTests (5) Modified: - Total tests: 28 → 72 (44 new tests added) - Tests structure reorganized into Unit/Services/{Layer} - Fixed null reference warnings (CS8602) Verification: - Build successful: 0 Warnings, 0 Errors - 72/72 tests passing --- CHANGELOG.md | 26 +++ README.md | 4 +- api-architecture.md | 4 +- .../Fixtures/ServiceTestFixture.cs | 153 ++++++++++++++ .../Unit/Services/Core/ClientServiceTests.cs | 106 ++++++++++ .../Services/Core/EmployeeServiceTests.cs | 108 ++++++++++ .../Unit/Services/Core/RoleServiceTests.cs | 139 +++++++++++++ .../Unit/Services/Core/TenantServiceTests.cs | 186 ++++++++++++++++++ .../Unit/Services/Fleet/TruckServiceTests.cs | 147 ++++++++++++++ .../Services/Network/LocationServiceTests.cs | 103 ++++++++++ .../Services/Shipment/ShipmentServiceTests.cs | 60 ++++++ 11 files changed, 1032 insertions(+), 4 deletions(-) create mode 100644 backend/tests/Parhelion.Tests/Fixtures/ServiceTestFixture.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Core/ClientServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Core/EmployeeServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Core/RoleServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Core/TenantServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Fleet/TruckServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Network/LocationServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 879fa78..70e15e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.5.3] - 2025-12-18 + +### Agregado + +- **Integration Tests para Services Layer (44 tests nuevos)**: + + - `ServiceTestFixture` - Fixture con UnitOfWork real y datos de prueba + - `TestIds` - IDs conocidos para testing consistente + - **Core Services Tests**: TenantServiceTests (10), RoleServiceTests (8), EmployeeServiceTests (6), ClientServiceTests (4) + - **Shipment Services Tests**: ShipmentServiceTests (3) + - **Fleet Services Tests**: TruckServiceTests (8) + - **Network Services Tests**: LocationServiceTests (5) + +### Modificado + +- Total de tests: 28 → 72 (incremento de 44 tests) +- Estructura de tests reorganizada en Unit/Services/{Layer} + +### Notas Tecnicas + +- Tests usan InMemory Database para aislamiento +- Cada test crea instancia fresca de UnitOfWork +- Cobertura de CRUD, validaciones de duplicados, y filtros + +--- + ## [0.5.2] - 2025-12-17 ### Agregado diff --git a/README.md b/README.md index 248db8e..95257f2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. -> **Estado:** Development Preview v0.5.2 - Services Layer Implementation +> **Estado:** Development Preview v0.5.3 - Services Layer Integration Tests --- @@ -36,7 +36,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in - [x] **API Skeleton:** 22 endpoints base para todas las entidades - [x] **Autenticacion:** JWT con roles SuperAdmin/Admin/Driver/Warehouse - [x] **Repository Pattern:** GenericRepository + UnitOfWork + Soft Delete -- [x] **xUnit Tests:** 28 tests de foundation (paginacion, CRUD) +- [x] **xUnit Tests:** 72 tests (foundation + services integration) - [x] **Services Layer:** 16 interfaces, 15 implementaciones (Core, Shipment, Fleet, Network) ### Gestion de Flotilla diff --git a/api-architecture.md b/api-architecture.md index 2ffc1ad..51058c0 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -4,8 +4,8 @@ Documentacion tecnica de la estructura API-First del backend Parhelion. ## Estado Actual -**Version:** 0.5.2 -**Enfoque:** Services Layer Implementation +**Version:** 0.5.3 +**Enfoque:** Services Layer + Integration Tests **Arquitectura:** Clean Architecture + Domain-Driven Design --- diff --git a/backend/tests/Parhelion.Tests/Fixtures/ServiceTestFixture.cs b/backend/tests/Parhelion.Tests/Fixtures/ServiceTestFixture.cs new file mode 100644 index 0000000..75aa830 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Fixtures/ServiceTestFixture.cs @@ -0,0 +1,153 @@ +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.Interfaces; +using Parhelion.Infrastructure.Data; +using Parhelion.Infrastructure.Repositories; + +namespace Parhelion.Tests.Fixtures; + +/// +/// Fixture para tests de Services con UnitOfWork real. +/// +public class ServiceTestFixture : IDisposable +{ + /// + /// Crea un UnitOfWork con base de datos en memoria. + /// + public (IUnitOfWork UnitOfWork, ParhelionDbContext Context) CreateUnitOfWork() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .EnableSensitiveDataLogging() + .Options; + + var context = new ParhelionDbContext(options); + context.Database.EnsureCreated(); + var unitOfWork = new UnitOfWork(context); + return (unitOfWork, context); + } + + /// + /// Crea un UnitOfWork con datos de prueba sembrados. + /// + public (IUnitOfWork UnitOfWork, ParhelionDbContext Context, TestIds Ids) CreateSeededUnitOfWork() + { + var (unitOfWork, context) = CreateUnitOfWork(); + var ids = SeedTestData(context); + return (unitOfWork, context, ids); + } + + private TestIds SeedTestData(ParhelionDbContext context) + { + var ids = new TestIds(); + + // Roles + var adminRole = new Domain.Entities.Role + { + Id = ids.AdminRoleId, + Name = "Admin", + Description = "Administrador", + CreatedAt = DateTime.UtcNow + }; + var driverRole = new Domain.Entities.Role + { + Id = ids.DriverRoleId, + Name = "Driver", + Description = "Chofer", + CreatedAt = DateTime.UtcNow + }; + context.Roles.AddRange(adminRole, driverRole); + + // Tenant + var tenant = new Domain.Entities.Tenant + { + Id = ids.TenantId, + CompanyName = "Test Company", + ContactEmail = "test@company.com", + FleetSize = 5, + DriverCount = 3, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.Tenants.Add(tenant); + + // User + var user = new Domain.Entities.User + { + Id = ids.UserId, + TenantId = ids.TenantId, + Email = "admin@test.com", + PasswordHash = "hashedpassword", + FullName = "Test Admin", + RoleId = ids.AdminRoleId, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.Users.Add(user); + + // Location + var location = new Domain.Entities.Location + { + Id = ids.LocationId, + TenantId = ids.TenantId, + Code = "MTY", + Name = "Monterrey Hub", + Type = Domain.Enums.LocationType.RegionalHub, + FullAddress = "123 Test Ave", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.Locations.Add(location); + + // Employee + var employee = new Domain.Entities.Employee + { + Id = ids.EmployeeId, + TenantId = ids.TenantId, + UserId = ids.UserId, + Phone = "555-0100", + Rfc = "XAXX010101000", + Curp = "XEXX010101HDFABC00", + HireDate = DateTime.UtcNow.AddYears(-1), + Department = "OPS", + CreatedAt = DateTime.UtcNow + }; + context.Employees.Add(employee); + + // Truck + var truck = new Domain.Entities.Truck + { + Id = ids.TruckId, + TenantId = ids.TenantId, + Plate = "ABC-123", + Model = "Freightliner", + Type = Domain.Enums.TruckType.DryBox, + MaxCapacityKg = 10000, + MaxVolumeM3 = 50, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.Trucks.Add(truck); + + context.SaveChanges(); + return ids; + } + + public void Dispose() { } +} + +/// +/// IDs conocidos para testing. +/// +public class TestIds +{ + public Guid TenantId { get; } = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + public Guid UserId { get; } = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); + public Guid AdminRoleId { get; } = Guid.Parse("11111111-1111-1111-1111-111111111111"); + public Guid DriverRoleId { get; } = Guid.Parse("22222222-2222-2222-2222-222222222222"); + public Guid LocationId { get; } = Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"); + public Guid EmployeeId { get; } = Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"); + public Guid TruckId { get; } = Guid.Parse("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"); +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Core/ClientServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Core/ClientServiceTests.cs new file mode 100644 index 0000000..d9a060e --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Core/ClientServiceTests.cs @@ -0,0 +1,106 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Infrastructure.Services.Core; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Core; + +/// +/// Tests para ClientService. +/// +public class ClientServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public ClientServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new ClientService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetAllAsync(request); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccess() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new ClientService(uow); + var request = new CreateClientRequest( + CompanyName: "Acme Corp", + TradeName: "Acme", + ContactName: "John Doe", + Email: "acme@corp.com", + Phone: "555-1234", + TaxId: "ACM123456789", + LegalName: "Acme Corporation SA de CV", + BillingAddress: "123 Main St", + ShippingAddress: "456 Warehouse Ave", + PreferredProductTypes: "General", + Priority: "High", + Notes: null + ); + + // Act + var result = await service.CreateAsync(request); + + // Assert + Assert.True(result.Success); + Assert.Equal("Acme Corp", result.Data!.CompanyName); + } + + [Fact] + public async Task CreateAsync_DuplicateEmail_ReturnsFailure() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new ClientService(uow); + + // Create first client + await service.CreateAsync(new CreateClientRequest( + "First Client", "First", "Contact", "duplicate@test.com", "555-0000", + "FC123", "First Legal", "First St", "First Shipping", + "General", "Normal", null + )); + + // Try to create with same email + var result = await service.CreateAsync(new CreateClientRequest( + "Second Client", "Second", "Contact", "duplicate@test.com", "555-0001", + "SC123", "Second Legal", "Second St", "Second Shipping", + "General", "Normal", null + )); + + // Assert + Assert.False(result.Success); + Assert.Contains("existe", result.Message?.ToLower() ?? string.Empty); + } + + [Fact] + public async Task DeleteAsync_ExistingClient_ReturnsSuccess() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new ClientService(uow); + var createResult = await service.CreateAsync(new CreateClientRequest( + "Delete Me", "Delete", "Contact", "delete@test.com", "555-0003", + "DEL123", "Delete Legal", "Delete St", "Delete Ship", + "General", "Low", null + )); + + // Act + var result = await service.DeleteAsync(createResult.Data!.Id); + + // Assert + Assert.True(result.Success); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Core/EmployeeServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Core/EmployeeServiceTests.cs new file mode 100644 index 0000000..cb71aec --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Core/EmployeeServiceTests.cs @@ -0,0 +1,108 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Infrastructure.Services.Core; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Core; + +/// +/// Tests para EmployeeService. +/// +public class EmployeeServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public EmployeeServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new EmployeeService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetAllAsync(request); + + // Assert + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task GetByIdAsync_ExistingEmployee_ReturnsEmployee() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new EmployeeService(uow); + + // Act + var result = await service.GetByIdAsync(ids.EmployeeId); + + // Assert + Assert.NotNull(result); + Assert.Equal("XAXX010101000", result.Rfc); + } + + [Fact] + public async Task GetByRfcAsync_ExistingRfc_ReturnsEmployee() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new EmployeeService(uow); + + // Act + var result = await service.GetByRfcAsync("XAXX010101000"); + + // Assert + Assert.NotNull(result); + Assert.Equal(ids.EmployeeId, result.Id); + } + + [Fact] + public async Task GetByTenantAsync_ReturnsTenantEmployees() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new EmployeeService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetByTenantAsync(ids.TenantId, request); + + // Assert + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task GetByDepartmentAsync_ValidDepartment_ReturnsEmployees() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new EmployeeService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetByDepartmentAsync(ids.TenantId, "OPS", request); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public async Task ExistsAsync_ExistingEmployee_ReturnsTrue() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new EmployeeService(uow); + + // Act + var exists = await service.ExistsAsync(ids.EmployeeId); + + // Assert + Assert.True(exists); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Core/RoleServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Core/RoleServiceTests.cs new file mode 100644 index 0000000..1ee1304 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Core/RoleServiceTests.cs @@ -0,0 +1,139 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Infrastructure.Services.Core; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Core; + +/// +/// Tests para RoleService. +/// +public class RoleServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public RoleServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RoleService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetAllAsync(request); + + // Assert + Assert.NotNull(result); + Assert.True(result.TotalCount >= 2); // Admin and Driver roles + } + + [Fact] + public async Task GetByIdAsync_ExistingRole_ReturnsRole() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RoleService(uow); + + // Act + var result = await service.GetByIdAsync(ids.AdminRoleId); + + // Assert + Assert.NotNull(result); + Assert.Equal("Admin", result.Name); + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccess() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new RoleService(uow); + var request = new CreateRoleRequest("Manager", "Gerente de operaciones"); + + // Act + var result = await service.CreateAsync(request); + + // Assert + Assert.True(result.Success); + Assert.Equal("Manager", result.Data!.Name); + } + + [Fact] + public async Task CreateAsync_DuplicateName_ReturnsFailure() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RoleService(uow); + var request = new CreateRoleRequest("Admin", "Duplicate admin"); + + // Act + var result = await service.CreateAsync(request); + + // Assert + Assert.False(result.Success); + } + + [Fact] + public async Task GetByNameAsync_ExistingName_ReturnsRole() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RoleService(uow); + + // Act + var result = await service.GetByNameAsync("Admin"); + + // Assert + Assert.NotNull(result); + Assert.Equal(ids.AdminRoleId, result.Id); + } + + [Fact] + public async Task UpdateAsync_ExistingRole_ReturnsSuccess() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RoleService(uow); + var request = new UpdateRoleRequest("Admin", "Updated Administrator description"); + + // Act + var result = await service.UpdateAsync(ids.AdminRoleId, request); + + // Assert + Assert.True(result.Success); + Assert.Equal("Updated Administrator description", result.Data!.Description); + } + + [Fact] + public async Task DeleteAsync_ExistingRole_ReturnsSuccess() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new RoleService(uow); + var createResult = await service.CreateAsync(new CreateRoleRequest("ToDelete", "To be deleted")); + + // Act + var result = await service.DeleteAsync(createResult.Data!.Id); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task ExistsAsync_ExistingRole_ReturnsTrue() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RoleService(uow); + + // Act + var exists = await service.ExistsAsync(ids.AdminRoleId); + + // Assert + Assert.True(exists); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Core/TenantServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Core/TenantServiceTests.cs new file mode 100644 index 0000000..9da3645 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Core/TenantServiceTests.cs @@ -0,0 +1,186 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Core; +using Parhelion.Infrastructure.Services.Core; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Core; + +/// +/// Tests para TenantService. +/// +public class TenantServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public TenantServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetAllAsync(request); + + // Assert + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + Assert.NotEmpty(result.Items); + } + + [Fact] + public async Task GetByIdAsync_ExistingTenant_ReturnsTenant() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + + // Act + var result = await service.GetByIdAsync(ids.TenantId); + + // Assert + Assert.NotNull(result); + Assert.Equal("Test Company", result.CompanyName); + } + + [Fact] + public async Task GetByIdAsync_NonExistingTenant_ReturnsNull() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + + // Act + var result = await service.GetByIdAsync(Guid.NewGuid()); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccess() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new TenantService(uow); + var request = new CreateTenantRequest("New Tenant Inc", "new@tenant.com", 10, 5); + + // Act + var result = await service.CreateAsync(request); + + // Assert + Assert.True(result.Success); + Assert.NotNull(result.Data); + Assert.Equal("New Tenant Inc", result.Data.CompanyName); + } + + [Fact] + public async Task CreateAsync_DuplicateEmail_ReturnsFailure() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + var request = new CreateTenantRequest("Another Company", "test@company.com", 5, 3); + + // Act + var result = await service.CreateAsync(request); + + // Assert + Assert.False(result.Success); + Assert.Contains("existe", result.Message?.ToLower() ?? string.Empty); + } + + [Fact] + public async Task UpdateAsync_ExistingTenant_ReturnsSuccess() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + var request = new UpdateTenantRequest("Updated Company", "updated@company.com", 20, 10, true); + + // Act + var result = await service.UpdateAsync(ids.TenantId, request); + + // Assert + Assert.True(result.Success); + Assert.Equal("Updated Company", result.Data!.CompanyName); + } + + [Fact] + public async Task DeleteAsync_ExistingTenant_ReturnsSuccess() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new TenantService(uow); + var createResult = await service.CreateAsync(new CreateTenantRequest("To Delete", "delete@test.com", 1, 1)); + + // Act + var result = await service.DeleteAsync(createResult.Data!.Id); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task ExistsAsync_ExistingTenant_ReturnsTrue() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + + // Act + var exists = await service.ExistsAsync(ids.TenantId); + + // Assert + Assert.True(exists); + } + + [Fact] + public async Task GetByEmailAsync_ExistingEmail_ReturnsTenant() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + + // Act + var result = await service.GetByEmailAsync("test@company.com"); + + // Assert + Assert.NotNull(result); + Assert.Equal(ids.TenantId, result.Id); + } + + [Fact] + public async Task SetActiveStatusAsync_TogglesState() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + + // Act + var result = await service.SetActiveStatusAsync(ids.TenantId, false); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task GetActiveAsync_ReturnsOnlyActive() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TenantService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetActiveAsync(request); + + // Assert + Assert.NotNull(result); + Assert.All(result.Items, item => Assert.True(item.IsActive)); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Fleet/TruckServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Fleet/TruckServiceTests.cs new file mode 100644 index 0000000..4400734 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Fleet/TruckServiceTests.cs @@ -0,0 +1,147 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Infrastructure.Services.Fleet; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Fleet; + +/// +/// Tests para TruckService. +/// +public class TruckServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public TruckServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TruckService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetAllAsync(request); + + // Assert + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task GetByIdAsync_ExistingTruck_ReturnsTruck() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TruckService(uow); + + // Act + var result = await service.GetByIdAsync(ids.TruckId); + + // Assert + Assert.NotNull(result); + Assert.Equal("ABC-123", result.Plate); + } + + [Fact] + public async Task GetByPlateAsync_ExistingPlate_ReturnsTruck() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TruckService(uow); + + // Act + var result = await service.GetByPlateAsync("ABC-123"); + + // Assert + Assert.NotNull(result); + Assert.Equal(ids.TruckId, result.Id); + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccess() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TruckService(uow); + var request = new CreateTruckRequest( + Plate: "XYZ-789", + Model: "Kenworth", + Type: "DryBox", + MaxCapacityKg: 15000, + MaxVolumeM3: 60, + Vin: "1HGBH41JXMN109186", + EngineNumber: "ENG123", + Year: 2023, + Color: "White", + InsurancePolicy: "POL-001", + InsuranceExpiration: DateTime.UtcNow.AddYears(1), + VerificationNumber: "VER-001", + VerificationExpiration: DateTime.UtcNow.AddMonths(6) + ); + + // Act + var result = await service.CreateAsync(request); + + // Assert + Assert.True(result.Success); + Assert.Equal("XYZ-789", result.Data!.Plate); + } + + [Fact] + public async Task CreateAsync_DuplicatePlate_ReturnsFailure() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TruckService(uow); + var request = new CreateTruckRequest( + Plate: "ABC-123", // Existing plate + Model: "Duplicate", + Type: "DryBox", + MaxCapacityKg: 10000, + MaxVolumeM3: 50, + Vin: null, EngineNumber: null, Year: null, Color: null, + InsurancePolicy: null, InsuranceExpiration: null, + VerificationNumber: null, VerificationExpiration: null + ); + + // Act + var result = await service.CreateAsync(request); + + // Assert + Assert.False(result.Success); + Assert.Contains("ABC-123", result.Message); + } + + [Fact] + public async Task ExistsAsync_ExistingTruck_ReturnsTrue() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TruckService(uow); + + // Act + var exists = await service.ExistsAsync(ids.TruckId); + + // Assert + Assert.True(exists); + } + + [Fact] + public async Task SetActiveStatusAsync_ChangesStatus() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new TruckService(uow); + + // Act + var result = await service.SetActiveStatusAsync(ids.TruckId, false); + + // Assert + Assert.True(result.Success); + Assert.False(result.Data!.IsActive); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Network/LocationServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Network/LocationServiceTests.cs new file mode 100644 index 0000000..5d916ae --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Network/LocationServiceTests.cs @@ -0,0 +1,103 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Infrastructure.Services.Network; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Network; + +/// +/// Tests para LocationService. +/// +public class LocationServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public LocationServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new LocationService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetAllAsync(request); + + // Assert + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task GetByIdAsync_ExistingLocation_ReturnsLocation() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new LocationService(uow); + + // Act + var result = await service.GetByIdAsync(ids.LocationId); + + // Assert + Assert.NotNull(result); + Assert.Equal("MTY", result.Code); + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccess() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new LocationService(uow); + var request = new CreateLocationRequest( + Code: "GDL", + Name: "Guadalajara Hub", + Type: "RegionalHub", + FullAddress: "789 Logistics Ave", + Latitude: 20.6597m, + Longitude: -103.3496m, + CanReceive: true, + CanDispatch: true, + IsInternal: true + ); + + // Act + var result = await service.CreateAsync(request); + + // Assert + Assert.True(result.Success); + Assert.Equal("GDL", result.Data!.Code); + } + + [Fact] + public async Task ExistsAsync_ExistingLocation_ReturnsTrue() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new LocationService(uow); + + // Act + var exists = await service.ExistsAsync(ids.LocationId); + + // Assert + Assert.True(exists); + } + + [Fact] + public async Task SearchByNameAsync_MatchingTerm_ReturnsResults() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new LocationService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.SearchByNameAsync(ids.TenantId, "Monterrey", request); + + // Assert + Assert.NotNull(result); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs new file mode 100644 index 0000000..4d2f433 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs @@ -0,0 +1,60 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Shipment; +using Parhelion.Infrastructure.Services.Shipment; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Shipment; + +/// +/// Tests para ShipmentService. +/// +public class ShipmentServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public ShipmentServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new ShipmentService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + // Act + var result = await service.GetAllAsync(request); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public async Task GetByIdAsync_NonExisting_ReturnsNull() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new ShipmentService(uow); + + // Act + var result = await service.GetByIdAsync(Guid.NewGuid()); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task ExistsAsync_NonExisting_ReturnsFalse() + { + // Arrange + var (uow, ctx) = _fixture.CreateUnitOfWork(); + var service = new ShipmentService(uow); + + // Act + var exists = await service.ExistsAsync(Guid.NewGuid()); + + // Assert + Assert.False(exists); + } +} From bfbe6bb488a050ac09f119ca7958bef1e9565a8c Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 18 Dec 2025 02:35:11 +0000 Subject: [PATCH 23/34] v0.5.4: Swagger/OpenAPI + Controllers + Business Logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swagger/OpenAPI Documentation: - OpenAPI Info con version, titulo, descripcion, contacto - JWT Bearer Security Scheme para autorizacion - XML Comments habilitados (GenerateDocumentationFile) - Atributos Produces/Consumes en Controllers Controllers Refactorizados (5): - TrucksController → ITruckService - DriversController → IDriverService - FleetLogsController → IFleetLogService - LocationsController → ILocationService - RouteBlueprintsController → IRouteService Business Logic: - ValidateStatusTransition en ShipmentService - Workflow: PendingApproval → Approved → Loaded → InTransit → AtHub/OutForDelivery → Delivered Build: 0 Warnings, 0 Errors Tests: 72/72 Passed --- v0.5.4: Swagger/OpenAPI + Controllers + Business Logic Swagger/OpenAPI Documentation: - OpenAPI Info with version, title, description, contact - JWT Bearer Security Scheme for authorization - XML Comments enabled (GenerateDocumentationFile) - Produces/Consumes attributes on Controllers Controllers Refactored (5): - TrucksController → ITruckService - DriversController → IDriverService - FleetLogsController → IFleetLogService - LocationsController → ILocationService - RouteBlueprintsController → IRouteService Business Logic: - ValidateStatusTransition in ShipmentService - Workflow: PendingApproval → Approved → Loaded → InTransit → AtHub/OutForDelivery → Delivered Build: 0 Warnings, 0 Errors Tests: 72/72 Passed --- CHANGELOG.md | 41 +++++ README.md | 2 +- api-architecture.md | 4 +- .../Controllers/DriversController.cs | 161 +++++++---------- .../Controllers/FleetLogsController.cs | 134 ++++---------- .../Controllers/LocationsController.cs | 126 +++++--------- .../Controllers/RouteBlueprintsController.cs | 111 +++++------- .../Controllers/ShipmentsController.cs | 3 + .../Controllers/TrucksController.cs | 164 +++++++----------- .../src/Parhelion.API/Parhelion.API.csproj | 2 + backend/src/Parhelion.API/Program.cs | 57 +++++- .../Services/Shipment/ShipmentService.cs | 49 +++++- 12 files changed, 409 insertions(+), 445 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e15e4..e267f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,47 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.5.4] - 2025-12-18 + +### Agregado + +- **Swagger/OpenAPI Documentation**: + + - OpenAPI Info con version, titulo, descripcion, contacto + - JWT Bearer Security Scheme con autorizacion + - XML Comments habilitados para documentacion automatica + - Atributos `[Produces]` y `[Consumes]` en Controllers + +- **Business Logic - Shipment Workflow**: + - Validación de transiciones de estado (`ValidateStatusTransition`) + - Workflow: PendingApproval → Approved → Loaded → InTransit → AtHub/OutForDelivery → Delivered + - Estado Exception para manejo de problemas con recuperación + +### Modificado + +- **Controllers Refactorizados (5 total)**: + + - `TrucksController` → `ITruckService` + - `DriversController` → `IDriverService` + - `FleetLogsController` → `IFleetLogService` + - `LocationsController` → `ILocationService` + - `RouteBlueprintsController` → `IRouteService` + +- **Nuevos Endpoints**: + - `PATCH /api/drivers/{id}/assign-truck` - Asignar camión a chofer + - `PATCH /api/drivers/{id}/status` - Actualizar estatus de chofer + - `POST /api/fleet-logs/start-usage` - Iniciar uso de camión + - `POST /api/fleet-logs/end-usage` - Finalizar uso de camión + - `GET /api/route-blueprints/{id}/steps` - Obtener pasos de ruta + +### Notas Tecnicas + +- Controllers ahora son thin wrappers que delegan a Services +- Validación de workflow previene transiciones inválidas de estado +- Preparación para tests de Business Logic en v0.5.5 + +--- + ## [0.5.3] - 2025-12-18 ### Agregado diff --git a/README.md b/README.md index 95257f2..f75cb5f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. -> **Estado:** Development Preview v0.5.3 - Services Layer Integration Tests +> **Estado:** Development Preview v0.5.4 - Swagger/OpenAPI + Business Logic --- diff --git a/api-architecture.md b/api-architecture.md index 51058c0..8fc49bf 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -4,8 +4,8 @@ Documentacion tecnica de la estructura API-First del backend Parhelion. ## Estado Actual -**Version:** 0.5.3 -**Enfoque:** Services Layer + Integration Tests +**Version:** 0.5.4 +**Enfoque:** Swagger/OpenAPI + Business Logic **Arquitectura:** Clean Architecture + Domain-Driven Design --- diff --git a/backend/src/Parhelion.API/Controllers/DriversController.cs b/backend/src/Parhelion.API/Controllers/DriversController.cs index 27a9c81..eb9be1f 100644 --- a/backend/src/Parhelion.API/Controllers/DriversController.cs +++ b/backend/src/Parhelion.API/Controllers/DriversController.cs @@ -1,147 +1,120 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Fleet; -using Parhelion.Domain.Entities; +using Parhelion.Application.Interfaces.Services; using Parhelion.Domain.Enums; -using Parhelion.Infrastructure.Data; namespace Parhelion.API.Controllers; /// /// Controlador para gestión de choferes. +/// CRUD, filtro por estatus, asignación de camiones y actualización de estado. /// [ApiController] [Route("api/drivers")] [Authorize] +[Produces("application/json")] +[Consumes("application/json")] public class DriversController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IDriverService _driverService; - public DriversController(ParhelionDbContext context) + public DriversController(IDriverService driverService) { - _context = context; + _driverService = driverService; } [HttpGet] - public async Task>> GetAll() + public async Task GetAll([FromQuery] PagedRequest request) { - var items = await _context.Drivers - .Include(x => x.Employee).ThenInclude(e => e.User) - .Include(x => x.DefaultTruck) - .Include(x => x.CurrentTruck) - .Where(x => !x.IsDeleted) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _driverService.GetAllAsync(request); + return Ok(result); } [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task GetById(Guid id) { - var item = await _context.Drivers - .Include(x => x.Employee).ThenInclude(e => e.User) - .Include(x => x.DefaultTruck) - .Include(x => x.CurrentTruck) - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Chofer no encontrado" }); - return Ok(MapToResponse(item)); + var result = await _driverService.GetByIdAsync(id); + if (result == null) return NotFound(new { error = "Chofer no encontrado" }); + return Ok(result); } [HttpGet("active")] - public async Task>> Active() + public async Task Active([FromQuery] PagedRequest request) { - var items = await _context.Drivers - .Include(x => x.Employee).ThenInclude(e => e.User) - .Include(x => x.DefaultTruck) - .Include(x => x.CurrentTruck) - .Where(x => !x.IsDeleted && x.Status == DriverStatus.Available) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var tenantId = GetTenantId(); + if (tenantId == null) return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _driverService.GetByStatusAsync(tenantId.Value, DriverStatus.Available, request); + return Ok(result); } [HttpGet("by-status/{status}")] - public async Task>> ByStatus(string status) + public async Task ByStatus(string status, [FromQuery] PagedRequest request) { if (!Enum.TryParse(status, out var driverStatus)) return BadRequest(new { error = "Estatus de chofer inválido" }); - var items = await _context.Drivers - .Include(x => x.Employee).ThenInclude(e => e.User) - .Include(x => x.DefaultTruck) - .Include(x => x.CurrentTruck) - .Where(x => !x.IsDeleted && x.Status == driverStatus) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var tenantId = GetTenantId(); + if (tenantId == null) return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _driverService.GetByStatusAsync(tenantId.Value, driverStatus, request); + return Ok(result); } [HttpPost] - public async Task> Create([FromBody] CreateDriverRequest request) + public async Task Create([FromBody] CreateDriverRequest request) { - var item = new Driver - { - Id = Guid.NewGuid(), - EmployeeId = request.EmployeeId, - LicenseNumber = request.LicenseNumber, - LicenseType = request.LicenseType, - LicenseExpiration = request.LicenseExpiration, - DefaultTruckId = request.DefaultTruckId, - Status = Enum.TryParse(request.Status, out var s) ? s : DriverStatus.Available, - CreatedAt = DateTime.UtcNow - }; - - _context.Drivers.Add(item); - await _context.SaveChangesAsync(); - - item = await _context.Drivers - .Include(x => x.Employee).ThenInclude(e => e.User) - .Include(x => x.DefaultTruck) - .Include(x => x.CurrentTruck) - .FirstAsync(x => x.Id == item.Id); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + var result = await _driverService.CreateAsync(request); + if (!result.Success) + return Conflict(new { error = result.Message }); + return CreatedAtAction(nameof(GetById), new { id = result.Data!.Id }, result.Data); } [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateDriverRequest request) + public async Task Update(Guid id, [FromBody] UpdateDriverRequest request) { - var item = await _context.Drivers - .Include(x => x.Employee).ThenInclude(e => e.User) - .Include(x => x.DefaultTruck) - .Include(x => x.CurrentTruck) - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Chofer no encontrado" }); - - item.LicenseNumber = request.LicenseNumber; - item.LicenseType = request.LicenseType; - item.LicenseExpiration = request.LicenseExpiration; - item.DefaultTruckId = request.DefaultTruckId; - item.CurrentTruckId = request.CurrentTruckId; - item.Status = Enum.TryParse(request.Status, out var s) ? s : item.Status; - item.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - return Ok(MapToResponse(item)); + var result = await _driverService.UpdateAsync(id, request); + if (!result.Success) + return (result.Message?.Contains("no encontrado") ?? false) + ? NotFound(new { error = result.Message }) + : BadRequest(new { error = result.Message }); + return Ok(result.Data); } [HttpDelete("{id:guid}")] public async Task Delete(Guid id) { - var item = await _context.Drivers.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Chofer no encontrado" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _driverService.DeleteAsync(id); + if (!result.Success) return NotFound(new { error = result.Message }); return NoContent(); } - private static DriverResponse MapToResponse(Driver x) => new( - x.Id, x.EmployeeId, x.Employee?.User?.FullName ?? "", - x.LicenseNumber, x.LicenseType, x.LicenseExpiration, - x.DefaultTruckId, x.DefaultTruck?.Plate, - x.CurrentTruckId, x.CurrentTruck?.Plate, - x.Status.ToString(), x.CreatedAt, x.UpdatedAt - ); + [HttpPatch("{id:guid}/assign-truck")] + public async Task AssignTruck(Guid id, [FromBody] AssignTruckRequest request) + { + var result = await _driverService.AssignTruckAsync(id, request.TruckId); + if (!result.Success) return BadRequest(new { error = result.Message }); + return Ok(result.Data); + } + + [HttpPatch("{id:guid}/status")] + public async Task UpdateStatus(Guid id, [FromBody] string status) + { + if (!Enum.TryParse(status, out var driverStatus)) + return BadRequest(new { error = "Estatus inválido" }); + + var result = await _driverService.UpdateStatusAsync(id, driverStatus); + if (!result.Success) return NotFound(new { error = result.Message }); + return Ok(result.Data); + } + + private Guid? GetTenantId() + { + var claim = User.FindFirst("tenant_id"); + return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null; + } } + +public record AssignTruckRequest(Guid TruckId); diff --git a/backend/src/Parhelion.API/Controllers/FleetLogsController.cs b/backend/src/Parhelion.API/Controllers/FleetLogsController.cs index b5b9ebe..fe6bcd5 100644 --- a/backend/src/Parhelion.API/Controllers/FleetLogsController.cs +++ b/backend/src/Parhelion.API/Controllers/FleetLogsController.cs @@ -1,10 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Fleet; -using Parhelion.Domain.Entities; -using Parhelion.Domain.Enums; -using Parhelion.Infrastructure.Data; +using Parhelion.Application.Interfaces.Services; namespace Parhelion.API.Controllers; @@ -17,124 +15,64 @@ namespace Parhelion.API.Controllers; [Authorize] public class FleetLogsController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IFleetLogService _fleetLogService; - public FleetLogsController(ParhelionDbContext context) + public FleetLogsController(IFleetLogService fleetLogService) { - _context = context; + _fleetLogService = fleetLogService; } [HttpGet] - public async Task>> GetAll() + public async Task GetAll([FromQuery] PagedRequest request) { - var items = await _context.FleetLogs - .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) - .Include(x => x.OldTruck) - .Include(x => x.NewTruck) - .Include(x => x.CreatedBy) - .Where(x => !x.IsDeleted) - .OrderByDescending(x => x.Timestamp) - .Take(100) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _fleetLogService.GetAllAsync(request); + return Ok(result); } [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task GetById(Guid id) { - var item = await _context.FleetLogs - .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) - .Include(x => x.OldTruck) - .Include(x => x.NewTruck) - .Include(x => x.CreatedBy) - .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Log no encontrado" }); - return Ok(MapToResponse(item)); + var result = await _fleetLogService.GetByIdAsync(id); + if (result == null) return NotFound(new { error = "Log no encontrado" }); + return Ok(result); } [HttpGet("by-driver/{driverId:guid}")] - public async Task>> ByDriver(Guid driverId) + public async Task ByDriver(Guid driverId, [FromQuery] PagedRequest request) { - var items = await _context.FleetLogs - .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) - .Include(x => x.OldTruck) - .Include(x => x.NewTruck) - .Include(x => x.CreatedBy) - .Where(x => !x.IsDeleted && x.DriverId == driverId) - .OrderByDescending(x => x.Timestamp) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _fleetLogService.GetByDriverAsync(driverId, request); + return Ok(result); } [HttpGet("by-truck/{truckId:guid}")] - public async Task>> ByTruck(Guid truckId) + public async Task ByTruck(Guid truckId, [FromQuery] PagedRequest request) { - var items = await _context.FleetLogs - .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) - .Include(x => x.OldTruck) - .Include(x => x.NewTruck) - .Include(x => x.CreatedBy) - .Where(x => !x.IsDeleted && (x.OldTruckId == truckId || x.NewTruckId == truckId)) - .OrderByDescending(x => x.Timestamp) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _fleetLogService.GetByTruckAsync(truckId, request); + return Ok(result); } - [HttpPost] - public async Task> Create([FromBody] CreateFleetLogRequest request) + [HttpPost("start-usage")] + public async Task StartUsage([FromBody] StartUsageRequest request) { - var tenantIdClaim = User.FindFirst("tenant_id"); - if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) - return Unauthorized(new { error = "No se pudo determinar el tenant" }); - - var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); - if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId)) - return Unauthorized(new { error = "No se pudo determinar el usuario" }); - - var item = new FleetLog - { - Id = Guid.NewGuid(), - TenantId = tenantId, - DriverId = request.DriverId, - OldTruckId = request.OldTruckId, - NewTruckId = request.NewTruckId, - Reason = Enum.TryParse(request.Reason, out var r) ? r : FleetLogReason.Reassignment, - Timestamp = DateTime.UtcNow, - CreatedByUserId = userId, - CreatedAt = DateTime.UtcNow - }; - - _context.FleetLogs.Add(item); - - // Update driver's current truck - var driver = await _context.Drivers.FirstOrDefaultAsync(d => d.Id == request.DriverId); - if (driver != null) - { - driver.CurrentTruckId = request.NewTruckId; - } + var result = await _fleetLogService.StartUsageAsync(request.DriverId, request.TruckId); + if (!result.Success) return BadRequest(new { error = result.Message }); + return Ok(result.Data); + } - await _context.SaveChangesAsync(); + [HttpPost("end-usage")] + public async Task EndUsage([FromBody] EndUsageRequest request) + { + // Get active log for driver and end it + var activeLog = await _fleetLogService.GetActiveLogForDriverAsync(request.DriverId); + if (activeLog == null) return NotFound(new { error = "No hay uso activo para este chofer" }); - item = await _context.FleetLogs - .Include(x => x.Driver).ThenInclude(d => d.Employee).ThenInclude(e => e.User) - .Include(x => x.OldTruck) - .Include(x => x.NewTruck) - .Include(x => x.CreatedBy) - .FirstAsync(x => x.Id == item.Id); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + var result = await _fleetLogService.EndUsageAsync(activeLog.Id, request.EndOdometer); + if (!result.Success) return BadRequest(new { error = result.Message }); + return Ok(result.Data); } // No PUT/DELETE - logs are immutable - - private static FleetLogResponse MapToResponse(FleetLog x) => new( - x.Id, x.DriverId, x.Driver?.Employee?.User?.FullName ?? "", - x.OldTruckId, x.OldTruck?.Plate, - x.NewTruckId, x.NewTruck?.Plate ?? "", - x.Reason.ToString(), x.Timestamp, - x.CreatedByUserId, x.CreatedBy?.FullName ?? "", - x.CreatedAt - ); } + +public record StartUsageRequest(Guid DriverId, Guid TruckId); +public record EndUsageRequest(Guid DriverId, decimal? EndOdometer = null); diff --git a/backend/src/Parhelion.API/Controllers/LocationsController.cs b/backend/src/Parhelion.API/Controllers/LocationsController.cs index e7510d2..a81fe43 100644 --- a/backend/src/Parhelion.API/Controllers/LocationsController.cs +++ b/backend/src/Parhelion.API/Controllers/LocationsController.cs @@ -1,10 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Warehouse; -using Parhelion.Domain.Entities; +using Parhelion.Application.Interfaces.Services; using Parhelion.Domain.Enums; -using Parhelion.Infrastructure.Data; namespace Parhelion.API.Controllers; @@ -16,115 +15,82 @@ namespace Parhelion.API.Controllers; [Authorize] public class LocationsController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly ILocationService _locationService; - public LocationsController(ParhelionDbContext context) + public LocationsController(ILocationService locationService) { - _context = context; + _locationService = locationService; } [HttpGet] - public async Task>> GetAll() + public async Task GetAll([FromQuery] PagedRequest request) { - var items = await _context.Locations - .Where(x => !x.IsDeleted) - .OrderBy(x => x.Code) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _locationService.GetAllAsync(request); + return Ok(result); } [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task GetById(Guid id) { - var item = await _context.Locations.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Ubicación no encontrada" }); - return Ok(MapToResponse(item)); + var result = await _locationService.GetByIdAsync(id); + if (result == null) return NotFound(new { error = "Ubicación no encontrada" }); + return Ok(result); } [HttpGet("by-type/{type}")] - public async Task>> ByType(string type) + public async Task ByType(string type, [FromQuery] PagedRequest request) { if (!Enum.TryParse(type, out var locType)) return BadRequest(new { error = "Tipo de ubicación inválido" }); - var items = await _context.Locations - .Where(x => !x.IsDeleted && x.Type == locType) - .OrderBy(x => x.Code) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var tenantId = GetTenantId(); + if (tenantId == null) return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _locationService.GetByTypeAsync(tenantId.Value, locType, request); + return Ok(result); } - [HttpPost] - public async Task> Create([FromBody] CreateLocationRequest request) + [HttpGet("search")] + public async Task Search([FromQuery] string name, [FromQuery] PagedRequest request) { - var tenantIdClaim = User.FindFirst("tenant_id"); - if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) - return Unauthorized(new { error = "No se pudo determinar el tenant" }); - - var existing = await _context.Locations.AnyAsync(x => x.Code == request.Code && !x.IsDeleted); - if (existing) return Conflict(new { error = $"Ya existe ubicación con código '{request.Code}'" }); + var tenantId = GetTenantId(); + if (tenantId == null) return Unauthorized(new { error = "No se pudo determinar el tenant" }); - var item = new Location - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Code = request.Code, - Name = request.Name, - Type = Enum.TryParse(request.Type, out var t) ? t : LocationType.Warehouse, - FullAddress = request.FullAddress, - Latitude = request.Latitude, - Longitude = request.Longitude, - CanReceive = request.CanReceive, - CanDispatch = request.CanDispatch, - IsInternal = request.IsInternal, - IsActive = true, - CreatedAt = DateTime.UtcNow - }; + var result = await _locationService.SearchByNameAsync(tenantId.Value, name, request); + return Ok(result); + } - _context.Locations.Add(item); - await _context.SaveChangesAsync(); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + [HttpPost] + public async Task Create([FromBody] CreateLocationRequest request) + { + var result = await _locationService.CreateAsync(request); + if (!result.Success) + return Conflict(new { error = result.Message }); + return CreatedAtAction(nameof(GetById), new { id = result.Data!.Id }, result.Data); } [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateLocationRequest request) + public async Task Update(Guid id, [FromBody] UpdateLocationRequest request) { - var item = await _context.Locations.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Ubicación no encontrada" }); - - item.Code = request.Code; - item.Name = request.Name; - item.Type = Enum.TryParse(request.Type, out var t) ? t : item.Type; - item.FullAddress = request.FullAddress; - item.Latitude = request.Latitude; - item.Longitude = request.Longitude; - item.CanReceive = request.CanReceive; - item.CanDispatch = request.CanDispatch; - item.IsInternal = request.IsInternal; - item.IsActive = request.IsActive; - item.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - return Ok(MapToResponse(item)); + var result = await _locationService.UpdateAsync(id, request); + if (!result.Success) + return (result.Message?.Contains("no encontrad") ?? false) + ? NotFound(new { error = result.Message }) + : BadRequest(new { error = result.Message }); + return Ok(result.Data); } [HttpDelete("{id:guid}")] public async Task Delete(Guid id) { - var item = await _context.Locations.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Ubicación no encontrada" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _locationService.DeleteAsync(id); + if (!result.Success) return NotFound(new { error = result.Message }); return NoContent(); } - private static LocationResponse MapToResponse(Location x) => new( - x.Id, x.Code, x.Name, x.Type.ToString(), x.FullAddress, - x.Latitude, x.Longitude, x.CanReceive, x.CanDispatch, - x.IsInternal, x.IsActive, x.CreatedAt, x.UpdatedAt - ); + private Guid? GetTenantId() + { + var claim = User.FindFirst("tenant_id"); + return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null; + } } diff --git a/backend/src/Parhelion.API/Controllers/RouteBlueprintsController.cs b/backend/src/Parhelion.API/Controllers/RouteBlueprintsController.cs index 77deed1..e14ca68 100644 --- a/backend/src/Parhelion.API/Controllers/RouteBlueprintsController.cs +++ b/backend/src/Parhelion.API/Controllers/RouteBlueprintsController.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Network; -using Parhelion.Domain.Entities; -using Parhelion.Infrastructure.Data; +using Parhelion.Application.Interfaces.Services; namespace Parhelion.API.Controllers; @@ -15,98 +14,76 @@ namespace Parhelion.API.Controllers; [Authorize] public class RouteBlueprintsController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IRouteService _routeService; - public RouteBlueprintsController(ParhelionDbContext context) + public RouteBlueprintsController(IRouteService routeService) { - _context = context; + _routeService = routeService; } [HttpGet] - public async Task>> GetAll() + public async Task GetAll([FromQuery] PagedRequest request) { - var items = await _context.RouteBlueprints - .Where(x => !x.IsDeleted) - .OrderBy(x => x.Name) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _routeService.GetAllAsync(request); + return Ok(result); } [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task GetById(Guid id) { - var item = await _context.RouteBlueprints.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Ruta no encontrada" }); - return Ok(MapToResponse(item)); + var result = await _routeService.GetByIdAsync(id); + if (result == null) return NotFound(new { error = "Ruta no encontrada" }); + return Ok(result); } [HttpGet("active")] - public async Task>> Active() + public async Task Active([FromQuery] PagedRequest request) { - var items = await _context.RouteBlueprints - .Where(x => !x.IsDeleted && x.IsActive) - .OrderBy(x => x.Name) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var tenantId = GetTenantId(); + if (tenantId == null) return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _routeService.GetActiveAsync(tenantId.Value, request); + return Ok(result); } - [HttpPost] - public async Task> Create([FromBody] CreateRouteBlueprintRequest request) + [HttpGet("{id:guid}/steps")] + public async Task GetSteps(Guid id) { - var tenantIdClaim = User.FindFirst("tenant_id"); - if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) - return Unauthorized(new { error = "No se pudo determinar el tenant" }); - - var item = new RouteBlueprint - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Name = request.Name, - Description = request.Description, - TotalSteps = 0, - TotalTransitTime = TimeSpan.Zero, - IsActive = true, - CreatedAt = DateTime.UtcNow - }; + var result = await _routeService.GetStepsAsync(id); + return Ok(result); + } - _context.RouteBlueprints.Add(item); - await _context.SaveChangesAsync(); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + [HttpPost] + public async Task Create([FromBody] CreateRouteBlueprintRequest request) + { + var result = await _routeService.CreateAsync(request); + if (!result.Success) + return Conflict(new { error = result.Message }); + return CreatedAtAction(nameof(GetById), new { id = result.Data!.Id }, result.Data); } [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateRouteBlueprintRequest request) + public async Task Update(Guid id, [FromBody] UpdateRouteBlueprintRequest request) { - var item = await _context.RouteBlueprints.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Ruta no encontrada" }); - - item.Name = request.Name; - item.Description = request.Description; - item.TotalSteps = request.TotalSteps; - item.TotalTransitTime = request.TotalTransitTime; - item.IsActive = request.IsActive; - item.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - return Ok(MapToResponse(item)); + var result = await _routeService.UpdateAsync(id, request); + if (!result.Success) + return (result.Message?.Contains("no encontrad") ?? false) + ? NotFound(new { error = result.Message }) + : BadRequest(new { error = result.Message }); + return Ok(result.Data); } [HttpDelete("{id:guid}")] public async Task Delete(Guid id) { - var item = await _context.RouteBlueprints.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Ruta no encontrada" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _routeService.DeleteAsync(id); + if (!result.Success) return NotFound(new { error = result.Message }); return NoContent(); } - private static RouteBlueprintResponse MapToResponse(RouteBlueprint x) => new( - x.Id, x.Name, x.Description, x.TotalSteps, x.TotalTransitTime, - x.IsActive, x.CreatedAt, x.UpdatedAt - ); + private Guid? GetTenantId() + { + var claim = User.FindFirst("tenant_id"); + return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null; + } } diff --git a/backend/src/Parhelion.API/Controllers/ShipmentsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentsController.cs index bab2d3a..82e1278 100644 --- a/backend/src/Parhelion.API/Controllers/ShipmentsController.cs +++ b/backend/src/Parhelion.API/Controllers/ShipmentsController.cs @@ -9,10 +9,13 @@ namespace Parhelion.API.Controllers; /// /// Controlador para gestión de envíos. +/// Endpoints para CRUD, tracking, asignación y workflow de estados. /// [ApiController] [Route("api/shipments")] [Authorize] +[Produces("application/json")] +[Consumes("application/json")] public class ShipmentsController : ControllerBase { private readonly IShipmentService _shipmentService; diff --git a/backend/src/Parhelion.API/Controllers/TrucksController.cs b/backend/src/Parhelion.API/Controllers/TrucksController.cs index fecdb58..b74596f 100644 --- a/backend/src/Parhelion.API/Controllers/TrucksController.cs +++ b/backend/src/Parhelion.API/Controllers/TrucksController.cs @@ -1,153 +1,115 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Fleet; -using Parhelion.Domain.Entities; +using Parhelion.Application.Interfaces.Services; using Parhelion.Domain.Enums; -using Parhelion.Infrastructure.Data; namespace Parhelion.API.Controllers; /// -/// Controlador para gestión de camiones. +/// Controlador para gestión de camiones de la flota. +/// CRUD completo, búsqueda por placa, filtro por tipo y gestión de estatus. /// [ApiController] [Route("api/trucks")] [Authorize] +[Produces("application/json")] +[Consumes("application/json")] public class TrucksController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly ITruckService _truckService; - public TrucksController(ParhelionDbContext context) + public TrucksController(ITruckService truckService) { - _context = context; + _truckService = truckService; } [HttpGet] - public async Task>> GetAll() + public async Task GetAll([FromQuery] PagedRequest request) { - var items = await _context.Trucks - .Where(x => !x.IsDeleted) - .OrderBy(x => x.Plate) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _truckService.GetAllAsync(request); + return Ok(result); } [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task GetById(Guid id) { - var item = await _context.Trucks.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Camión no encontrado" }); - return Ok(MapToResponse(item)); + var result = await _truckService.GetByIdAsync(id); + if (result == null) return NotFound(new { error = "Camión no encontrado" }); + return Ok(result); } [HttpGet("available")] - public async Task>> Available() + public async Task Available([FromQuery] PagedRequest request) { - var items = await _context.Trucks - .Where(x => !x.IsDeleted && x.IsActive) - .OrderBy(x => x.Plate) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var tenantId = GetTenantId(); + if (tenantId == null) return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _truckService.GetAvailableAsync(tenantId.Value, request); + return Ok(result); } [HttpGet("by-type/{type}")] - public async Task>> ByType(string type) + public async Task ByType(string type, [FromQuery] PagedRequest request) { if (!Enum.TryParse(type, out var truckType)) return BadRequest(new { error = "Tipo de camión inválido" }); - var items = await _context.Trucks - .Where(x => !x.IsDeleted && x.Type == truckType) - .OrderBy(x => x.Plate) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var tenantId = GetTenantId(); + if (tenantId == null) return Unauthorized(new { error = "No se pudo determinar el tenant" }); + + var result = await _truckService.GetByTypeAsync(tenantId.Value, truckType, request); + return Ok(result); + } + + [HttpGet("by-plate/{plate}")] + public async Task ByPlate(string plate) + { + var result = await _truckService.GetByPlateAsync(plate); + if (result == null) return NotFound(new { error = "Camión no encontrado" }); + return Ok(result); } [HttpPost] - public async Task> Create([FromBody] CreateTruckRequest request) + public async Task Create([FromBody] CreateTruckRequest request) { - var tenantIdClaim = User.FindFirst("tenant_id"); - if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId)) - return Unauthorized(new { error = "No se pudo determinar el tenant" }); - - var existing = await _context.Trucks.AnyAsync(x => x.Plate == request.Plate && !x.IsDeleted); - if (existing) return Conflict(new { error = $"Ya existe camión con placa '{request.Plate}'" }); - - var item = new Truck - { - Id = Guid.NewGuid(), - TenantId = tenantId, - Plate = request.Plate, - Model = request.Model, - Type = Enum.TryParse(request.Type, out var t) ? t : TruckType.DryBox, - MaxCapacityKg = request.MaxCapacityKg, - MaxVolumeM3 = request.MaxVolumeM3, - IsActive = true, - Vin = request.Vin, - EngineNumber = request.EngineNumber, - Year = request.Year, - Color = request.Color, - InsurancePolicy = request.InsurancePolicy, - InsuranceExpiration = request.InsuranceExpiration, - VerificationNumber = request.VerificationNumber, - VerificationExpiration = request.VerificationExpiration, - CreatedAt = DateTime.UtcNow - }; - - _context.Trucks.Add(item); - await _context.SaveChangesAsync(); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + var result = await _truckService.CreateAsync(request); + if (!result.Success) + return Conflict(new { error = result.Message }); + return CreatedAtAction(nameof(GetById), new { id = result.Data!.Id }, result.Data); } [HttpPut("{id:guid}")] - public async Task> Update(Guid id, [FromBody] UpdateTruckRequest request) + public async Task Update(Guid id, [FromBody] UpdateTruckRequest request) { - var item = await _context.Trucks.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Camión no encontrado" }); - - item.Plate = request.Plate; - item.Model = request.Model; - item.Type = Enum.TryParse(request.Type, out var t) ? t : item.Type; - item.MaxCapacityKg = request.MaxCapacityKg; - item.MaxVolumeM3 = request.MaxVolumeM3; - item.IsActive = request.IsActive; - item.Vin = request.Vin; - item.EngineNumber = request.EngineNumber; - item.Year = request.Year; - item.Color = request.Color; - item.InsurancePolicy = request.InsurancePolicy; - item.InsuranceExpiration = request.InsuranceExpiration; - item.VerificationNumber = request.VerificationNumber; - item.VerificationExpiration = request.VerificationExpiration; - item.LastMaintenanceDate = request.LastMaintenanceDate; - item.NextMaintenanceDate = request.NextMaintenanceDate; - item.CurrentOdometerKm = request.CurrentOdometerKm; - item.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - return Ok(MapToResponse(item)); + var result = await _truckService.UpdateAsync(id, request); + if (!result.Success) + return (result.Message?.Contains("no encontrado") ?? false) + ? NotFound(new { error = result.Message }) + : BadRequest(new { error = result.Message }); + return Ok(result.Data); } [HttpDelete("{id:guid}")] public async Task Delete(Guid id) { - var item = await _context.Trucks.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Camión no encontrado" }); - - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); + var result = await _truckService.DeleteAsync(id); + if (!result.Success) return NotFound(new { error = result.Message }); return NoContent(); } - private static TruckResponse MapToResponse(Truck x) => new( - x.Id, x.Plate, x.Model, x.Type.ToString(), x.MaxCapacityKg, x.MaxVolumeM3, x.IsActive, - x.Vin, x.EngineNumber, x.Year, x.Color, x.InsurancePolicy, x.InsuranceExpiration, - x.VerificationNumber, x.VerificationExpiration, x.LastMaintenanceDate, - x.NextMaintenanceDate, x.CurrentOdometerKm, x.CreatedAt, x.UpdatedAt - ); + [HttpPatch("{id:guid}/status")] + public async Task SetStatus(Guid id, [FromBody] bool isActive) + { + var result = await _truckService.SetActiveStatusAsync(id, isActive); + if (!result.Success) return NotFound(new { error = result.Message }); + return Ok(result.Data); + } + + private Guid? GetTenantId() + { + var claim = User.FindFirst("tenant_id"); + return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null; + } } diff --git a/backend/src/Parhelion.API/Parhelion.API.csproj b/backend/src/Parhelion.API/Parhelion.API.csproj index df5f7d2..666dc1f 100644 --- a/backend/src/Parhelion.API/Parhelion.API.csproj +++ b/backend/src/Parhelion.API/Parhelion.API.csproj @@ -4,6 +4,8 @@ net8.0 enable enable + true + $(NoWarn);1591 diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index dc96f45..2cddd68 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -1,7 +1,9 @@ +using System.Reflection; using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using Parhelion.Application.Auth; using Parhelion.Application.Services; using Parhelion.Infrastructure.Auth; @@ -14,7 +16,60 @@ // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + +// ========== SWAGGER/OPENAPI CONFIGURATION ========== +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v0.5.4", + Title = "Parhelion Logistics API", + Description = "API para gestión de logística B2B: envíos, flotas, rutas y almacenes (WMS + TMS)", + Contact = new OpenApiContact + { + Name = "Parhelion Logistics", + Email = "dev@parhelion.com" + }, + License = new OpenApiLicense + { + Name = "Proprietary", + Url = new Uri("https://parhelion.com/license") + } + }); + + // JWT Bearer Authentication + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header. Formato: 'Bearer {token}'", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + + // Include XML Comments + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + { + options.IncludeXmlComments(xmlPath); + } +}); // ========== INFRASTRUCTURE SERVICES ========== builder.Services.AddHttpContextAccessor(); diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs index 4faf117..52d9fd2 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs @@ -172,15 +172,62 @@ public async Task> UpdateStatusAsync(Guid ship var entity = await _unitOfWork.Shipments.GetByIdAsync(shipmentId, cancellationToken); if (entity == null) return OperationResult.Fail("Envío no encontrado"); + // Validate status transition + var validationResult = ValidateStatusTransition(entity.Status, newStatus); + if (!validationResult.IsValid) + return OperationResult.Fail(validationResult.ErrorMessage!); + entity.Status = newStatus; entity.UpdatedAt = DateTime.UtcNow; - if (newStatus == ShipmentStatus.Delivered) entity.DeliveredAt = DateTime.UtcNow; + + // Set DeliveredAt when status changes to Delivered + if (newStatus == ShipmentStatus.Delivered) + entity.DeliveredAt = DateTime.UtcNow; _unitOfWork.Shipments.Update(entity); await _unitOfWork.SaveChangesAsync(cancellationToken); return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), $"Estado actualizado a {newStatus}"); } + /// + /// Validates if a status transition is allowed based on business rules. + /// Valid transitions: + /// PendingApproval → Approved, Exception + /// Approved → Loaded, Exception + /// Loaded → InTransit, Exception + /// InTransit → AtHub, OutForDelivery, Exception + /// AtHub → InTransit, OutForDelivery, Exception + /// OutForDelivery → Delivered, Exception + /// Exception → Any previous state (recovery) + /// + private static (bool IsValid, string? ErrorMessage) ValidateStatusTransition(ShipmentStatus current, ShipmentStatus next) + { + if (current == next) + return (true, null); // No change is always valid + + // From Exception, can go to any state (recovery) + if (current == ShipmentStatus.Exception) + return (true, null); + + // Cannot go backwards in workflow (except from Exception) + var validTransitions = current switch + { + ShipmentStatus.PendingApproval => new[] { ShipmentStatus.Approved, ShipmentStatus.Exception }, + ShipmentStatus.Approved => new[] { ShipmentStatus.Loaded, ShipmentStatus.Exception }, + ShipmentStatus.Loaded => new[] { ShipmentStatus.InTransit, ShipmentStatus.Exception }, + ShipmentStatus.InTransit => new[] { ShipmentStatus.AtHub, ShipmentStatus.OutForDelivery, ShipmentStatus.Exception }, + ShipmentStatus.AtHub => new[] { ShipmentStatus.InTransit, ShipmentStatus.OutForDelivery, ShipmentStatus.Exception }, + ShipmentStatus.OutForDelivery => new[] { ShipmentStatus.Delivered, ShipmentStatus.AtHub, ShipmentStatus.Exception }, + ShipmentStatus.Delivered => new[] { ShipmentStatus.Exception }, // Delivered is final, only Exception allowed + _ => Array.Empty() + }; + + if (!validTransitions.Contains(next)) + return (false, $"Transición de estado inválida: {current} → {next}. Estados válidos: {string.Join(", ", validTransitions)}"); + + return (true, null); + } + private static string GenerateTrackingNumber() => $"PAR-{DateTime.UtcNow:yyyyMMddHHmmss}-{new Random().Next(1000, 9999)}"; private async Task MapToResponseAsync(Domain.Entities.Shipment e, CancellationToken ct) From 4434391a7898dd60864dde1ea32b637a8b398c61 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 18 Dec 2025 02:39:03 +0000 Subject: [PATCH 24/34] chore: Actualiza Docker a v0.5.4 - Dockerfile: Labels de version, maintainer y descripcion - docker-compose.yml: Comentarios de version y URL de Swagger Build: 0 Warnings, 0 Errors Tests: 72/72 Passed --- backend/Dockerfile | 4 ++++ docker-compose.yml | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 2728372..87bffd5 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,10 +1,14 @@ # =================================== # PARHELION API - Dockerfile # Multi-stage build para producción +# Version: 0.5.4 # =================================== # --- STAGE 1: Build --- FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +LABEL maintainer="dev@parhelion.com" +LABEL version="0.5.4" +LABEL description="Parhelion Logistics API - WMS + TMS" WORKDIR /app # Copiar archivo de solución diff --git a/docker-compose.yml b/docker-compose.yml index 56bf981..38842d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ # =================================== # PARHELION LOGISTICS - Docker Compose +# Version: 0.5.4 # Red unificada: parhelion-network +# Swagger UI: http://localhost:5100/swagger # =================================== services: @@ -20,7 +22,7 @@ services: networks: - parhelion-network healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-MetaCodeX} -d parhelion_dev"] + test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-MetaCodeX} -d parhelion_dev" ] interval: 10s timeout: 5s retries: 5 @@ -45,7 +47,7 @@ services: networks: - parhelion-network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + test: [ "CMD", "curl", "-f", "http://localhost:5000/health" ] interval: 30s timeout: 10s retries: 3 @@ -63,7 +65,7 @@ services: networks: - parhelion-network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:80"] + test: [ "CMD", "curl", "-f", "http://localhost:80" ] interval: 30s timeout: 10s retries: 3 @@ -80,7 +82,7 @@ services: networks: - parhelion-network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:80"] + test: [ "CMD", "curl", "-f", "http://localhost:80" ] interval: 30s timeout: 10s retries: 3 @@ -97,7 +99,7 @@ services: networks: - parhelion-network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:80"] + test: [ "CMD", "curl", "-f", "http://localhost:80" ] interval: 30s timeout: 10s retries: 3 From 1ffdff0dd052ac90e300d786044d247264042dd4 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Thu, 18 Dec 2025 05:04:37 +0000 Subject: [PATCH 25/34] feat(release): v0.5.5 - WMS/TMS Services + Business Rules SPANISH: - Servicios WMS: WarehouseZone, WarehouseOperator, InventoryStock, InventoryTransaction - Servicios TMS: NetworkLink, RouteStep con reordenamiento automatico - Validador de compatibilidad carga-camion (Refrigerado/HAZMAT/Blindado) - FleetLog automatico al cambiar CurrentTruck - Validacion de codigos aeropuerto (2-4 letras) - 122 tests (+50 nuevos) ENGLISH: - WMS Services: WarehouseZone, WarehouseOperator, InventoryStock, InventoryTransaction - TMS Services: NetworkLink, RouteStep with auto-reordering - Cargo-truck compatibility validator (Refrigerated/HAZMAT/Armored) - Automatic FleetLog on CurrentTruck changes - Airport code validation (2-4 letters) - 122 tests (+50 new) Breaking Changes: None Tests: All 122 passing --- CHANGELOG.md | 55 ++++++ README.md | 55 ++++-- api-architecture.md | 41 ++--- backend/src/Parhelion.API/Program.cs | 18 ++ .../Services/IInventoryStockService.cs | 41 +++++ .../Services/IInventoryTransactionService.cs | 57 ++++++ .../Services/INetworkLinkService.cs | 31 ++++ .../Interfaces/Services/IRouteStepService.cs | 26 +++ .../Services/IWarehouseOperatorService.cs | 26 +++ .../Services/IWarehouseZoneService.cs | 26 +++ .../ICargoCompatibilityValidator.cs | 39 ++++ .../Services/Fleet/DriverService.cs | 22 ++- .../Services/Network/LocationService.cs | 15 +- .../Services/Network/NetworkLinkService.cs | 124 +++++++++++++ .../Services/Network/RouteStepService.cs | 157 ++++++++++++++++ .../Services/Shipment/ShipmentService.cs | 16 +- .../Warehouse/InventoryStockService.cs | 162 +++++++++++++++++ .../Warehouse/InventoryTransactionService.cs | 141 +++++++++++++++ .../Warehouse/WarehouseOperatorService.cs | 117 ++++++++++++ .../Warehouse/WarehouseZoneService.cs | 106 +++++++++++ .../Validators/CargoCompatibilityValidator.cs | 74 ++++++++ .../Fixtures/ServiceTestFixture.cs | 82 +++++++++ .../Integration/FleetIntegrationTests.cs | 147 +++++++++++++++ .../Integration/NetworkIntegrationTests.cs | 157 ++++++++++++++++ .../Integration/WMSIntegrationTests.cs | 122 +++++++++++++ .../Network/NetworkLinkServiceTests.cs | 86 +++++++++ .../Services/Network/RouteStepServiceTests.cs | 96 ++++++++++ .../Services/Shipment/ShipmentServiceTests.cs | 9 +- .../Warehouse/InventoryStockServiceTests.cs | 121 +++++++++++++ .../Warehouse/WarehouseZoneServiceTests.cs | 104 +++++++++++ .../CargoCompatibilityValidatorTests.cs | 170 ++++++++++++++++++ 31 files changed, 2404 insertions(+), 39 deletions(-) create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IInventoryStockService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IInventoryTransactionService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/INetworkLinkService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IRouteStepService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IWarehouseOperatorService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/IWarehouseZoneService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Validators/ICargoCompatibilityValidator.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Network/NetworkLinkService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Network/RouteStepService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Warehouse/InventoryStockService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Warehouse/InventoryTransactionService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Warehouse/WarehouseOperatorService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Warehouse/WarehouseZoneService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Validators/CargoCompatibilityValidator.cs create mode 100644 backend/tests/Parhelion.Tests/Integration/FleetIntegrationTests.cs create mode 100644 backend/tests/Parhelion.Tests/Integration/NetworkIntegrationTests.cs create mode 100644 backend/tests/Parhelion.Tests/Integration/WMSIntegrationTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Network/NetworkLinkServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Network/RouteStepServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Warehouse/InventoryStockServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Services/Warehouse/WarehouseZoneServiceTests.cs create mode 100644 backend/tests/Parhelion.Tests/Unit/Validators/CargoCompatibilityValidatorTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e267f39..e5bd6eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,61 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.5.5] - 2025-12-18 + +### Agregado + +- **WMS Services Layer (4 servicios)**: + + - `WarehouseZoneService` - Gestión de zonas de almacén + - `WarehouseOperatorService` - Gestión de almacenistas + - `InventoryStockService` - Stock con reserva/liberación + - `InventoryTransactionService` - Kardex de movimientos (Receipt, Dispatch, Transfer) + +- **TMS Network Services (2 servicios)**: + + - `NetworkLinkService` - Enlaces bidireccionales entre nodos + - `RouteStepService` - Pasos de ruta con reordenamiento automático + +- **Business Rules Validators**: + + - `ICargoCompatibilityValidator` - Interface de validación carga-camión + - `CargoCompatibilityValidator` - Implementación con reglas: + - Cargo refrigerado → Truck Refrigerated + - Cargo HAZMAT → Truck HazmatTank + - Cargo alto valor (>$500K) → Truck Armored + +- **Automatic FleetLog Generation**: + + - `DriverService.AssignTruckAsync` ahora genera FleetLog automáticamente + - Audit trail completo de cambios de camión + +- **Airport Code Validation**: + + - `LocationService.CreateAsync` valida formato 2-4 letras (MTY, GDL, MM) + - Normalización automática a mayúsculas + +- **Tests (50 nuevos, total: 122)**: + + - Unit tests para WMS services (15) + - Unit tests para Network services (10) + - Integration tests WMS/Fleet/Network (13) + - CargoCompatibilityValidator tests (12) + +### Modificado + +- `ShipmentService.AssignToDriverAsync` - Ahora valida compatibilidad carga-camión +- `Program.cs` - Registro de todos los nuevos servicios en DI +- `ServiceTestFixture` - Datos seed para WMS (Zone, CatalogItem, InventoryStock) + +### Notas Técnicas + +- Validators registrados como Singleton (stateless) +- Services registrados como Scoped +- Todas las validaciones son fail-safe con mensajes descriptivos + +--- + ## [0.5.4] - 2025-12-18 ### Agregado diff --git a/README.md b/README.md index f75cb5f..df5ed5d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. -> **Estado:** Development Preview v0.5.4 - Swagger/OpenAPI + Business Logic +> **Estado:** Development Preview v0.5.5 - Business Rules + WMS/TMS Services --- @@ -31,31 +31,31 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in - [x] Documentacion de requerimientos y esquema de base de datos - [x] **Arquitectura Base:** Configuracion de Clean Architecture y estructura de proyecto - [x] **Multi-tenancy:** Query Filters globales por TenantId -- [x] **Domain Layer:** 14 entidades + 11 enumeraciones +- [x] **Domain Layer:** 23 entidades + 15 enumeraciones - [x] **Infrastructure Layer:** EF Core + PostgreSQL + Migrations - [x] **API Skeleton:** 22 endpoints base para todas las entidades - [x] **Autenticacion:** JWT con roles SuperAdmin/Admin/Driver/Warehouse - [x] **Repository Pattern:** GenericRepository + UnitOfWork + Soft Delete -- [x] **xUnit Tests:** 72 tests (foundation + services integration) -- [x] **Services Layer:** 16 interfaces, 15 implementaciones (Core, Shipment, Fleet, Network) +- [x] **xUnit Tests:** 122 tests (foundation + services + business rules) +- [x] **Services Layer:** 22 servicios (Core, Shipment, Fleet, Network, Warehouse) ### Gestion de Flotilla -- [ ] **Camiones Tipificados:** DryBox, Refrigerado, HAZMAT, Plataforma, Blindado -- [ ] **Choferes:** Asignacion fija (default_truck) y dinamica (current_truck) -- [ ] **Bitacora de Flotilla:** Historial de cambios de vehiculo (FleetLog) +- [x] **Camiones Tipificados:** DryBox, Refrigerado, HAZMAT, Plataforma, Blindado +- [x] **Choferes:** Asignacion fija (default_truck) y dinamica (current_truck) +- [x] **Bitacora de Flotilla:** Historial de cambios de vehiculo (FleetLog automático) ### Red Logistica (Hub and Spoke) -- [ ] **Nodos de Red:** RegionalHub, CrossDock, Warehouse, Store, SupplierPlant -- [ ] **Codigos Aeroportuarios:** Identificadores unicos por ubicacion (MTY, GDL, MM) -- [ ] **Enlaces de Red:** Conexiones FirstMile, LineHaul, LastMile -- [ ] **Rutas Predefinidas:** RouteBlueprint con paradas y tiempos de transito +- [x] **Nodos de Red:** RegionalHub, CrossDock, Warehouse, Store, SupplierPlant +- [x] **Codigos Aeroportuarios:** Identificadores unicos por ubicacion (MTY, GDL, MM) +- [x] **Enlaces de Red:** Conexiones FirstMile, LineHaul, LastMile +- [x] **Rutas Predefinidas:** RouteBlueprint con paradas y tiempos de transito ### Envios y Trazabilidad -- [ ] **Manifiesto de Carga:** Items con peso volumetrico y valor declarado -- [ ] **Restricciones de Compatibilidad:** Cadena de frio, HAZMAT, Alto valor +- [x] **Manifiesto de Carga:** Items con peso volumetrico y valor declarado +- [x] **Restricciones de Compatibilidad:** Cadena de frio, HAZMAT, Alto valor (validador automatico) - [ ] **Checkpoints:** Bitacora de eventos (Loaded, QrScanned, ArrivedHub, Delivered) - [ ] **QR Handshake:** Transferencia de custodia digital mediante escaneo @@ -236,6 +236,35 @@ src/ --- +## Roadmap + +### Completado + +| Version | Fecha | Descripcion | +| ---------- | ----------- | ----------------------------------------------------------- | +| v0.1.0 | 2025-12 | Estructura inicial, documentación de requerimientos | +| v0.2.0 | 2025-12 | Domain Layer: Entidades base y enumeraciones | +| v0.3.0 | 2025-12 | Infrastructure Layer: EF Core, PostgreSQL, Migrations | +| v0.4.0 | 2025-12 | API Layer: Controllers base, JWT Authentication | +| v0.5.0 | 2025-12 | Services Layer: Repository Pattern, UnitOfWork | +| v0.5.1 | 2025-12 | Foundation Tests: DTOs, Repository, UnitOfWork | +| v0.5.2 | 2025-12 | Services Implementation: 16 interfaces, 15 implementaciones | +| v0.5.3 | 2025-12 | Integration Tests: 72 tests para Services | +| v0.5.4 | 2025-12 | Swagger/OpenAPI, Business Logic Workflow | +| **v0.5.5** | **2025-12** | **WMS/TMS Services, Business Rules, 122 tests** | + +### Próximas Versiones + +| Version | Objetivo | Características | +| ---------- | --------------------- | ---------------------------------------------- | +| v0.5.6 | Trazabilidad | Endpoints de Checkpoints, Upload de documentos | +| v0.5.7 | QR Handshake | Transferencia de custodia digital | +| v0.5.8 | Rutas | Asignación de rutas, avance por pasos | +| v0.5.9 | Dashboard | KPIs operativos, métricas por status | +| **v0.6.0** | **Perfeccionamiento** | **Bug fixes, E2E testing, optimización** | + +--- + ## Autor **MetaCodeX** | 2025 diff --git a/api-architecture.md b/api-architecture.md index 8fc49bf..44e422a 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -4,8 +4,8 @@ Documentacion tecnica de la estructura API-First del backend Parhelion. ## Estado Actual -**Version:** 0.5.4 -**Enfoque:** Swagger/OpenAPI + Business Logic +**Version:** 0.5.5 +**Enfoque:** WMS/TMS Services + Business Rules **Arquitectura:** Clean Architecture + Domain-Driven Design --- @@ -30,13 +30,13 @@ Gestion de identidad, usuarios y estructura organizacional. Gestion de almacenes, zonas e inventario. -| Endpoint | Entidad | Estado | Service | -| ----------------------------- | -------------------- | -------- | --------------- | -| `/api/locations` | Location | Services | LocationService | -| `/api/warehouse-zones` | WarehouseZone | Skeleton | - | -| `/api/warehouse-operators` | WarehouseOperator | Skeleton | - | -| `/api/inventory-stocks` | InventoryStock | Skeleton | - | -| `/api/inventory-transactions` | InventoryTransaction | Skeleton | - | +| Endpoint | Entidad | Estado | Service | +| ----------------------------- | -------------------- | -------- | --------------------------- | +| `/api/locations` | Location | Services | LocationService | +| `/api/warehouse-zones` | WarehouseZone | Services | WarehouseZoneService | +| `/api/warehouse-operators` | WarehouseOperator | Services | WarehouseOperatorService | +| `/api/inventory-stocks` | InventoryStock | Services | InventoryStockService | +| `/api/inventory-transactions` | InventoryTransaction | Services | InventoryTransactionService | ### Fleet Layer @@ -65,11 +65,11 @@ Gestion de envios, items y trazabilidad. Gestion de red logistica Hub and Spoke. -| Endpoint | Entidad | Estado | Service | -| ----------------------- | -------------- | -------- | ------------ | -| `/api/network-links` | NetworkLink | Skeleton | - | -| `/api/route-blueprints` | RouteBlueprint | Services | RouteService | -| `/api/route-steps` | RouteStep | Skeleton | - | +| Endpoint | Entidad | Estado | Service | +| ----------------------- | -------------- | -------- | ------------------ | +| `/api/network-links` | NetworkLink | Services | NetworkLinkService | +| `/api/route-blueprints` | RouteBlueprint | Services | RouteService | +| `/api/route-steps` | RouteStep | Services | RouteStepService | --- @@ -88,12 +88,13 @@ Capa de servicios que encapsula logica de negocio. ### Implementaciones por Capa -| Capa | Services | -| -------- | ----------------------------------------------------- | -| Core | Tenant, User, Role, Employee, Client | -| Shipment | Shipment, ShipmentItem, Checkpoint, Document, Catalog | -| Fleet | Driver, Truck, FleetLog | -| Network | Location, Route | +| Capa | Services | +| --------- | ---------------------------------------------------------------------- | +| Core | Tenant, User, Role, Employee, Client | +| Shipment | Shipment, ShipmentItem, Checkpoint, Document, Catalog | +| Fleet | Driver, Truck, FleetLog | +| Network | Location, Route, NetworkLink, RouteStep | +| Warehouse | WarehouseZone, WarehouseOperator, InventoryStock, InventoryTransaction | --- diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index 2cddd68..13ab06d 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -137,6 +137,24 @@ Parhelion.Infrastructure.Services.Network.LocationService>(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// ========== WAREHOUSE LAYER SERVICES ========== +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// ========== VALIDATORS ========== +builder.Services.AddSingleton(); // ========== JWT AUTHENTICATION ========== var jwtSecretKey = builder.Configuration["Jwt:SecretKey"] diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IInventoryStockService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IInventoryStockService.cs new file mode 100644 index 0000000..f883636 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IInventoryStockService.cs @@ -0,0 +1,41 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de inventario físico. +/// +public interface IInventoryStockService : IGenericService +{ + /// + /// Obtiene stock por tenant. + /// + Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene stock por zona. + /// + Task> GetByZoneAsync(Guid zoneId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene stock por producto. + /// + Task> GetByProductAsync(Guid productId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene productos con bajo stock. + /// + Task> GetLowStockAsync(Guid tenantId, decimal threshold, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Reserva cantidad de un producto. + /// + Task> ReserveQuantityAsync(Guid stockId, decimal quantity, CancellationToken cancellationToken = default); + + /// + /// Libera cantidad reservada. + /// + Task> ReleaseReservedAsync(Guid stockId, decimal quantity, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IInventoryTransactionService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IInventoryTransactionService.cs new file mode 100644 index 0000000..fcc3d85 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IInventoryTransactionService.cs @@ -0,0 +1,57 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para transacciones de inventario. +/// +public interface IInventoryTransactionService +{ + /// + /// Obtiene transacciones por tenant. + /// + Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene transacción por ID. + /// + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// Obtiene transacciones por producto. + /// + Task> GetByProductAsync(Guid productId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene transacciones por zona. + /// + Task> GetByZoneAsync(Guid zoneId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene transacciones por tipo. + /// + Task> GetByTypeAsync(Guid tenantId, InventoryTransactionType type, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Registra entrada de inventario. + /// + Task> RecordReceiptAsync(CreateInventoryTransactionRequest request, Guid performedByUserId, CancellationToken cancellationToken = default); + + /// + /// Registra salida de inventario. + /// + Task> RecordDispatchAsync(CreateInventoryTransactionRequest request, Guid performedByUserId, CancellationToken cancellationToken = default); + + /// + /// Registra transferencia entre zonas. + /// + Task> RecordTransferAsync(CreateInventoryTransactionRequest request, Guid performedByUserId, CancellationToken cancellationToken = default); + + /// + /// Verifica si existe la transacción. + /// + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/INetworkLinkService.cs b/backend/src/Parhelion.Application/Interfaces/Services/INetworkLinkService.cs new file mode 100644 index 0000000..089733e --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/INetworkLinkService.cs @@ -0,0 +1,31 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de enlaces de red logística. +/// +public interface INetworkLinkService : IGenericService +{ + /// + /// Obtiene enlaces por tenant. + /// + Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene enlaces desde una ubicación de origen. + /// + Task> GetByOriginAsync(Guid originLocationId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene enlaces hacia una ubicación de destino. + /// + Task> GetByDestinationAsync(Guid destinationLocationId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene enlaces activos. + /// + Task> GetActiveAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IRouteStepService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IRouteStepService.cs new file mode 100644 index 0000000..08dc707 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IRouteStepService.cs @@ -0,0 +1,26 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de pasos de ruta. +/// +public interface IRouteStepService : IGenericService +{ + /// + /// Obtiene pasos de una ruta específica ordenados. + /// + Task> GetByRouteAsync(Guid routeBlueprintId, CancellationToken cancellationToken = default); + + /// + /// Reordena los pasos de una ruta. + /// + Task ReorderStepsAsync(Guid routeBlueprintId, IEnumerable stepIdsInOrder, CancellationToken cancellationToken = default); + + /// + /// Añade un paso a una ruta. + /// + Task> AddStepToRouteAsync(Guid routeBlueprintId, CreateRouteStepRequest request, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IWarehouseOperatorService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IWarehouseOperatorService.cs new file mode 100644 index 0000000..51bfae9 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IWarehouseOperatorService.cs @@ -0,0 +1,26 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de operadores de almacén. +/// +public interface IWarehouseOperatorService : IGenericService +{ + /// + /// Obtiene operadores por ubicación asignada. + /// + Task> GetByLocationAsync(Guid locationId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene operador por empleado. + /// + Task GetByEmployeeAsync(Guid employeeId, CancellationToken cancellationToken = default); + + /// + /// Asigna operador a una zona. + /// + Task> AssignToZoneAsync(Guid operatorId, Guid zoneId, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IWarehouseZoneService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IWarehouseZoneService.cs new file mode 100644 index 0000000..f1f5bd9 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/IWarehouseZoneService.cs @@ -0,0 +1,26 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Domain.Entities; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de zonas de almacén. +/// +public interface IWarehouseZoneService : IGenericService +{ + /// + /// Obtiene zonas por ubicación/bodega. + /// + Task> GetByLocationAsync(Guid locationId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene zonas activas de una ubicación. + /// + Task> GetActiveAsync(Guid locationId, PagedRequest request, CancellationToken cancellationToken = default); + + /// + /// Obtiene zona por código. + /// + Task GetByCodeAsync(Guid locationId, string code, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Validators/ICargoCompatibilityValidator.cs b/backend/src/Parhelion.Application/Interfaces/Validators/ICargoCompatibilityValidator.cs new file mode 100644 index 0000000..44e6904 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Validators/ICargoCompatibilityValidator.cs @@ -0,0 +1,39 @@ +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces.Validators; + +/// +/// Resultado de validación de compatibilidad carga-camión. +/// +public record CargoValidationResult(bool IsValid, string? ErrorMessage = null, TruckType? RequiredTruckType = null) +{ + public static CargoValidationResult Success() => new(true); + public static CargoValidationResult Fail(string message, TruckType? required = null) => new(false, message, required); +} + +/// +/// Validador de compatibilidad entre carga y tipo de camión. +/// Implementa reglas de negocio del README: +/// - Cargo refrigerado → Truck Refrigerated +/// - Cargo HAZMAT → Truck HazmatTank +/// - Cargo alto valor → Truck Armored +/// +public interface ICargoCompatibilityValidator +{ + /// + /// Valida si un envío puede asignarse a un camión específico. + /// + CargoValidationResult ValidateShipmentForTruck(IEnumerable items, TruckType truckType); + + /// + /// Determina qué tipo de camión requiere un conjunto de items. + /// + TruckType DetermineRequiredTruckType(IEnumerable items); + + /// + /// Threshold de valor declarado para requerir camión blindado. + /// Configurable por tenant en futuras versiones. + /// + decimal HighValueThreshold { get; } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs b/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs index a93e826..250496a 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs @@ -145,11 +145,31 @@ public async Task> AssignTruckAsync(Guid driverI var truck = await _unitOfWork.Trucks.GetByIdAsync(truckId, cancellationToken); if (truck == null) return OperationResult.Fail("Camión no encontrado"); + // Store old truck for FleetLog + var oldTruckId = entity.CurrentTruckId; + + // Update driver's current truck entity.CurrentTruckId = truckId; entity.UpdatedAt = DateTime.UtcNow; _unitOfWork.Drivers.Update(entity); + + // Auto-generate FleetLog for audit trail + var fleetLog = new FleetLog + { + Id = Guid.NewGuid(), + TenantId = truck.TenantId, + DriverId = driverId, + OldTruckId = oldTruckId, + NewTruckId = truckId, + Reason = FleetLogReason.Reassignment, + Timestamp = DateTime.UtcNow, + CreatedByUserId = Guid.Empty, // TODO: Inject from CurrentUserService + CreatedAt = DateTime.UtcNow + }; + await _unitOfWork.FleetLogs.AddAsync(fleetLog, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); - return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Camión asignado exitosamente"); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Camión asignado exitosamente (FleetLog generado)"); } private async Task MapToResponseAsync(Driver e, CancellationToken ct) diff --git a/backend/src/Parhelion.Infrastructure/Services/Network/LocationService.cs b/backend/src/Parhelion.Infrastructure/Services/Network/LocationService.cs index 49947b8..55d49e4 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Network/LocationService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Network/LocationService.cs @@ -30,13 +30,17 @@ public async Task> GetAllAsync(PagedRequest reques public async Task> CreateAsync(CreateLocationRequest request, CancellationToken cancellationToken = default) { + // Validate airport-style code format + if (!IsValidAirportCode(request.Code)) + return OperationResult.Fail("Código debe ser 2-4 caracteres mayúsculas (ej: MTY, GDL, MM)"); + if (!Enum.TryParse(request.Type, out var locationType)) return OperationResult.Fail("Tipo de ubicación inválido"); var entity = new Location { Id = Guid.NewGuid(), - Code = request.Code, + Code = request.Code.ToUpperInvariant(), Name = request.Name, Type = locationType, FullAddress = request.FullAddress, @@ -116,5 +120,14 @@ public async Task> SearchByNameAsync(Guid tenantId return PagedResult.From(items.Select(MapToResponse), totalCount, request); } + /// + /// Validates airport-style location code format (2-4 uppercase letters). + /// Examples: MTY, GDL, MM, CDMX + /// + private static bool IsValidAirportCode(string? code) => + !string.IsNullOrEmpty(code) && + code.Length >= 2 && code.Length <= 4 && + code.All(c => char.IsLetter(c)); + private static LocationResponse MapToResponse(Location e) => new(e.Id, e.Code, e.Name, e.Type.ToString(), e.FullAddress, e.Latitude, e.Longitude, e.CanReceive, e.CanDispatch, e.IsInternal, e.IsActive, e.CreatedAt, e.UpdatedAt); } diff --git a/backend/src/Parhelion.Infrastructure/Services/Network/NetworkLinkService.cs b/backend/src/Parhelion.Infrastructure/Services/Network/NetworkLinkService.cs new file mode 100644 index 0000000..4f839fc --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Network/NetworkLinkService.cs @@ -0,0 +1,124 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Network; + +/// +/// Implementación del servicio de enlaces de red logística. +/// +public class NetworkLinkService : INetworkLinkService +{ + private readonly IUnitOfWork _unitOfWork; + + public NetworkLinkService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.NetworkLinks.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(l => l.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var l in items) dtos.Add(await MapToResponseAsync(l, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.NetworkLinks.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> CreateAsync(CreateNetworkLinkRequest request, CancellationToken cancellationToken = default) + { + // Get tenant from origin location + var originLocation = await _unitOfWork.Locations.GetByIdAsync(request.OriginLocationId, cancellationToken); + if (originLocation == null) return OperationResult.Fail("Ubicación origen no encontrada"); + + var entity = new NetworkLink + { + Id = Guid.NewGuid(), + TenantId = originLocation.TenantId, + OriginLocationId = request.OriginLocationId, + DestinationLocationId = request.DestinationLocationId, + LinkType = Enum.TryParse(request.LinkType, out var lt) ? lt : NetworkLinkType.LineHaul, + TransitTime = request.TransitTime, + IsBidirectional = request.IsBidirectional, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.NetworkLinks.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Enlace creado exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateNetworkLinkRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.NetworkLinks.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Enlace no encontrado"); + + if (Enum.TryParse(request.LinkType, out var lt)) entity.LinkType = lt; + entity.TransitTime = request.TransitTime; + entity.IsBidirectional = request.IsBidirectional; + entity.IsActive = request.IsActive; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.NetworkLinks.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Enlace actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.NetworkLinks.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Enlace no encontrado"); + _unitOfWork.NetworkLinks.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Enlace eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.NetworkLinks.AnyAsync(l => l.Id == id, cancellationToken); + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.NetworkLinks.GetPagedAsync(request, filter: l => l.TenantId == tenantId, orderBy: q => q.OrderBy(l => l.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var l in items) dtos.Add(await MapToResponseAsync(l, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByOriginAsync(Guid originLocationId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.NetworkLinks.GetPagedAsync(request, filter: l => l.OriginLocationId == originLocationId, orderBy: q => q.OrderBy(l => l.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var l in items) dtos.Add(await MapToResponseAsync(l, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByDestinationAsync(Guid destinationLocationId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.NetworkLinks.GetPagedAsync(request, filter: l => l.DestinationLocationId == destinationLocationId, orderBy: q => q.OrderBy(l => l.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var l in items) dtos.Add(await MapToResponseAsync(l, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetActiveAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.NetworkLinks.GetPagedAsync(request, filter: l => l.TenantId == tenantId && l.IsActive, orderBy: q => q.OrderBy(l => l.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var l in items) dtos.Add(await MapToResponseAsync(l, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + private async Task MapToResponseAsync(NetworkLink l, CancellationToken ct) + { + var origin = await _unitOfWork.Locations.GetByIdAsync(l.OriginLocationId, ct); + var destination = await _unitOfWork.Locations.GetByIdAsync(l.DestinationLocationId, ct); + return new NetworkLinkResponse(l.Id, l.OriginLocationId, origin?.Name ?? "", l.DestinationLocationId, destination?.Name ?? "", + l.LinkType.ToString(), l.TransitTime, l.IsBidirectional, l.IsActive, l.CreatedAt, l.UpdatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Network/RouteStepService.cs b/backend/src/Parhelion.Infrastructure/Services/Network/RouteStepService.cs new file mode 100644 index 0000000..a5d8254 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Network/RouteStepService.cs @@ -0,0 +1,157 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Network; + +/// +/// Implementación del servicio de pasos de ruta. +/// +public class RouteStepService : IRouteStepService +{ + private readonly IUnitOfWork _unitOfWork; + + public RouteStepService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.RouteSteps.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(s => s.StepOrder), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.RouteSteps.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> CreateAsync(CreateRouteStepRequest request, CancellationToken cancellationToken = default) + { + var route = await _unitOfWork.RouteBlueprints.GetByIdAsync(request.RouteBlueprintId, cancellationToken); + if (route == null) return OperationResult.Fail("Ruta no encontrada"); + + var entity = new RouteStep + { + Id = Guid.NewGuid(), + RouteBlueprintId = request.RouteBlueprintId, + LocationId = request.LocationId, + StepOrder = request.StepOrder, + StandardTransitTime = request.StandardTransitTime, + StepType = Enum.TryParse(request.StepType, out var st) ? st : RouteStepType.Intermediate, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.RouteSteps.AddAsync(entity, cancellationToken); + + // Update route totals + route.TotalSteps += 1; + route.TotalTransitTime += request.StandardTransitTime; + route.UpdatedAt = DateTime.UtcNow; + _unitOfWork.RouteBlueprints.Update(route); + + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Paso creado exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateRouteStepRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.RouteSteps.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Paso no encontrado"); + + var oldTransitTime = entity.StandardTransitTime; + + entity.StepOrder = request.StepOrder; + entity.StandardTransitTime = request.StandardTransitTime; + if (Enum.TryParse(request.StepType, out var st)) entity.StepType = st; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.RouteSteps.Update(entity); + + // Update route transit time + var route = await _unitOfWork.RouteBlueprints.GetByIdAsync(entity.RouteBlueprintId, cancellationToken); + if (route != null) + { + route.TotalTransitTime = route.TotalTransitTime - oldTransitTime + request.StandardTransitTime; + route.UpdatedAt = DateTime.UtcNow; + _unitOfWork.RouteBlueprints.Update(route); + } + + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Paso actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.RouteSteps.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Paso no encontrado"); + + // Update route totals + var route = await _unitOfWork.RouteBlueprints.GetByIdAsync(entity.RouteBlueprintId, cancellationToken); + if (route != null) + { + route.TotalSteps -= 1; + route.TotalTransitTime -= entity.StandardTransitTime; + route.UpdatedAt = DateTime.UtcNow; + _unitOfWork.RouteBlueprints.Update(route); + } + + _unitOfWork.RouteSteps.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Paso eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.RouteSteps.AnyAsync(s => s.Id == id, cancellationToken); + + public async Task> GetByRouteAsync(Guid routeBlueprintId, CancellationToken cancellationToken = default) + { + var steps = await _unitOfWork.RouteSteps.FindAsync(s => s.RouteBlueprintId == routeBlueprintId, cancellationToken); + var orderedSteps = steps.OrderBy(s => s.StepOrder).ToList(); + var dtos = new List(); + foreach (var s in orderedSteps) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return dtos; + } + + public async Task ReorderStepsAsync(Guid routeBlueprintId, IEnumerable stepIdsInOrder, CancellationToken cancellationToken = default) + { + var steps = await _unitOfWork.RouteSteps.FindAsync(s => s.RouteBlueprintId == routeBlueprintId, cancellationToken); + var stepDict = steps.ToDictionary(s => s.Id); + + int order = 1; + foreach (var stepId in stepIdsInOrder) + { + if (stepDict.TryGetValue(stepId, out var step)) + { + step.StepOrder = order++; + step.UpdatedAt = DateTime.UtcNow; + _unitOfWork.RouteSteps.Update(step); + } + } + + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Pasos reordenados exitosamente"); + } + + public async Task> AddStepToRouteAsync(Guid routeBlueprintId, CreateRouteStepRequest request, CancellationToken cancellationToken = default) + { + // Get max step order + var existingSteps = await _unitOfWork.RouteSteps.FindAsync(s => s.RouteBlueprintId == routeBlueprintId, cancellationToken); + var maxOrder = existingSteps.Any() ? existingSteps.Max(s => s.StepOrder) : 0; + + var newRequest = new CreateRouteStepRequest(routeBlueprintId, request.LocationId, maxOrder + 1, request.StandardTransitTime, request.StepType); + return await CreateAsync(newRequest, cancellationToken); + } + + private async Task MapToResponseAsync(RouteStep s, CancellationToken ct) + { + var route = await _unitOfWork.RouteBlueprints.GetByIdAsync(s.RouteBlueprintId, ct); + var location = await _unitOfWork.Locations.GetByIdAsync(s.LocationId, ct); + return new RouteStepResponse(s.Id, s.RouteBlueprintId, route?.Name ?? "", s.LocationId, location?.Name ?? "", + s.StepOrder, s.StandardTransitTime, s.StepType.ToString(), s.CreatedAt, s.UpdatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs index 52d9fd2..ffcb1c7 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs @@ -3,6 +3,7 @@ using Parhelion.Application.DTOs.Shipment; using Parhelion.Application.Interfaces; using Parhelion.Application.Interfaces.Services; +using Parhelion.Application.Interfaces.Validators; using Parhelion.Domain.Entities; using Parhelion.Domain.Enums; @@ -15,12 +16,15 @@ namespace Parhelion.Infrastructure.Services.Shipment; public class ShipmentService : IShipmentService { private readonly IUnitOfWork _unitOfWork; + private readonly ICargoCompatibilityValidator _cargoValidator; - public ShipmentService(IUnitOfWork unitOfWork) + public ShipmentService(IUnitOfWork unitOfWork, ICargoCompatibilityValidator cargoValidator) { _unitOfWork = unitOfWork; + _cargoValidator = cargoValidator; } + public async Task> GetAllAsync( PagedRequest request, CancellationToken cancellationToken = default) { @@ -156,6 +160,15 @@ public async Task> AssignToDriverAsync(Guid sh var truck = await _unitOfWork.Trucks.GetByIdAsync(truckId, cancellationToken); if (truck == null) return OperationResult.Fail("Camión no encontrado"); + // Validate cargo-truck compatibility + var items = await _unitOfWork.ShipmentItems.FindAsync(i => i.ShipmentId == shipmentId, cancellationToken); + var validation = _cargoValidator.ValidateShipmentForTruck(items, truck.Type); + if (!validation.IsValid) + { + var requiredType = validation.RequiredTruckType?.ToString() ?? "compatible"; + return OperationResult.Fail($"{validation.ErrorMessage} (Requerido: {requiredType}, Asignado: {truck.Type})"); + } + entity.DriverId = driverId; entity.TruckId = truckId; entity.AssignedAt = DateTime.UtcNow; @@ -167,6 +180,7 @@ public async Task> AssignToDriverAsync(Guid sh return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Envío asignado exitosamente"); } + public async Task> UpdateStatusAsync(Guid shipmentId, ShipmentStatus newStatus, CancellationToken cancellationToken = default) { var entity = await _unitOfWork.Shipments.GetByIdAsync(shipmentId, cancellationToken); diff --git a/backend/src/Parhelion.Infrastructure/Services/Warehouse/InventoryStockService.cs b/backend/src/Parhelion.Infrastructure/Services/Warehouse/InventoryStockService.cs new file mode 100644 index 0000000..b47711d --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Warehouse/InventoryStockService.cs @@ -0,0 +1,162 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Services.Warehouse; + +/// +/// Implementación del servicio de stock de inventario. +/// +public class InventoryStockService : IInventoryStockService +{ + private readonly IUnitOfWork _unitOfWork; + + public InventoryStockService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryStocks.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.InventoryStocks.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> CreateAsync(CreateInventoryStockRequest request, CancellationToken cancellationToken = default) + { + var zone = await _unitOfWork.WarehouseZones.GetByIdAsync(request.ZoneId, cancellationToken); + if (zone == null) return OperationResult.Fail("Zona no encontrada"); + + var location = await _unitOfWork.Locations.GetByIdAsync(zone.LocationId, cancellationToken); + + var entity = new InventoryStock + { + Id = Guid.NewGuid(), + TenantId = location?.TenantId ?? Guid.Empty, + ZoneId = request.ZoneId, + ProductId = request.ProductId, + Quantity = request.Quantity, + QuantityReserved = request.QuantityReserved, + BatchNumber = request.BatchNumber, + ExpiryDate = request.ExpiryDate, + UnitCost = request.UnitCost, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.InventoryStocks.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Stock creado exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateInventoryStockRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.InventoryStocks.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Stock no encontrado"); + + entity.Quantity = request.Quantity; + entity.QuantityReserved = request.QuantityReserved; + entity.BatchNumber = request.BatchNumber; + entity.ExpiryDate = request.ExpiryDate; + entity.LastCountDate = request.LastCountDate; + entity.UnitCost = request.UnitCost; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.InventoryStocks.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Stock actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.InventoryStocks.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Stock no encontrado"); + _unitOfWork.InventoryStocks.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Stock eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.InventoryStocks.AnyAsync(s => s.Id == id, cancellationToken); + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryStocks.GetPagedAsync(request, filter: s => s.TenantId == tenantId, orderBy: q => q.OrderBy(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByZoneAsync(Guid zoneId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryStocks.GetPagedAsync(request, filter: s => s.ZoneId == zoneId, orderBy: q => q.OrderBy(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByProductAsync(Guid productId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryStocks.GetPagedAsync(request, filter: s => s.ProductId == productId, orderBy: q => q.OrderBy(s => s.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetLowStockAsync(Guid tenantId, decimal threshold, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryStocks.GetPagedAsync(request, + filter: s => s.TenantId == tenantId && s.QuantityAvailable <= threshold, + orderBy: q => q.OrderBy(s => s.QuantityAvailable), cancellationToken); + var dtos = new List(); + foreach (var s in items) dtos.Add(await MapToResponseAsync(s, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> ReserveQuantityAsync(Guid stockId, decimal quantity, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.InventoryStocks.GetByIdAsync(stockId, cancellationToken); + if (entity == null) return OperationResult.Fail("Stock no encontrado"); + + if (entity.QuantityAvailable < quantity) + return OperationResult.Fail($"Cantidad insuficiente. Disponible: {entity.QuantityAvailable}"); + + entity.QuantityReserved += quantity; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.InventoryStocks.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), $"Reservados {quantity} unidades"); + } + + public async Task> ReleaseReservedAsync(Guid stockId, decimal quantity, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.InventoryStocks.GetByIdAsync(stockId, cancellationToken); + if (entity == null) return OperationResult.Fail("Stock no encontrado"); + + if (entity.QuantityReserved < quantity) + return OperationResult.Fail($"Cantidad reservada insuficiente: {entity.QuantityReserved}"); + + entity.QuantityReserved -= quantity; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.InventoryStocks.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), $"Liberadas {quantity} unidades"); + } + + private async Task MapToResponseAsync(InventoryStock s, CancellationToken ct) + { + var zone = await _unitOfWork.WarehouseZones.GetByIdAsync(s.ZoneId, ct); + var product = await _unitOfWork.CatalogItems.GetByIdAsync(s.ProductId, ct); + + return new InventoryStockResponse(s.Id, s.ZoneId, zone?.Name ?? "", s.ProductId, product?.Name ?? "", product?.Sku ?? "", + s.Quantity, s.QuantityReserved, s.QuantityAvailable, s.BatchNumber, s.ExpiryDate, s.LastCountDate, s.UnitCost, s.CreatedAt, s.UpdatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Warehouse/InventoryTransactionService.cs b/backend/src/Parhelion.Infrastructure/Services/Warehouse/InventoryTransactionService.cs new file mode 100644 index 0000000..be369e6 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Warehouse/InventoryTransactionService.cs @@ -0,0 +1,141 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Warehouse; + +/// +/// Implementación del servicio de transacciones de inventario. +/// +public class InventoryTransactionService : IInventoryTransactionService +{ + private readonly IUnitOfWork _unitOfWork; + + public InventoryTransactionService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetByTenantAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryTransactions.GetPagedAsync(request, filter: t => t.TenantId == tenantId, orderBy: q => q.OrderByDescending(t => t.Timestamp), cancellationToken); + var dtos = new List(); + foreach (var t in items) dtos.Add(await MapToResponseAsync(t, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.InventoryTransactions.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> GetByProductAsync(Guid productId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryTransactions.GetPagedAsync(request, filter: t => t.ProductId == productId, orderBy: q => q.OrderByDescending(t => t.Timestamp), cancellationToken); + var dtos = new List(); + foreach (var t in items) dtos.Add(await MapToResponseAsync(t, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByZoneAsync(Guid zoneId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryTransactions.GetPagedAsync(request, + filter: t => t.OriginZoneId == zoneId || t.DestinationZoneId == zoneId, + orderBy: q => q.OrderByDescending(t => t.Timestamp), cancellationToken); + var dtos = new List(); + foreach (var t in items) dtos.Add(await MapToResponseAsync(t, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetByTypeAsync(Guid tenantId, InventoryTransactionType type, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.InventoryTransactions.GetPagedAsync(request, + filter: t => t.TenantId == tenantId && t.TransactionType == type, + orderBy: q => q.OrderByDescending(t => t.Timestamp), cancellationToken); + var dtos = new List(); + foreach (var t in items) dtos.Add(await MapToResponseAsync(t, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> RecordReceiptAsync(CreateInventoryTransactionRequest request, Guid performedByUserId, CancellationToken cancellationToken = default) + { + return await CreateTransactionAsync(request, performedByUserId, InventoryTransactionType.Receipt, cancellationToken); + } + + public async Task> RecordDispatchAsync(CreateInventoryTransactionRequest request, Guid performedByUserId, CancellationToken cancellationToken = default) + { + return await CreateTransactionAsync(request, performedByUserId, InventoryTransactionType.Dispatch, cancellationToken); + } + + public async Task> RecordTransferAsync(CreateInventoryTransactionRequest request, Guid performedByUserId, CancellationToken cancellationToken = default) + { + if (!request.OriginZoneId.HasValue || !request.DestinationZoneId.HasValue) + return OperationResult.Fail("Transferencia requiere zona origen y destino"); + + return await CreateTransactionAsync(request, performedByUserId, InventoryTransactionType.InternalMove, cancellationToken); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.InventoryTransactions.AnyAsync(t => t.Id == id, cancellationToken); + + private async Task> CreateTransactionAsync( + CreateInventoryTransactionRequest request, + Guid performedByUserId, + InventoryTransactionType type, + CancellationToken cancellationToken) + { + // Get tenant from zone + Guid tenantId = Guid.Empty; + if (request.DestinationZoneId.HasValue) + { + var zone = await _unitOfWork.WarehouseZones.GetByIdAsync(request.DestinationZoneId.Value, cancellationToken); + if (zone != null) + { + var location = await _unitOfWork.Locations.GetByIdAsync(zone.LocationId, cancellationToken); + tenantId = location?.TenantId ?? Guid.Empty; + } + } + else if (request.OriginZoneId.HasValue) + { + var zone = await _unitOfWork.WarehouseZones.GetByIdAsync(request.OriginZoneId.Value, cancellationToken); + if (zone != null) + { + var location = await _unitOfWork.Locations.GetByIdAsync(zone.LocationId, cancellationToken); + tenantId = location?.TenantId ?? Guid.Empty; + } + } + + var entity = new InventoryTransaction + { + Id = Guid.NewGuid(), + TenantId = tenantId, + ProductId = request.ProductId, + OriginZoneId = request.OriginZoneId, + DestinationZoneId = request.DestinationZoneId, + Quantity = request.Quantity, + TransactionType = type, + PerformedByUserId = performedByUserId, + ShipmentId = request.ShipmentId, + BatchNumber = request.BatchNumber, + Remarks = request.Remarks, + Timestamp = DateTime.UtcNow, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.InventoryTransactions.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), $"Transacción {type} registrada exitosamente"); + } + + private async Task MapToResponseAsync(InventoryTransaction t, CancellationToken ct) + { + var product = await _unitOfWork.CatalogItems.GetByIdAsync(t.ProductId, ct); + var originZone = t.OriginZoneId.HasValue ? await _unitOfWork.WarehouseZones.GetByIdAsync(t.OriginZoneId.Value, ct) : null; + var destZone = t.DestinationZoneId.HasValue ? await _unitOfWork.WarehouseZones.GetByIdAsync(t.DestinationZoneId.Value, ct) : null; + var user = await _unitOfWork.Users.GetByIdAsync(t.PerformedByUserId, ct); + + return new InventoryTransactionResponse(t.Id, t.ProductId, product?.Name ?? "", t.OriginZoneId, originZone?.Name, t.DestinationZoneId, destZone?.Name, + t.Quantity, t.TransactionType.ToString(), t.PerformedByUserId, user?.FullName ?? "", t.ShipmentId, t.BatchNumber, t.Remarks, t.Timestamp, t.CreatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Warehouse/WarehouseOperatorService.cs b/backend/src/Parhelion.Infrastructure/Services/Warehouse/WarehouseOperatorService.cs new file mode 100644 index 0000000..24d3d57 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Warehouse/WarehouseOperatorService.cs @@ -0,0 +1,117 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Services.Warehouse; + +/// +/// Implementación del servicio de operadores de almacén. +/// +public class WarehouseOperatorService : IWarehouseOperatorService +{ + private readonly IUnitOfWork _unitOfWork; + + public WarehouseOperatorService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.WarehouseOperators.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(o => o.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var o in items) dtos.Add(await MapToResponseAsync(o, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseOperators.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> CreateAsync(CreateWarehouseOperatorRequest request, CancellationToken cancellationToken = default) + { + // Check if employee already has operator role + var existing = await _unitOfWork.WarehouseOperators.FirstOrDefaultAsync(o => o.EmployeeId == request.EmployeeId, cancellationToken); + if (existing != null) return OperationResult.Fail("El empleado ya es operador de almacén"); + + var entity = new WarehouseOperator + { + Id = Guid.NewGuid(), + EmployeeId = request.EmployeeId, + AssignedLocationId = request.AssignedLocationId, + PrimaryZoneId = request.PrimaryZoneId, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.WarehouseOperators.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Operador creado exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateWarehouseOperatorRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseOperators.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Operador no encontrado"); + + entity.AssignedLocationId = request.AssignedLocationId; + entity.PrimaryZoneId = request.PrimaryZoneId; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.WarehouseOperators.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Operador actualizado exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseOperators.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Operador no encontrado"); + _unitOfWork.WarehouseOperators.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Operador eliminado exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.WarehouseOperators.AnyAsync(o => o.Id == id, cancellationToken); + + public async Task> GetByLocationAsync(Guid locationId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.WarehouseOperators.GetPagedAsync(request, filter: o => o.AssignedLocationId == locationId, orderBy: q => q.OrderBy(o => o.CreatedAt), cancellationToken); + var dtos = new List(); + foreach (var o in items) dtos.Add(await MapToResponseAsync(o, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByEmployeeAsync(Guid employeeId, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseOperators.FirstOrDefaultAsync(o => o.EmployeeId == employeeId, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> AssignToZoneAsync(Guid operatorId, Guid zoneId, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseOperators.GetByIdAsync(operatorId, cancellationToken); + if (entity == null) return OperationResult.Fail("Operador no encontrado"); + + var zone = await _unitOfWork.WarehouseZones.GetByIdAsync(zoneId, cancellationToken); + if (zone == null) return OperationResult.Fail("Zona no encontrada"); + + entity.PrimaryZoneId = zoneId; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.WarehouseOperators.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Operador asignado a zona"); + } + + private async Task MapToResponseAsync(WarehouseOperator o, CancellationToken ct) + { + var employee = await _unitOfWork.Employees.GetByIdAsync(o.EmployeeId, ct); + var user = employee != null ? await _unitOfWork.Users.GetByIdAsync(employee.UserId, ct) : null; + var location = await _unitOfWork.Locations.GetByIdAsync(o.AssignedLocationId, ct); + var zone = o.PrimaryZoneId.HasValue ? await _unitOfWork.WarehouseZones.GetByIdAsync(o.PrimaryZoneId.Value, ct) : null; + + return new WarehouseOperatorResponse(o.Id, o.EmployeeId, user?.FullName ?? "", o.AssignedLocationId, location?.Name ?? "", o.PrimaryZoneId, zone?.Name, o.CreatedAt, o.UpdatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Warehouse/WarehouseZoneService.cs b/backend/src/Parhelion.Infrastructure/Services/Warehouse/WarehouseZoneService.cs new file mode 100644 index 0000000..46e9c44 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Warehouse/WarehouseZoneService.cs @@ -0,0 +1,106 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Warehouse; + +/// +/// Implementación del servicio de zonas de almacén. +/// +public class WarehouseZoneService : IWarehouseZoneService +{ + private readonly IUnitOfWork _unitOfWork; + + public WarehouseZoneService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + + public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.WarehouseZones.GetPagedAsync(request, filter: null, orderBy: q => q.OrderBy(z => z.Code), cancellationToken); + var dtos = new List(); + foreach (var z in items) dtos.Add(await MapToResponseAsync(z, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseZones.GetByIdAsync(id, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + public async Task> CreateAsync(CreateWarehouseZoneRequest request, CancellationToken cancellationToken = default) + { + var entity = new WarehouseZone + { + Id = Guid.NewGuid(), + LocationId = request.LocationId, + Code = request.Code, + Name = request.Name, + Type = Enum.TryParse(request.Type, out var t) ? t : WarehouseZoneType.Storage, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + await _unitOfWork.WarehouseZones.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Zona creada exitosamente"); + } + + public async Task> UpdateAsync(Guid id, UpdateWarehouseZoneRequest request, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseZones.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Zona no encontrada"); + + entity.Code = request.Code; + entity.Name = request.Name; + if (Enum.TryParse(request.Type, out var t)) entity.Type = t; + entity.IsActive = request.IsActive; + entity.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.WarehouseZones.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Zona actualizada exitosamente"); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseZones.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Zona no encontrada"); + _unitOfWork.WarehouseZones.Delete(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok("Zona eliminada exitosamente"); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) => + await _unitOfWork.WarehouseZones.AnyAsync(z => z.Id == id, cancellationToken); + + public async Task> GetByLocationAsync(Guid locationId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.WarehouseZones.GetPagedAsync(request, filter: z => z.LocationId == locationId, orderBy: q => q.OrderBy(z => z.Code), cancellationToken); + var dtos = new List(); + foreach (var z in items) dtos.Add(await MapToResponseAsync(z, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task> GetActiveAsync(Guid locationId, PagedRequest request, CancellationToken cancellationToken = default) + { + var (items, totalCount) = await _unitOfWork.WarehouseZones.GetPagedAsync(request, filter: z => z.LocationId == locationId && z.IsActive, orderBy: q => q.OrderBy(z => z.Code), cancellationToken); + var dtos = new List(); + foreach (var z in items) dtos.Add(await MapToResponseAsync(z, cancellationToken)); + return PagedResult.From(dtos, totalCount, request); + } + + public async Task GetByCodeAsync(Guid locationId, string code, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.WarehouseZones.FirstOrDefaultAsync(z => z.LocationId == locationId && z.Code == code, cancellationToken); + return entity != null ? await MapToResponseAsync(entity, cancellationToken) : null; + } + + private async Task MapToResponseAsync(WarehouseZone z, CancellationToken ct) + { + var location = await _unitOfWork.Locations.GetByIdAsync(z.LocationId, ct); + return new WarehouseZoneResponse(z.Id, z.LocationId, location?.Name ?? "", z.Code, z.Name, z.Type.ToString(), z.IsActive, z.CreatedAt, z.UpdatedAt); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Validators/CargoCompatibilityValidator.cs b/backend/src/Parhelion.Infrastructure/Validators/CargoCompatibilityValidator.cs new file mode 100644 index 0000000..45a244a --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Validators/CargoCompatibilityValidator.cs @@ -0,0 +1,74 @@ +using Parhelion.Application.Interfaces.Validators; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Validators; + +/// +/// Implementación de las reglas de compatibilidad carga-camión. +/// Valida según el README: +/// - Refrigerado → Refrigerated +/// - HAZMAT → HazmatTank +/// - Alto valor → Armored +/// +public class CargoCompatibilityValidator : ICargoCompatibilityValidator +{ + /// + /// Umbral de valor para requerir camión blindado (MXN). + /// + public decimal HighValueThreshold => 500_000m; + + public CargoValidationResult ValidateShipmentForTruck(IEnumerable items, TruckType truckType) + { + var itemList = items.ToList(); + if (!itemList.Any()) return CargoValidationResult.Success(); + + // Check refrigeration requirement + var needsRefrigeration = itemList.Any(i => i.RequiresRefrigeration); + if (needsRefrigeration && truckType != TruckType.Refrigerated) + { + return CargoValidationResult.Fail( + "Carga requiere cadena de frío. Asigne un camión Refrigerado.", + TruckType.Refrigerated); + } + + // Check HAZMAT requirement + var hasHazmat = itemList.Any(i => i.IsHazardous); + if (hasHazmat && truckType != TruckType.HazmatTank) + { + return CargoValidationResult.Fail( + "Carga contiene materiales peligrosos (HAZMAT). Asigne un camión HazmatTank.", + TruckType.HazmatTank); + } + + // Check high value requirement + var totalDeclaredValue = itemList.Sum(i => i.DeclaredValue * i.Quantity); + if (totalDeclaredValue > HighValueThreshold && truckType != TruckType.Armored) + { + return CargoValidationResult.Fail( + $"Valor declarado ({totalDeclaredValue:C}) excede umbral de alto valor ({HighValueThreshold:C}). Asigne un camión Blindado.", + TruckType.Armored); + } + + return CargoValidationResult.Success(); + } + + public TruckType DetermineRequiredTruckType(IEnumerable items) + { + var itemList = items.ToList(); + if (!itemList.Any()) return TruckType.DryBox; + + // Priority order: HAZMAT > Refrigerated > Armored > DryBox + if (itemList.Any(i => i.IsHazardous)) + return TruckType.HazmatTank; + + if (itemList.Any(i => i.RequiresRefrigeration)) + return TruckType.Refrigerated; + + var totalValue = itemList.Sum(i => i.DeclaredValue * i.Quantity); + if (totalValue > HighValueThreshold) + return TruckType.Armored; + + return TruckType.DryBox; + } +} diff --git a/backend/tests/Parhelion.Tests/Fixtures/ServiceTestFixture.cs b/backend/tests/Parhelion.Tests/Fixtures/ServiceTestFixture.cs index 75aa830..4e72829 100644 --- a/backend/tests/Parhelion.Tests/Fixtures/ServiceTestFixture.cs +++ b/backend/tests/Parhelion.Tests/Fixtures/ServiceTestFixture.cs @@ -101,6 +101,23 @@ private TestIds SeedTestData(ParhelionDbContext context) }; context.Locations.Add(location); + // Location 2 (for network tests) + var location2 = new Domain.Entities.Location + { + Id = ids.Location2Id, + TenantId = ids.TenantId, + Code = "GDL", + Name = "Guadalajara Hub", + Type = Domain.Enums.LocationType.RegionalHub, + FullAddress = "456 Test Blvd", + CanReceive = true, + CanDispatch = true, + IsInternal = true, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.Locations.Add(location2); + // Employee var employee = new Domain.Entities.Employee { @@ -131,6 +148,64 @@ private TestIds SeedTestData(ParhelionDbContext context) }; context.Trucks.Add(truck); + // ========== WMS DATA ========== + + // WarehouseZone + var zone = new Domain.Entities.WarehouseZone + { + Id = ids.ZoneId, + LocationId = ids.LocationId, + Code = "A1", + Name = "Zona A1 - Recepción", + Type = Domain.Enums.WarehouseZoneType.Receiving, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.WarehouseZones.Add(zone); + + // CatalogItem (Product) + var product = new Domain.Entities.CatalogItem + { + Id = ids.ProductId, + TenantId = ids.TenantId, + Sku = "PROD-001", + Name = "Test Product", + Description = "Test product description", + DefaultWeightKg = 5.0m, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.CatalogItems.Add(product); + + // InventoryStock + var stock = new Domain.Entities.InventoryStock + { + Id = ids.StockId, + TenantId = ids.TenantId, + ZoneId = ids.ZoneId, + ProductId = ids.ProductId, + Quantity = 100, + QuantityReserved = 10, + BatchNumber = "LOT-001", + ExpiryDate = DateTime.UtcNow.AddMonths(6), + CreatedAt = DateTime.UtcNow + }; + context.InventoryStocks.Add(stock); + + // RouteBlueprint + var route = new Domain.Entities.RouteBlueprint + { + Id = ids.RouteId, + TenantId = ids.TenantId, + Name = "MTY-GDL Express", + Description = "Ruta directa Monterrey-Guadalajara", + TotalSteps = 2, + TotalTransitTime = TimeSpan.FromHours(8), + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + context.RouteBlueprints.Add(route); + context.SaveChanges(); return ids; } @@ -148,6 +223,13 @@ public class TestIds public Guid AdminRoleId { get; } = Guid.Parse("11111111-1111-1111-1111-111111111111"); public Guid DriverRoleId { get; } = Guid.Parse("22222222-2222-2222-2222-222222222222"); public Guid LocationId { get; } = Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"); + public Guid Location2Id { get; } = Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccd"); public Guid EmployeeId { get; } = Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"); public Guid TruckId { get; } = Guid.Parse("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"); + // WMS IDs + public Guid ZoneId { get; } = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"); + public Guid ProductId { get; } = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab"); + public Guid StockId { get; } = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaac"); + public Guid RouteId { get; } = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaad"); } + diff --git a/backend/tests/Parhelion.Tests/Integration/FleetIntegrationTests.cs b/backend/tests/Parhelion.Tests/Integration/FleetIntegrationTests.cs new file mode 100644 index 0000000..25b02fa --- /dev/null +++ b/backend/tests/Parhelion.Tests/Integration/FleetIntegrationTests.cs @@ -0,0 +1,147 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Fleet; +using Parhelion.Infrastructure.Services.Fleet; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Integration; + +/// +/// Tests de integración para el área de Fleet Management (TMS - Fleet). +/// Verifica flujos completos que involucran Trucks, Drivers, FleetLogs. +/// +public class FleetIntegrationTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public FleetIntegrationTests(ServiceTestFixture fixture) => _fixture = fixture; + + /// + /// Flujo completo: Crear camión → Crear driver → Asignar → Registrar log + /// + [Fact] + public async Task FleetFlow_CreateTruckAndDriver_AssignAndLog() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var truckService = new TruckService(uow); + var driverService = new DriverService(uow); + var logService = new FleetLogService(uow); + + // Act 1: Create truck + var truckResult = await truckService.CreateAsync(new CreateTruckRequest( + Plate: "FLEET-001", + Model: "Volvo FH16", + Type: "Refrigerated", + MaxCapacityKg: 25000, + MaxVolumeM3: 80, + Vin: "VIN123456789", EngineNumber: null, Year: 2024, Color: "White", + InsurancePolicy: "INS-001", InsuranceExpiration: DateTime.UtcNow.AddYears(1), + VerificationNumber: null, VerificationExpiration: null)); + Assert.True(truckResult.Success); + var truckId = truckResult.Data!.Id; + + // Act 2: Create driver + var driverResult = await driverService.CreateAsync(new CreateDriverRequest( + EmployeeId: ids.EmployeeId, + LicenseNumber: "LIC-FLEET-001", + LicenseType: "Federal", + LicenseExpiration: DateTime.UtcNow.AddYears(2), + DefaultTruckId: null, + Status: "Available")); + Assert.True(driverResult.Success); + var driverId = driverResult.Data!.Id; + + // Act 3: Assign driver to truck + var assignResult = await driverService.AssignTruckAsync(driverId, truckId); + Assert.True(assignResult.Success); + Assert.Equal(truckId, assignResult.Data!.CurrentTruckId); + + // Act 4: Create fleet log for assignment via StartUsageAsync + var logResult = await logService.StartUsageAsync(driverId, truckId); + Assert.True(logResult.Success); + } + + /// + /// Flujo: Verificar que un driver puede cambiar de camión + /// + [Fact] + public async Task FleetGuard_DriverCanChangeTruck() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var truckService = new TruckService(uow); + var driverService = new DriverService(uow); + + // Create driver + var driverResult = await driverService.CreateAsync(new CreateDriverRequest( + EmployeeId: ids.EmployeeId, + LicenseNumber: "LIC-GUARD-001", + LicenseType: "CDL-A", + LicenseExpiration: DateTime.UtcNow.AddYears(2), + DefaultTruckId: null, + Status: "Available")); + var driverId = driverResult.Data!.Id; + + // Create two trucks + var truck1 = await truckService.CreateAsync(new CreateTruckRequest("GUARD-01", "Truck1", "DryBox", 10000, 40, null, null, null, null, null, null, null, null)); + var truck2 = await truckService.CreateAsync(new CreateTruckRequest("GUARD-02", "Truck2", "DryBox", 10000, 40, null, null, null, null, null, null, null, null)); + + // Act: Assign to first truck + var assign1 = await driverService.AssignTruckAsync(driverId, truck1.Data!.Id); + Assert.True(assign1.Success); + + // Act: Assign to second truck (should reassign) + var assign2 = await driverService.AssignTruckAsync(driverId, truck2.Data!.Id); + + // Assert: Should have changed to new truck + Assert.True(assign2.Success); + Assert.Equal(truck2.Data!.Id, assign2.Data!.CurrentTruckId); + } + + /// + /// Flujo: Desactivar camión y verificar que aparece en queries filtradas + /// + [Fact] + public async Task FleetManagement_TruckActiveStatus() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var truckService = new TruckService(uow); + var request = new PagedRequest { Page = 1, PageSize = 20 }; + + // Act: Set truck to inactive + await truckService.SetActiveStatusAsync(ids.TruckId, false); + var activeTrucks = await truckService.GetByActiveStatusAsync(ids.TenantId, true, request); + + // Assert: Inactive truck should not be in active list + Assert.True(activeTrucks.Items.All(t => t.IsActive)); + } + + /// + /// Flujo: Buscar logs por camión específico + /// + [Fact] + public async Task FleetLogs_TrackByTruck() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var driverService = new DriverService(uow); + var logService = new FleetLogService(uow); + + // Create a driver first + var driverResult = await driverService.CreateAsync(new CreateDriverRequest( + ids.EmployeeId, "LIC-LOG-001", "Federal", DateTime.UtcNow.AddYears(2), null, "Available")); + var driverId = driverResult.Data!.Id; + + // Create fleet logs via StartUsageAsync + await logService.StartUsageAsync(driverId, ids.TruckId); + + // Act: Get logs for truck + var request = new PagedRequest { Page = 1, PageSize = 20 }; + var truckLogs = await logService.GetByTruckAsync(ids.TruckId, request); + + // Assert: Should have at least 1 log + Assert.True(truckLogs.TotalCount >= 1); + } +} diff --git a/backend/tests/Parhelion.Tests/Integration/NetworkIntegrationTests.cs b/backend/tests/Parhelion.Tests/Integration/NetworkIntegrationTests.cs new file mode 100644 index 0000000..8c34b14 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Integration/NetworkIntegrationTests.cs @@ -0,0 +1,157 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Infrastructure.Services.Network; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Integration; + +/// +/// Tests de integración para el área de Network (TMS - Network/Routes). +/// Verifica flujos completos que involucran Locations, NetworkLinks, Routes, RouteSteps. +/// +public class NetworkIntegrationTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public NetworkIntegrationTests(ServiceTestFixture fixture) => _fixture = fixture; + + /// + /// Flujo completo: Crear enlace → Crear ruta → Agregar pasos ordenados + /// + [Fact] + public async Task NetworkFlow_CreateLinkAndRoute_WithOrderedSteps() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var linkService = new NetworkLinkService(uow); + var routeService = new RouteService(uow); + var stepService = new RouteStepService(uow); + + // Act 1: Create network link between locations + var linkResult = await linkService.CreateAsync(new CreateNetworkLinkRequest( + OriginLocationId: ids.LocationId, + DestinationLocationId: ids.Location2Id, + LinkType: "LineHaul", + TransitTime: TimeSpan.FromHours(8), + IsBidirectional: true)); + Assert.True(linkResult.Success); + Assert.True(linkResult.Data!.IsBidirectional); + + // Act 2: Create route blueprint + var routeResult = await routeService.CreateAsync(new CreateRouteBlueprintRequest( + Name: "MTY-GDL Direct", + Description: "Direct route from Monterrey to Guadalajara")); + Assert.True(routeResult.Success); + var routeId = routeResult.Data!.Id; + + // Act 3: Add route steps + var step1 = await stepService.CreateAsync(new CreateRouteStepRequest( + routeId, ids.LocationId, 1, TimeSpan.Zero, "Origin")); + var step2 = await stepService.CreateAsync(new CreateRouteStepRequest( + routeId, ids.Location2Id, 2, TimeSpan.FromHours(8), "Destination")); + Assert.True(step1.Success); + Assert.True(step2.Success); + + // Act 4: Verify route steps are ordered + var steps = await stepService.GetByRouteAsync(routeId); + var stepList = steps.ToList(); + Assert.Equal(2, stepList.Count); + Assert.Equal("Monterrey Hub", stepList[0].LocationName); + Assert.Equal("Guadalajara Hub", stepList[1].LocationName); + } + + /// + /// Flujo: Verificar búsqueda de rutas activas + /// + [Fact] + public async Task RouteManagement_SearchActiveRoutes() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var routeService = new RouteService(uow); + var request = new PagedRequest { Page = 1, PageSize = 20 }; + + // Act: Get active routes (seeded route is active) + var activeRoutes = await routeService.GetActiveAsync(ids.TenantId, request); + + // Assert: Should include seeded route + Assert.True(activeRoutes.TotalCount >= 1); + Assert.Contains(activeRoutes.Items, r => r.Name == "MTY-GDL Express"); + } + + /// + /// Flujo: Agregar pasos a ruta existente usando AddStepToRoute + /// + [Fact] + public async Task RouteSteps_AddStepsToExistingRoute() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var stepService = new RouteStepService(uow); + + // Act: Add multiple steps using AddStepToRoute (auto-ordering) + var step1 = await stepService.AddStepToRouteAsync(ids.RouteId, + new CreateRouteStepRequest(ids.RouteId, ids.LocationId, 0, TimeSpan.Zero, "Origin")); + var step2 = await stepService.AddStepToRouteAsync(ids.RouteId, + new CreateRouteStepRequest(ids.RouteId, ids.Location2Id, 0, TimeSpan.FromHours(8), "Destination")); + + // Assert: Steps should be auto-ordered + Assert.True(step1.Success); + Assert.True(step2.Success); + Assert.Equal(1, step1.Data!.StepOrder); + Assert.Equal(2, step2.Data!.StepOrder); + } + + /// + /// Flujo: Verificar enlaces bidireccionales + /// + [Fact] + public async Task NetworkLinks_BidirectionalLinks() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var linkService = new NetworkLinkService(uow); + + // Create bidirectional link + var linkResult = await linkService.CreateAsync(new CreateNetworkLinkRequest( + ids.LocationId, ids.Location2Id, "LineHaul", TimeSpan.FromHours(8), true)); + Assert.True(linkResult.Success); + + // Act: Query from both directions + var request = new PagedRequest { Page = 1, PageSize = 20 }; + var fromOrigin = await linkService.GetByOriginAsync(ids.LocationId, request); + var toDestination = await linkService.GetByDestinationAsync(ids.Location2Id, request); + + // Assert: Same link appears in both queries + Assert.True(fromOrigin.TotalCount >= 1); + Assert.True(toDestination.TotalCount >= 1); + } + + /// + /// Flujo: Eliminar paso y verificar que ruta se actualiza + /// + [Fact] + public async Task RouteSteps_DeleteStep_UpdatesRouteTotal() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var routeService = new RouteService(uow); + var stepService = new RouteStepService(uow); + + // Create route and add step + var routeResult = await routeService.CreateAsync(new CreateRouteBlueprintRequest("Test Route", "For deletion test")); + var routeId = routeResult.Data!.Id; + + var stepResult = await stepService.CreateAsync(new CreateRouteStepRequest( + routeId, ids.LocationId, 1, TimeSpan.FromHours(2), "Origin")); + var stepId = stepResult.Data!.Id; + + // Act: Delete step + var deleteResult = await stepService.DeleteAsync(stepId); + Assert.True(deleteResult.Success); + + // Verify step no longer exists + Assert.False(await stepService.ExistsAsync(stepId)); + } +} diff --git a/backend/tests/Parhelion.Tests/Integration/WMSIntegrationTests.cs b/backend/tests/Parhelion.Tests/Integration/WMSIntegrationTests.cs new file mode 100644 index 0000000..3a62c9d --- /dev/null +++ b/backend/tests/Parhelion.Tests/Integration/WMSIntegrationTests.cs @@ -0,0 +1,122 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Infrastructure.Services.Warehouse; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Integration; + +/// +/// Tests de integración para el área de WMS (Warehouse Management System). +/// Verifica flujos completos que involucran múltiples servicios WMS. +/// +public class WMSIntegrationTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public WMSIntegrationTests(ServiceTestFixture fixture) => _fixture = fixture; + + /// + /// Flujo completo: Crear zona → Crear producto → Agregar stock → Reservar → Liberar + /// + [Fact] + public async Task WarehouseFlow_CreateZone_AddStock_ReserveAndRelease() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var zoneService = new WarehouseZoneService(uow); + var stockService = new InventoryStockService(uow); + + // Act 1: Create new zone + var zoneResult = await zoneService.CreateAsync( + new CreateWarehouseZoneRequest(ids.LocationId, "C3", "Zona C3 - Cold Storage", "ColdChain")); + Assert.True(zoneResult.Success); + var newZoneId = zoneResult.Data!.Id; + + // Act 2: Add inventory to new zone + var stockResult = await stockService.CreateAsync( + new CreateInventoryStockRequest(newZoneId, ids.ProductId, 200, 0, "LOT-COLD-001", DateTime.UtcNow.AddDays(30), 50.00m)); + Assert.True(stockResult.Success); + var stockId = stockResult.Data!.Id; + Assert.Equal(200, stockResult.Data.QuantityAvailable); + + // Act 3: Reserve quantity + var reserveResult = await stockService.ReserveQuantityAsync(stockId, 75); + Assert.True(reserveResult.Success); + Assert.Equal(125, reserveResult.Data!.QuantityAvailable); + Assert.Equal(75, reserveResult.Data.QuantityReserved); + + // Act 4: Release part of reserved + var releaseResult = await stockService.ReleaseReservedAsync(stockId, 25); + Assert.True(releaseResult.Success); + Assert.Equal(150, releaseResult.Data!.QuantityAvailable); + Assert.Equal(50, releaseResult.Data.QuantityReserved); + } + + /// + /// Flujo: Verificar stock por zona después de múltiples operaciones + /// + [Fact] + public async Task InventoryTracking_MultipleStocksPerZone() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var stockService = new InventoryStockService(uow); + + // Create multiple stocks in same zone with different batches + await stockService.CreateAsync(new CreateInventoryStockRequest(ids.ZoneId, ids.ProductId, 100, 0, "LOT-A", DateTime.UtcNow.AddMonths(3), null)); + await stockService.CreateAsync(new CreateInventoryStockRequest(ids.ZoneId, ids.ProductId, 150, 0, "LOT-B", DateTime.UtcNow.AddMonths(6), null)); + + // Act: Get all stocks for zone + var request = new PagedRequest { Page = 1, PageSize = 20 }; + var zoneStocks = await stockService.GetByZoneAsync(ids.ZoneId, request); + + // Assert: Should have original seeded stock + 2 new ones + Assert.True(zoneStocks.TotalCount >= 3); + } + + /// + /// Flujo: Verificar que zonas inactivas no afectan queries de zona activa + /// + [Fact] + public async Task ZoneManagement_ActiveVsInactive() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var zoneService = new WarehouseZoneService(uow); + var request = new PagedRequest { Page = 1, PageSize = 20 }; + + // Create active zone + await zoneService.CreateAsync(new CreateWarehouseZoneRequest(ids.LocationId, "ACTIVE-1", "Active Zone", "Storage")); + + // Create and deactivate zone + var inactiveResult = await zoneService.CreateAsync(new CreateWarehouseZoneRequest(ids.LocationId, "INACTIVE-1", "Inactive Zone", "Storage")); + await zoneService.UpdateAsync(inactiveResult.Data!.Id, new UpdateWarehouseZoneRequest("INACTIVE-1", "Inactive Zone", "Storage", false)); + + // Act: Get only active zones + var activeZones = await zoneService.GetActiveAsync(ids.LocationId, request); + + // Assert: Should not include inactive zone + Assert.True(activeZones.Items.All(z => z.IsActive)); + } + + /// + /// Flujo: Fail-safe - Reservar más de lo disponible debe fallar + /// + [Fact] + public async Task InventoryGuard_CannotOverReserve() + { + // Arrange + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var stockService = new InventoryStockService(uow); + + // Stock has 100 total, 10 reserved = 90 available + + // Act: Try to reserve 95 (more than 90 available) + var result = await stockService.ReserveQuantityAsync(ids.StockId, 95); + + // Assert: Should fail + Assert.False(result.Success); + Assert.Contains("insuficiente", result.Message.ToLower()); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Network/NetworkLinkServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Network/NetworkLinkServiceTests.cs new file mode 100644 index 0000000..d01323c --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Network/NetworkLinkServiceTests.cs @@ -0,0 +1,86 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Infrastructure.Services.Network; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Network; + +/// +/// Tests para NetworkLinkService. +/// +public class NetworkLinkServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public NetworkLinkServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new NetworkLinkService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + var result = await service.GetAllAsync(request); + + Assert.NotNull(result); + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccess() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new NetworkLinkService(uow); + var request = new CreateNetworkLinkRequest( + ids.LocationId, ids.Location2Id, "LineHaul", TimeSpan.FromHours(8), true); + + var result = await service.CreateAsync(request); + + Assert.True(result.Success); + Assert.Equal("Monterrey Hub", result.Data!.OriginLocationName); + Assert.Equal("Guadalajara Hub", result.Data!.DestinationLocationName); + } + + [Fact] + public async Task CreateAsync_InvalidOrigin_ReturnsFailure() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new NetworkLinkService(uow); + var request = new CreateNetworkLinkRequest( + Guid.NewGuid(), ids.Location2Id, "LineHaul", TimeSpan.FromHours(8), true); + + var result = await service.CreateAsync(request); + + Assert.False(result.Success); + Assert.Contains("origen", result.Message.ToLower()); + } + + [Fact] + public async Task GetByOriginAsync_ReturnsLinksFromLocation() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new NetworkLinkService(uow); + + // First create a link + await service.CreateAsync(new CreateNetworkLinkRequest( + ids.LocationId, ids.Location2Id, "LineHaul", TimeSpan.FromHours(8), true)); + + var request = new PagedRequest { Page = 1, PageSize = 10 }; + var result = await service.GetByOriginAsync(ids.LocationId, request); + + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task ExistsAsync_NonExistingLink_ReturnsFalse() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new NetworkLinkService(uow); + + var exists = await service.ExistsAsync(Guid.NewGuid()); + + Assert.False(exists); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Network/RouteStepServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Network/RouteStepServiceTests.cs new file mode 100644 index 0000000..5466bc3 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Network/RouteStepServiceTests.cs @@ -0,0 +1,96 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Network; +using Parhelion.Infrastructure.Services.Network; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Network; + +/// +/// Tests para RouteStepService. +/// +public class RouteStepServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public RouteStepServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RouteStepService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + var result = await service.GetAllAsync(request); + + Assert.NotNull(result); + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccessAndUpdatesRoute() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RouteStepService(uow); + var request = new CreateRouteStepRequest(ids.RouteId, ids.LocationId, 1, TimeSpan.FromHours(2), "Origin"); + + var result = await service.CreateAsync(request); + + Assert.True(result.Success); + Assert.Equal(1, result.Data!.StepOrder); + Assert.Equal("Monterrey Hub", result.Data!.LocationName); + } + + [Fact] + public async Task GetByRouteAsync_ReturnsOrderedSteps() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RouteStepService(uow); + + // Create two steps + await service.CreateAsync(new CreateRouteStepRequest(ids.RouteId, ids.LocationId, 1, TimeSpan.FromHours(0), "Origin")); + await service.CreateAsync(new CreateRouteStepRequest(ids.RouteId, ids.Location2Id, 2, TimeSpan.FromHours(8), "Destination")); + + var result = await service.GetByRouteAsync(ids.RouteId); + + Assert.NotNull(result); + var steps = result.ToList(); + Assert.Equal(2, steps.Count); + Assert.Equal(1, steps[0].StepOrder); + Assert.Equal(2, steps[1].StepOrder); + } + + [Fact] + public async Task AddStepToRouteAsync_AddsAtEnd() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RouteStepService(uow); + + // Create first step + await service.CreateAsync(new CreateRouteStepRequest(ids.RouteId, ids.LocationId, 1, TimeSpan.FromHours(0), "Origin")); + + // Add step at end + var result = await service.AddStepToRouteAsync(ids.RouteId, + new CreateRouteStepRequest(ids.RouteId, ids.Location2Id, 0, TimeSpan.FromHours(4), "Intermediate")); + + Assert.True(result.Success); + Assert.Equal(2, result.Data!.StepOrder); // Auto-incremented + } + + [Fact] + public async Task DeleteAsync_RemovesStepAndUpdatesRoute() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new RouteStepService(uow); + + // Create step + var createResult = await service.CreateAsync(new CreateRouteStepRequest(ids.RouteId, ids.LocationId, 1, TimeSpan.FromHours(2), "Origin")); + var stepId = createResult.Data!.Id; + + // Delete + var deleteResult = await service.DeleteAsync(stepId); + + Assert.True(deleteResult.Success); + Assert.False(await service.ExistsAsync(stepId)); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs index 4d2f433..0519568 100644 --- a/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs +++ b/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs @@ -1,6 +1,7 @@ using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Shipment; using Parhelion.Infrastructure.Services.Shipment; +using Parhelion.Infrastructure.Validators; using Parhelion.Tests.Fixtures; using Xunit; @@ -12,6 +13,7 @@ namespace Parhelion.Tests.Unit.Services.Shipment; public class ShipmentServiceTests : IClassFixture { private readonly ServiceTestFixture _fixture; + private readonly CargoCompatibilityValidator _cargoValidator = new(); public ShipmentServiceTests(ServiceTestFixture fixture) => _fixture = fixture; @@ -20,7 +22,7 @@ public async Task GetAllAsync_ReturnsPagedResult() { // Arrange var (uow, ctx) = _fixture.CreateUnitOfWork(); - var service = new ShipmentService(uow); + var service = new ShipmentService(uow, _cargoValidator); var request = new PagedRequest { Page = 1, PageSize = 10 }; // Act @@ -35,7 +37,7 @@ public async Task GetByIdAsync_NonExisting_ReturnsNull() { // Arrange var (uow, ctx) = _fixture.CreateUnitOfWork(); - var service = new ShipmentService(uow); + var service = new ShipmentService(uow, _cargoValidator); // Act var result = await service.GetByIdAsync(Guid.NewGuid()); @@ -49,7 +51,7 @@ public async Task ExistsAsync_NonExisting_ReturnsFalse() { // Arrange var (uow, ctx) = _fixture.CreateUnitOfWork(); - var service = new ShipmentService(uow); + var service = new ShipmentService(uow, _cargoValidator); // Act var exists = await service.ExistsAsync(Guid.NewGuid()); @@ -58,3 +60,4 @@ public async Task ExistsAsync_NonExisting_ReturnsFalse() Assert.False(exists); } } + diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Warehouse/InventoryStockServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Warehouse/InventoryStockServiceTests.cs new file mode 100644 index 0000000..b43d491 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Warehouse/InventoryStockServiceTests.cs @@ -0,0 +1,121 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Infrastructure.Services.Warehouse; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Warehouse; + +/// +/// Tests para InventoryStockService. +/// +public class InventoryStockServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public InventoryStockServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new InventoryStockService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + var result = await service.GetAllAsync(request); + + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task GetByIdAsync_ExistingStock_ReturnsStock() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new InventoryStockService(uow); + + var result = await service.GetByIdAsync(ids.StockId); + + Assert.NotNull(result); + Assert.Equal(100, result.Quantity); + Assert.Equal(10, result.QuantityReserved); + Assert.Equal(90, result.QuantityAvailable); + } + + [Fact] + public async Task GetByZoneAsync_ReturnsStockForZone() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new InventoryStockService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + var result = await service.GetByZoneAsync(ids.ZoneId, request); + + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task GetByProductAsync_ReturnsStockForProduct() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new InventoryStockService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + var result = await service.GetByProductAsync(ids.ProductId, request); + + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task ReserveQuantityAsync_SufficientStock_ReturnsSuccess() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new InventoryStockService(uow); + + var result = await service.ReserveQuantityAsync(ids.StockId, 20); + + Assert.True(result.Success); + Assert.Equal(30, result.Data!.QuantityReserved); // 10 original + 20 + Assert.Equal(70, result.Data!.QuantityAvailable); + } + + [Fact] + public async Task ReserveQuantityAsync_InsufficientStock_ReturnsFailure() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new InventoryStockService(uow); + + var result = await service.ReserveQuantityAsync(ids.StockId, 100); // Only 90 available + + Assert.False(result.Success); + Assert.Contains("insuficiente", result.Message); + } + + [Fact] + public async Task ReleaseReservedAsync_ValidQuantity_ReturnsSuccess() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new InventoryStockService(uow); + + var result = await service.ReleaseReservedAsync(ids.StockId, 5); + + Assert.True(result.Success); + Assert.Equal(5, result.Data!.QuantityReserved); // 10 original - 5 + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccess() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new InventoryStockService(uow); + var request = new CreateInventoryStockRequest(ids.ZoneId, ids.ProductId, 50, 0, "LOT-002", DateTime.UtcNow.AddMonths(12), 25.00m); + + var result = await service.CreateAsync(request); + + Assert.True(result.Success); + Assert.Equal(50, result.Data!.Quantity); + Assert.Equal("LOT-002", result.Data!.BatchNumber); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Warehouse/WarehouseZoneServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Warehouse/WarehouseZoneServiceTests.cs new file mode 100644 index 0000000..3a2befc --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Services/Warehouse/WarehouseZoneServiceTests.cs @@ -0,0 +1,104 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Warehouse; +using Parhelion.Infrastructure.Services.Warehouse; +using Parhelion.Tests.Fixtures; +using Xunit; + +namespace Parhelion.Tests.Unit.Services.Warehouse; + +/// +/// Tests para WarehouseZoneService. +/// +public class WarehouseZoneServiceTests : IClassFixture +{ + private readonly ServiceTestFixture _fixture; + + public WarehouseZoneServiceTests(ServiceTestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task GetAllAsync_ReturnsPagedResult() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new WarehouseZoneService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + var result = await service.GetAllAsync(request); + + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task GetByIdAsync_ExistingZone_ReturnsZone() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new WarehouseZoneService(uow); + + var result = await service.GetByIdAsync(ids.ZoneId); + + Assert.NotNull(result); + Assert.Equal("A1", result.Code); + } + + [Fact] + public async Task GetByLocationAsync_ReturnsZonesForLocation() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new WarehouseZoneService(uow); + var request = new PagedRequest { Page = 1, PageSize = 10 }; + + var result = await service.GetByLocationAsync(ids.LocationId, request); + + Assert.NotNull(result); + Assert.True(result.TotalCount >= 1); + } + + [Fact] + public async Task GetByCodeAsync_ExistingCode_ReturnsZone() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new WarehouseZoneService(uow); + + var result = await service.GetByCodeAsync(ids.LocationId, "A1"); + + Assert.NotNull(result); + Assert.Equal(ids.ZoneId, result.Id); + } + + [Fact] + public async Task CreateAsync_ValidRequest_ReturnsSuccess() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new WarehouseZoneService(uow); + var request = new CreateWarehouseZoneRequest(ids.LocationId, "B2", "Zona B2 - Storage", "Storage"); + + var result = await service.CreateAsync(request); + + Assert.True(result.Success); + Assert.Equal("B2", result.Data!.Code); + } + + [Fact] + public async Task UpdateAsync_ExistingZone_ReturnsSuccess() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new WarehouseZoneService(uow); + var request = new UpdateWarehouseZoneRequest("A1-UPD", "Zona A1 Updated", "Storage", true); + + var result = await service.UpdateAsync(ids.ZoneId, request); + + Assert.True(result.Success); + Assert.Equal("A1-UPD", result.Data!.Code); + } + + [Fact] + public async Task ExistsAsync_ExistingZone_ReturnsTrue() + { + var (uow, ctx, ids) = _fixture.CreateSeededUnitOfWork(); + var service = new WarehouseZoneService(uow); + + var exists = await service.ExistsAsync(ids.ZoneId); + + Assert.True(exists); + } +} diff --git a/backend/tests/Parhelion.Tests/Unit/Validators/CargoCompatibilityValidatorTests.cs b/backend/tests/Parhelion.Tests/Unit/Validators/CargoCompatibilityValidatorTests.cs new file mode 100644 index 0000000..b7f6035 --- /dev/null +++ b/backend/tests/Parhelion.Tests/Unit/Validators/CargoCompatibilityValidatorTests.cs @@ -0,0 +1,170 @@ +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Infrastructure.Validators; +using Xunit; + +namespace Parhelion.Tests.Unit.Validators; + +/// +/// Tests para CargoCompatibilityValidator. +/// Verifica las reglas de negocio de compatibilidad carga-camión. +/// +public class CargoCompatibilityValidatorTests +{ + private readonly CargoCompatibilityValidator _validator = new(); + + // ========== REFRIGERATION TESTS ========== + + [Fact] + public void RefrigeratedCargo_WithRefrigeratedTruck_ReturnsSuccess() + { + var items = new[] { CreateItem(requiresRefrigeration: true) }; + + var result = _validator.ValidateShipmentForTruck(items, TruckType.Refrigerated); + + Assert.True(result.IsValid); + } + + [Fact] + public void RefrigeratedCargo_WithDryBoxTruck_ReturnsFailure() + { + var items = new[] { CreateItem(requiresRefrigeration: true) }; + + var result = _validator.ValidateShipmentForTruck(items, TruckType.DryBox); + + Assert.False(result.IsValid); + Assert.Equal(TruckType.Refrigerated, result.RequiredTruckType); + Assert.Contains("cadena de frío", result.ErrorMessage!.ToLower()); + } + + // ========== HAZMAT TESTS ========== + + [Fact] + public void HazmatCargo_WithHazmatTruck_ReturnsSuccess() + { + var items = new[] { CreateItem(isHazardous: true) }; + + var result = _validator.ValidateShipmentForTruck(items, TruckType.HazmatTank); + + Assert.True(result.IsValid); + } + + [Fact] + public void HazmatCargo_WithRefrigeratedTruck_ReturnsFailure() + { + var items = new[] { CreateItem(isHazardous: true) }; + + var result = _validator.ValidateShipmentForTruck(items, TruckType.Refrigerated); + + Assert.False(result.IsValid); + Assert.Equal(TruckType.HazmatTank, result.RequiredTruckType); + Assert.Contains("hazmat", result.ErrorMessage!.ToLower()); + } + + // ========== HIGH VALUE TESTS ========== + + [Fact] + public void HighValueCargo_WithArmoredTruck_ReturnsSuccess() + { + var items = new[] { CreateItem(declaredValue: 600_000m) }; + + var result = _validator.ValidateShipmentForTruck(items, TruckType.Armored); + + Assert.True(result.IsValid); + } + + [Fact] + public void HighValueCargo_WithDryBoxTruck_ReturnsFailure() + { + var items = new[] { CreateItem(declaredValue: 600_000m) }; + + var result = _validator.ValidateShipmentForTruck(items, TruckType.DryBox); + + Assert.False(result.IsValid); + Assert.Equal(TruckType.Armored, result.RequiredTruckType); + Assert.Contains("blindado", result.ErrorMessage!.ToLower()); + } + + // ========== STANDARD CARGO TESTS ========== + + [Fact] + public void StandardCargo_WithDryBoxTruck_ReturnsSuccess() + { + var items = new[] { CreateItem() }; + + var result = _validator.ValidateShipmentForTruck(items, TruckType.DryBox); + + Assert.True(result.IsValid); + } + + [Fact] + public void EmptyItems_ReturnsSuccess() + { + var items = Array.Empty(); + + var result = _validator.ValidateShipmentForTruck(items, TruckType.DryBox); + + Assert.True(result.IsValid); + } + + // ========== DETERMINE REQUIRED TYPE TESTS ========== + + [Fact] + public void DetermineRequiredType_Hazmat_ReturnsHazmatTank() + { + var items = new[] { CreateItem(isHazardous: true) }; + + var result = _validator.DetermineRequiredTruckType(items); + + Assert.Equal(TruckType.HazmatTank, result); + } + + [Fact] + public void DetermineRequiredType_Refrigerated_ReturnsRefrigerated() + { + var items = new[] { CreateItem(requiresRefrigeration: true) }; + + var result = _validator.DetermineRequiredTruckType(items); + + Assert.Equal(TruckType.Refrigerated, result); + } + + [Fact] + public void DetermineRequiredType_HighValue_ReturnsArmored() + { + var items = new[] { CreateItem(declaredValue: 600_000m) }; + + var result = _validator.DetermineRequiredTruckType(items); + + Assert.Equal(TruckType.Armored, result); + } + + [Fact] + public void DetermineRequiredType_Standard_ReturnsDryBox() + { + var items = new[] { CreateItem() }; + + var result = _validator.DetermineRequiredTruckType(items); + + Assert.Equal(TruckType.DryBox, result); + } + + private static ShipmentItem CreateItem( + bool requiresRefrigeration = false, + bool isHazardous = false, + decimal declaredValue = 1000m) => new() + { + Id = Guid.NewGuid(), + ShipmentId = Guid.NewGuid(), + Description = "Test Item", + Quantity = 1, + WeightKg = 10, + WidthCm = 50, + HeightCm = 50, + LengthCm = 50, + RequiresRefrigeration = requiresRefrigeration, + IsHazardous = isHazardous, + DeclaredValue = declaredValue, + CreatedAt = DateTime.UtcNow + }; +} From 17ce7867dc50174b004360779d7cf002af976e5f Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Tue, 23 Dec 2025 02:11:01 +0000 Subject: [PATCH 26/34] feat(v0.5.6): Secure Webhooks, n8n Integration & Driver Geospatial Search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementación del sistema de automatización e integración con n8n mediante Webhooks Seguros y Event-Driven Architecture. Características Principales: - **Secure Webhooks:** Implementación de (JWT efímero de 15 min) para autenticación segura en flujos de ida y vuelta (Round-trip) sin exponer API Keys estáticas. - **Notification Engine:** Nuevo servicio de notificaciones con soporte para alertas de Crisis Management y persistencia en base de datos. - **Geospatial Search:** Nuevo endpoint utilizando fórmula Haversine para localizar choferes disponibles en un radio específico. - **Multi-Tenant Auth:** Refactorización de para soportar autenticación híbrida (Header y ). Cambios Técnicos: - Estandarización de envelope con y . - Serialización JSON estrictamente para compatibilidad con n8n. - Integración de (Fire-and-Forget) en . - Actualización de documentación técnica (, ) y Swagger. - Validación E2E completa del flujo de excepción de envíos. --- CHANGELOG.md | 62 + README.md | 72 +- api-architecture.md | 5 +- backend/scripts/seed-e2e-fix.sql | 59 + backend/scripts/seed-e2e-test.sql | 307 ++ .../Controllers/DriversController.cs | 42 + .../Controllers/NotificationsController.cs | 209 ++ .../Controllers/ShipmentsController.cs | 4 + .../Controllers/TrucksController.cs | 10 + .../src/Parhelion.API/Data/CrisisSeeder.cs | 166 ++ .../Filters/ServiceApiKeyAttribute.cs | 140 + backend/src/Parhelion.API/Program.cs | 68 +- backend/src/Parhelion.API/appsettings.json | 19 +- .../DTOs/Core/CoreDtos.cs | 6 +- .../DTOs/Notification/NotificationDTOs.cs | 70 + .../DTOs/Webhooks/WebhookEvent.cs | 53 + .../DTOs/Webhooks/WebhookEvents.cs | 149 + .../Interfaces/ICallbackTokenService.cs | 33 + .../Interfaces/IUnitOfWork.cs | 4 + .../Interfaces/IWebhookPublisher.cs | 28 + .../Interfaces/Services/IDriverService.cs | 4 + .../Services/INotificationService.cs | 63 + .../Interfaces/Services/ITruckService.cs | 4 + .../Parhelion.Domain/Entities/Notification.cs | 89 + .../Entities/ServiceApiKey.cs | 43 + .../src/Parhelion.Domain/Entities/Truck.cs | 11 + .../Enums/NotificationEnums.cs | 43 + .../Auth/JwtService.cs | 5 +- .../NotificationConfiguration.cs | 64 + .../ServiceApiKeyConfiguration.cs | 51 + .../Data/Configurations/TruckConfiguration.cs | 6 + ...0251221235514_AddNotifications.Designer.cs | 2530 ++++++++++++++++ .../20251221235514_AddNotifications.cs | 103 + ...251222022719_AddTruckTelemetry.Designer.cs | 2541 ++++++++++++++++ .../20251222022719_AddTruckTelemetry.cs | 53 + ...251222225257_AddServiceApiKeys.Designer.cs | 2628 +++++++++++++++++ .../20251222225257_AddServiceApiKeys.cs | 66 + .../ParhelionDbContextModelSnapshot.cs | 225 ++ .../Data/ParhelionDbContext.cs | 6 + .../External/Webhooks/N8nConfiguration.cs | 42 + .../External/Webhooks/N8nWebhookPublisher.cs | 147 + .../Repositories/UnitOfWork.cs | 10 + .../Services/Auth/CallbackTokenService.cs | 109 + .../Services/Core/TenantService.cs | 56 +- .../Services/Fleet/DriverService.cs | 58 + .../Services/Fleet/TruckService.cs | 14 + .../Notification/NotificationService.cs | 233 ++ .../Services/Shipment/ShipmentService.cs | 119 +- .../Integration/WMSIntegrationTests.cs | 1 + .../Network/NetworkLinkServiceTests.cs | 1 + .../Services/Shipment/ShipmentServiceTests.cs | 9 +- database-schema.md | 121 +- docker-compose.yml | 80 +- service-webhooks.md | 108 + 54 files changed, 11101 insertions(+), 48 deletions(-) create mode 100644 backend/scripts/seed-e2e-fix.sql create mode 100644 backend/scripts/seed-e2e-test.sql create mode 100644 backend/src/Parhelion.API/Controllers/NotificationsController.cs create mode 100644 backend/src/Parhelion.API/Data/CrisisSeeder.cs create mode 100644 backend/src/Parhelion.API/Filters/ServiceApiKeyAttribute.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Notification/NotificationDTOs.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Webhooks/WebhookEvent.cs create mode 100644 backend/src/Parhelion.Application/DTOs/Webhooks/WebhookEvents.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/ICallbackTokenService.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/IWebhookPublisher.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/Services/INotificationService.cs create mode 100644 backend/src/Parhelion.Domain/Entities/Notification.cs create mode 100644 backend/src/Parhelion.Domain/Entities/ServiceApiKey.cs create mode 100644 backend/src/Parhelion.Domain/Enums/NotificationEnums.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/NotificationConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Configurations/ServiceApiKeyConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251221235514_AddNotifications.Designer.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251221235514_AddNotifications.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251222022719_AddTruckTelemetry.Designer.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251222022719_AddTruckTelemetry.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251222225257_AddServiceApiKeys.Designer.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251222225257_AddServiceApiKeys.cs create mode 100644 backend/src/Parhelion.Infrastructure/External/Webhooks/N8nConfiguration.cs create mode 100644 backend/src/Parhelion.Infrastructure/External/Webhooks/N8nWebhookPublisher.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Auth/CallbackTokenService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Notification/NotificationService.cs create mode 100644 service-webhooks.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e5bd6eb..37f6cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,68 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.5.6] - 2025-12-22 + +### Agregado + +- **Sistema de Webhooks (Backend → n8n)**: + + - `IWebhookPublisher` - Interface en Application Layer + - `N8nWebhookPublisher` - Implementación fire-and-forget con logging + - `ICallbackTokenService` & `CallbackTokenService` - Implementación de tokens JWT efímeros (15m) + - `NullWebhookPublisher` - Implementación vacía para desactivar webhooks + - `N8nConfiguration` - Configuración tipada desde appsettings + - 5 DTOs de eventos: ShipmentException, BookingRequest, HandshakeAttempt, StatusChanged, CheckpointCreated + +- **Sistema de Notificaciones (n8n → Backend)**: + + - Nueva entidad `Notification` con tipos (Alert, Info, Warning, Success) y prioridades + - `NotificationsController` con endpoints para n8n (POST) y apps móviles (GET) + - `INotificationService` + `NotificationService` implementación + - Migración `AddNotifications` aplicada + +- **Autenticación de Servicios Externos (Multi-Tenant)**: + + - Nueva entidad `ServiceApiKey` con TenantId, KeyHash (SHA256), Scopes, Expiración + - `ServiceApiKeyAttribute` - Filtro que valida X-Service-Key contra BD + - Lookup de TenantId desde tabla en lugar de hardcoding + - **Generación automática de API Key** al crear nuevo Tenant (responsabilidad del SuperAdmin) + - Migración `AddServiceApiKeys` aplicada + +- **Telemetría GPS de Camiones**: + + - Campos `LastLatitude`, `LastLongitude`, `LastLocationUpdate` en Truck + - Endpoint `POST /api/trucks/{id}/location` para simulación GPS + +- **Búsqueda Geoespacial de Choferes**: + + - `IDriverService.GetNearbyDriversAsync` con fórmula Haversine + - Endpoint `GET /api/drivers/nearby?lat=&lon=&radius=` + - Filtrado por DriverStatus.Available y TenantId + +### Modificado + +- `DriversController.GetNearby` - Ahora resuelve TenantId desde ServiceApiKey (producción-ready) +- `ServiceApiKeyAttribute` - Refinado para soportar auth híbrida (Header `X-Service-Key` y `Authorization: Bearer`) +- `ShipmentService.UpdateStatusAsync` - Publica webhook `shipment.exception` automáticamente +- `Program.cs` - Registro condicional de IWebhookPublisher (N8n o Null) +- `docker-compose.yml` - Agregado servicio n8n con PostgreSQL compartido +- Estandarización de "Envelope" JSON para todos los eventos de sistema (CorrelationId, Timestamp, CallbackToken) + +### Seguridad + +- API Keys almacenadas como SHA256 hash (nunca plain text) +- Validación de expiración y estado activo +- Rate limiting de actualización LastUsedAt (fire-and-forget) + +### Notas Técnicas + +- Webhooks son fire-and-forget: errores se loguean pero no interrumpen flujo +- Configuración `N8n:Enabled` controla activación de webhooks +- ServiceApiKeys requieren seed manual para tenants + +--- + ## [0.5.5] - 2025-12-18 ### Agregado diff --git a/README.md b/README.md index df5ed5d..209272f 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ ![Angular](https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white) ![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) -![EF Core](https://img.shields.io/badge/EF%20Core-512BD4?style=for-the-badge&logo=dotnet&logoColor=white) +![n8n](https://img.shields.io/badge/n8n-EA4B71?style=for-the-badge&logo=n8n&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white) ![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge) -Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant. +Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant con **agentes de IA automatizados**. -> **Estado:** Development Preview v0.5.5 - Business Rules + WMS/TMS Services +> **Estado:** Development Preview v0.5.6 - n8n Integration + Webhooks + Notifications --- @@ -20,7 +20,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in **Parhelion-Logistics** es una plataforma SaaS multi-tenant de nivel Enterprise que unifica las capacidades de un WMS (Warehouse Management System) y un TMS (Transportation Management System). Diseñada para empresas de transporte B2B que requieren gestión integral: inventarios estáticos en almacén, flotas tipificadas (refrigerado, HAZMAT, blindado), redes de distribución Hub & Spoke, trazabilidad por checkpoints y documentación legal mexicana (Carta Porte, POD). -**Objetivo Técnico:** Implementación de **Clean Architecture** y **Domain-Driven Design (DDD)** en un entorno de producción utilizando .NET 8, Angular, React, Docker y PostgreSQL. +**Objetivo Técnico:** Implementación de **Clean Architecture** y **Domain-Driven Design (DDD)** en un entorno de producción utilizando .NET 8, Angular, React, Docker, PostgreSQL y **n8n** para automatización inteligente. --- @@ -29,9 +29,10 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in ### Core - [x] Documentacion de requerimientos y esquema de base de datos +- [x] **[NUEVO]** [Guía de Webhooks y Automatización](./service-webhooks.md) - [x] **Arquitectura Base:** Configuracion de Clean Architecture y estructura de proyecto - [x] **Multi-tenancy:** Query Filters globales por TenantId -- [x] **Domain Layer:** 23 entidades + 15 enumeraciones +- [x] **Domain Layer:** 25 entidades + 17 enumeraciones - [x] **Infrastructure Layer:** EF Core + PostgreSQL + Migrations - [x] **API Skeleton:** 22 endpoints base para todas las entidades - [x] **Autenticacion:** JWT con roles SuperAdmin/Admin/Driver/Warehouse @@ -44,6 +45,16 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in - [x] **Camiones Tipificados:** DryBox, Refrigerado, HAZMAT, Plataforma, Blindado - [x] **Choferes:** Asignacion fija (default_truck) y dinamica (current_truck) - [x] **Bitacora de Flotilla:** Historial de cambios de vehiculo (FleetLog automático) +- [x] **Telemetría GPS:** Campos LastLatitude/LastLongitude en Trucks +- [x] **Búsqueda Geoespacial:** Endpoint `/api/drivers/nearby` (Haversine) + +### Automatización e Inteligencia (n8n) + +- [x] **Webhooks (Backend → n8n):** 5 tipos de eventos (ShipmentException, BookingRequest, HandshakeAttempt, StatusChanged, CheckpointCreated) +- [x] **Notificaciones (n8n → Backend):** Push notifications persistidas para apps móviles +- [x] **ServiceApiKey Multi-Tenant:** Autenticación de agentes IA por tenant con SHA256 +- [x] **Generación Automática:** API Key creada junto con cada nuevo Tenant +- [x] **Agente Crisis Management:** Búsqueda de chofer cercano ante excepciones ### Red Logistica (Hub and Spoke) @@ -82,6 +93,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in | **Backend** | C# / .NET 8 Web API | - | | **Base de Datos** | PostgreSQL 17 | - | | **ORM** | Entity Framework Core (Code First) | - | +| **Automatización** | n8n (Workflow Automation) | Agentes IA | | **Frontend (Admin)** | Angular 18+ (Material Design) | Admin | | **Frontend (Operacion)** | React + Vite + Tailwind CSS (PWA) | Almacenista | | **Frontend (Campo)** | React + Vite + Tailwind CSS (PWA) | Chofer | @@ -157,6 +169,43 @@ graph TD CC -->|LastMile| G ``` +### Integración n8n (Automatización) + +```mermaid +flowchart LR + subgraph Backend["Parhelion API"] + API[Controllers] + WP[WebhookPublisher] + NC[NotificationsController] + end + + subgraph n8n["n8n Workflows"] + WH{{Webhook Trigger}} + AI[/"AI Agent
(Claude/GPT)"/] + HTTP[HTTP Request] + end + + subgraph Mobile["Apps Móviles"] + APP[Driver App] + end + + API -->|"shipment.exception"| WP + WP -->|"POST /webhook"| WH + WH --> AI + AI --> HTTP + HTTP -->|"POST /api/notifications"| NC + HTTP -->|"GET /api/drivers/nearby"| API + NC -.->|"Push Notification"| APP +``` + +**Flujo de Crisis Management:** + +1. Backend detecta `ShipmentStatus.Exception` → publica webhook +2. n8n recibe evento → activa Agente IA +3. Agente consulta `/api/drivers/nearby` con coordenadas del incidente +4. Agente crea notificación para chofer de rescate +5. App móvil recibe push notification + --- ## Base de Datos @@ -251,17 +300,18 @@ src/ | v0.5.2 | 2025-12 | Services Implementation: 16 interfaces, 15 implementaciones | | v0.5.3 | 2025-12 | Integration Tests: 72 tests para Services | | v0.5.4 | 2025-12 | Swagger/OpenAPI, Business Logic Workflow | -| **v0.5.5** | **2025-12** | **WMS/TMS Services, Business Rules, 122 tests** | +| v0.5.5 | 2025-12 | WMS/TMS Services, Business Rules, 122 tests | +| **v0.5.6** | **2025-12** | **n8n Integration, Webhooks, Notifications, ServiceApiKey** | ### Próximas Versiones | Version | Objetivo | Características | | ---------- | --------------------- | ---------------------------------------------- | -| v0.5.6 | Trazabilidad | Endpoints de Checkpoints, Upload de documentos | -| v0.5.7 | QR Handshake | Transferencia de custodia digital | -| v0.5.8 | Rutas | Asignación de rutas, avance por pasos | -| v0.5.9 | Dashboard | KPIs operativos, métricas por status | -| **v0.6.0** | **Perfeccionamiento** | **Bug fixes, E2E testing, optimización** | +| v0.5.7 | Trazabilidad | Endpoints de Checkpoints, Upload de documentos | +| v0.5.8 | QR Handshake | Transferencia de custodia digital | +| v0.5.9 | Rutas | Asignación de rutas, avance por pasos | +| v0.6.0 | Dashboard | KPIs operativos, métricas por status | +| **v0.7.0** | **Perfeccionamiento** | **Bug fixes, E2E testing, optimización** | --- diff --git a/api-architecture.md b/api-architecture.md index 44e422a..57e5684 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -4,8 +4,8 @@ Documentacion tecnica de la estructura API-First del backend Parhelion. ## Estado Actual -**Version:** 0.5.5 -**Enfoque:** WMS/TMS Services + Business Rules +**Version:** 0.5.6 +**Enfoque:** n8n Integration + Secure Webhooks **Arquitectura:** Clean Architecture + Domain-Driven Design --- @@ -60,6 +60,7 @@ Gestion de envios, items y trazabilidad. | `/api/shipment-checkpoints` | ShipmentCheckpoint | Services | ShipmentCheckpointService | | `/api/shipment-documents` | ShipmentDocument | Services | ShipmentDocumentService | | `/api/catalog-items` | CatalogItem | Services | CatalogItemService | +| `/api/notifications` | Notification | Services | NotificationService | ### Network Layer diff --git a/backend/scripts/seed-e2e-fix.sql b/backend/scripts/seed-e2e-fix.sql new file mode 100644 index 0000000..c9324ff --- /dev/null +++ b/backend/scripts/seed-e2e-fix.sql @@ -0,0 +1,59 @@ +-- ================================================================ +-- PARHELION WMS - Test Data for n8n Integration E2E (FIXED) +-- Tenant: "TransporteMX" con Admin y 3 Choferes con ubicaciones GPS +-- ================================================================ + +-- Insertar Drivers con nombres correctos de columnas +INSERT INTO "Drivers" ("Id", "TenantId", "LicenseNumber", "LicenseType", "LicenseExpiration", "Status", "DefaultTruckId", "CurrentTruckId", "UserId", "CreatedAt", "IsDeleted") +VALUES + ('aaaaaaaa-bbbb-cccc-dddd-drvr00000001', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'LIC-MTY-001', 'Federal', '2026-12-31', 2, 'aaaaaaaa-bbbb-cccc-dddd-truck0000001', 'aaaaaaaa-bbbb-cccc-dddd-truck0000001', 'aaaaaaaa-bbbb-cccc-dddd-driver000001', NOW(), false), + ('aaaaaaaa-bbbb-cccc-dddd-drvr00000002', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'LIC-MTY-002', 'Federal', '2027-06-30', 0, 'aaaaaaaa-bbbb-cccc-dddd-truck0000002', 'aaaaaaaa-bbbb-cccc-dddd-truck0000002', 'aaaaaaaa-bbbb-cccc-dddd-driver000002', NOW(), false), + ('aaaaaaaa-bbbb-cccc-dddd-drvr00000003', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'LIC-GDL-003', 'Federal', '2025-08-15', 0, 'aaaaaaaa-bbbb-cccc-dddd-truck0000003', 'aaaaaaaa-bbbb-cccc-dddd-truck0000003', 'aaaaaaaa-bbbb-cccc-dddd-driver000003', NOW(), false) +ON CONFLICT ("Id") DO NOTHING; + +-- Insertar Shipment +INSERT INTO "Shipments" ("Id", "TenantId", "TrackingNumber", "Status", "OriginLocationId", "DestinationLocationId", "TruckId", "DriverId", "TotalWeightKg", "TotalVolumeM3", "DeclaredValue", "ScheduledDeparture", "EstimatedArrival", "IsDelayed", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaabbbb-cccc-dddd-eeee-ffffffffffff', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'TRX-2025-001', + 5, + 'aaaaaaaa-bbbb-cccc-dddd-loc000000001', + 'aaaaaaaa-bbbb-cccc-dddd-loc000000002', + 'aaaaaaaa-bbbb-cccc-dddd-truck0000001', + 'aaaaaaaa-bbbb-cccc-dddd-drvr00000001', + 5000, + 20, + 150000.00, + NOW() - INTERVAL '2 hours', + NOW() + INTERVAL '10 hours', + false, + NOW(), + false +) +ON CONFLICT ("Id") DO NOTHING; + +-- Insertar ShipmentItem con dimensiones correctas +INSERT INTO "ShipmentItems" ("Id", "ShipmentId", "Description", "PackagingType", "Quantity", "WeightKg", "WidthCm", "HeightCm", "LengthCm", "DeclaredValue", "IsFragile", "IsHazardous", "RequiresRefrigeration", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaabbbb-cccc-dddd-eeee-111111111111', + 'aaaabbbb-cccc-dddd-eeee-ffffffffffff', + 'Electrodomésticos', + 0, + 50, + 5000, + 100, + 100, + 200, + 150000.00, + false, + false, + false, + NOW(), + false +) +ON CONFLICT ("Id") DO NOTHING; + +SELECT '=== DATOS ADICIONALES INSERTADOS ===' AS resultado; +SELECT 'Drivers: Juan(OnTrip), María(Available/Cerca), Pedro(Available/Lejos)' AS drivers; +SELECT 'Shipment: TRX-2025-001 (Status=InTransit)' AS shipment; diff --git a/backend/scripts/seed-e2e-test.sql b/backend/scripts/seed-e2e-test.sql new file mode 100644 index 0000000..dc94076 --- /dev/null +++ b/backend/scripts/seed-e2e-test.sql @@ -0,0 +1,307 @@ +-- ================================================================ +-- PARHELION WMS - Test Data for n8n Integration E2E +-- Tenant: "TransporteMX" con Admin y 3 Choferes con ubicaciones GPS +-- ================================================================ + +-- Primero limpiamos datos de prueba anteriores (si existen) +DELETE FROM "Notifications" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "ShipmentItems" WHERE "ShipmentId" IN (SELECT "Id" FROM "Shipments" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'); +DELETE FROM "Shipments" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "FleetLogs" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "Drivers" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "Trucks" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "Employees" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "Locations" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "Users" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "ServiceApiKeys" WHERE "TenantId" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +DELETE FROM "Tenants" WHERE "Id" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; + +-- ================================================================ +-- 1. TENANT +-- ================================================================ +INSERT INTO "Tenants" ("Id", "CompanyName", "ContactEmail", "FleetSize", "DriverCount", "IsActive", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'TransporteMX', + 'admin@transportemx.com', + 5, + 3, + true, + NOW(), + false +); + +-- ================================================================ +-- 2. SERVICE API KEY (para n8n callback - hasheada) +-- Hash of: 'test-n8n-key-transportemx-2025' +-- ================================================================ +INSERT INTO "ServiceApiKeys" ("Id", "TenantId", "KeyHash", "Name", "Description", "IsActive", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-111111111111', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + encode(sha256('test-n8n-key-transportemx-2025'::bytea), 'hex'), + 'n8n-transportemx-test', + 'API Key de prueba para test E2E', + true, + NOW(), + false +); + +-- ================================================================ +-- 3. ROLE (Admin) +-- ================================================================ +-- Usamos el rol Admin que ya debería existir +-- Si no existe, crearlo: +INSERT INTO "Roles" ("Id", "Name", "Description", "CreatedAt", "IsDeleted") +VALUES ('11111111-1111-1111-1111-111111111111', 'Admin', 'Administrator', NOW(), false) +ON CONFLICT ("Id") DO NOTHING; + +INSERT INTO "Roles" ("Id", "Name", "Description", "CreatedAt", "IsDeleted") +VALUES ('22222222-2222-2222-2222-222222222222', 'Driver', 'Chofer', NOW(), false) +ON CONFLICT ("Id") DO NOTHING; + +-- ================================================================ +-- 4. USERS (1 Admin + 3 Drivers) +-- Password: Test1234! (bcrypt hash) +-- ================================================================ +-- Admin +INSERT INTO "Users" ("Id", "TenantId", "Email", "PasswordHash", "FullName", "RoleId", "IsDemoUser", "IsActive", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-admin0000001', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'admin@transportemx.com', + '$2a$11$K.0HwPqDcBH3V5yJ8EQR.eL1K4F5nC3.V5vI4n1tA5m6O3r7R9s0e', -- Test1234! + 'Carlos Admin', + '11111111-1111-1111-1111-111111111111', + false, + true, + NOW(), + false +); + +-- Driver 1: Juan (EN MONTERREY - el que tendrá el problema) +INSERT INTO "Users" ("Id", "TenantId", "Email", "PasswordHash", "FullName", "RoleId", "IsDemoUser", "IsActive", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-driver000001', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'juan@transportemx.com', + '$2a$11$K.0HwPqDcBH3V5yJ8EQR.eL1K4F5nC3.V5vI4n1tA5m6O3r7R9s0e', + 'Juan Pérez (Afectado)', + '22222222-2222-2222-2222-222222222222', + false, + true, + NOW(), + false +); + +-- Driver 2: María (CERCA DE MONTERREY - 10km - rescatista cercana) +INSERT INTO "Users" ("Id", "TenantId", "Email", "PasswordHash", "FullName", "RoleId", "IsDemoUser", "IsActive", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-driver000002', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'maria@transportemx.com', + '$2a$11$K.0HwPqDcBH3V5yJ8EQR.eL1K4F5nC3.V5vI4n1tA5m6O3r7R9s0e', + 'María García (Rescatista Cercana)', + '22222222-2222-2222-2222-222222222222', + false, + true, + NOW(), + false +); + +-- Driver 3: Pedro (LEJOS - Guadalajara - 500km) +INSERT INTO "Users" ("Id", "TenantId", "Email", "PasswordHash", "FullName", "RoleId", "IsDemoUser", "IsActive", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-driver000003', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'pedro@transportemx.com', + '$2a$11$K.0HwPqDcBH3V5yJ8EQR.eL1K4F5nC3.V5vI4n1tA5m6O3r7R9s0e', + 'Pedro Ramírez (Lejano)', + '22222222-2222-2222-2222-222222222222', + false, + true, + NOW(), + false +); + +-- ================================================================ +-- 5. EMPLOYEES +-- ================================================================ +INSERT INTO "Employees" ("Id", "TenantId", "FirstName", "LastName", "Position", "Email", "Phone", "HireDate", "IsActive", "UserId", "CreatedAt", "IsDeleted") +VALUES + ('aaaaaaaa-bbbb-cccc-dddd-empl00000001', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'Juan', 'Pérez', 'Chofer', 'juan@transportemx.com', '+52 81 1234 5678', '2023-01-15', true, 'aaaaaaaa-bbbb-cccc-dddd-driver000001', NOW(), false), + ('aaaaaaaa-bbbb-cccc-dddd-empl00000002', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'María', 'García', 'Chofer', 'maria@transportemx.com', '+52 81 2345 6789', '2023-03-20', true, 'aaaaaaaa-bbbb-cccc-dddd-driver000002', NOW(), false), + ('aaaaaaaa-bbbb-cccc-dddd-empl00000003', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'Pedro', 'Ramírez', 'Chofer', 'pedro@transportemx.com', '+52 33 3456 7890', '2023-06-01', true, 'aaaaaaaa-bbbb-cccc-dddd-driver000003', NOW(), false); + +-- ================================================================ +-- 6. LOCATIONS (Hub en Monterrey, destino en CDMX) +-- ================================================================ +INSERT INTO "Locations" ("Id", "TenantId", "Code", "Name", "Type", "Latitude", "Longitude", "IsActive", "CreatedAt", "IsDeleted") +VALUES + ('aaaaaaaa-bbbb-cccc-dddd-loc000000001', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'MTY-HUB', 'Hub Monterrey', 0, 25.6866, -100.3161, true, NOW(), false), + ('aaaaaaaa-bbbb-cccc-dddd-loc000000002', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'CDMX-DST', 'Destino CDMX', 3, 19.4326, -99.1332, true, NOW(), false); + +-- ================================================================ +-- 7. TRUCKS (3 camiones con ubicaciones GPS) +-- ================================================================ +-- Camión 1: Juan (EN MONTERREY CENTRO - el averiado) +INSERT INTO "Trucks" ("Id", "TenantId", "PlateNumber", "Type", "MaxCapacityKg", "MaxVolumeM3", "Model", "Year", "IsActive", "LastLatitude", "LastLongitude", "LastLocationUpdate", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-truck0000001', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'MTY-001-AAA', + 0, -- DryBox + 10000, + 40, + 'Kenworth T680', + 2022, + true, + 25.6750, -- Lat: Centro Monterrey (punto de avería) + -100.3100, -- Lon + NOW(), + NOW(), + false +); + +-- Camión 2: María (10km al norte de Monterrey - CERCANA y DISPONIBLE) +INSERT INTO "Trucks" ("Id", "TenantId", "PlateNumber", "Type", "MaxCapacityKg", "MaxVolumeM3", "Model", "Year", "IsActive", "LastLatitude", "LastLongitude", "LastLocationUpdate", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-truck0000002', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'MTY-002-BBB', + 0, -- DryBox + 10000, + 40, + 'Freightliner Cascadia', + 2023, + true, + 25.7700, -- Lat: ~10km norte de MTY + -100.3000, -- Lon + NOW(), + NOW(), + false +); + +-- Camión 3: Pedro (Guadalajara - LEJANO) +INSERT INTO "Trucks" ("Id", "TenantId", "PlateNumber", "Type", "MaxCapacityKg", "MaxVolumeM3", "Model", "Year", "IsActive", "LastLatitude", "LastLongitude", "LastLocationUpdate", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-truck0000003', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'GDL-003-CCC', + 0, -- DryBox + 10000, + 40, + 'Volvo VNL 860', + 2021, + true, + 20.6597, -- Lat: Guadalajara + -103.3496, -- Lon + NOW(), + NOW(), + false +); + +-- ================================================================ +-- 8. DRIVERS (con status y asignaciones) +-- ================================================================ +-- Juan: Status=OnTrip (está trabajando, tendrá la avería) +INSERT INTO "Drivers" ("Id", "TenantId", "LicenseNumber", "LicenseExpiry", "Status", "DefaultTruckId", "CurrentTruckId", "UserId", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-drvr00000001', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'LIC-MTY-001', + '2026-12-31', + 2, -- OnTrip + 'aaaaaaaa-bbbb-cccc-dddd-truck0000001', + 'aaaaaaaa-bbbb-cccc-dddd-truck0000001', + 'aaaaaaaa-bbbb-cccc-dddd-driver000001', + NOW(), + false +); + +-- María: Status=Available (DISPONIBLE para rescate) +INSERT INTO "Drivers" ("Id", "TenantId", "LicenseNumber", "LicenseExpiry", "Status", "DefaultTruckId", "CurrentTruckId", "UserId", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-drvr00000002', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'LIC-MTY-002', + '2027-06-30', + 0, -- Available ← Esta es la que debe encontrar n8n + 'aaaaaaaa-bbbb-cccc-dddd-truck0000002', + 'aaaaaaaa-bbbb-cccc-dddd-truck0000002', + 'aaaaaaaa-bbbb-cccc-dddd-driver000002', + NOW(), + false +); + +-- Pedro: Status=Available pero lejano +INSERT INTO "Drivers" ("Id", "TenantId", "LicenseNumber", "LicenseExpiry", "Status", "DefaultTruckId", "CurrentTruckId", "UserId", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-drvr00000003', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'LIC-GDL-003', + '2025-08-15', + 0, -- Available pero lejos + 'aaaaaaaa-bbbb-cccc-dddd-truck0000003', + 'aaaaaaaa-bbbb-cccc-dddd-truck0000003', + 'aaaaaaaa-bbbb-cccc-dddd-driver000003', + NOW(), + false +); + +-- ================================================================ +-- 9. SHIPMENT (El envío que tendrá la avería) +-- Status: InTransit (para que pueda cambiar a Exception) +-- ================================================================ +INSERT INTO "Shipments" ("Id", "TenantId", "TrackingNumber", "Status", "OriginLocationId", "DestinationLocationId", "TruckId", "DriverId", "TotalWeightKg", "TotalVolumeM3", "DeclaredValue", "ScheduledDeparture", "EstimatedArrival", "IsDelayed", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-ship00000001', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'TRX-2025-001', + 5, -- InTransit (puede cambiar a Exception) + 'aaaaaaaa-bbbb-cccc-dddd-loc000000001', -- MTY Hub + 'aaaaaaaa-bbbb-cccc-dddd-loc000000002', -- CDMX Destino + 'aaaaaaaa-bbbb-cccc-dddd-truck0000001', -- Camión de Juan + 'aaaaaaaa-bbbb-cccc-dddd-drvr00000001', -- Juan (el afectado) + 5000, + 20, + 150000.00, + NOW() - INTERVAL '2 hours', + NOW() + INTERVAL '10 hours', + false, + NOW(), + false +); + +-- Item del shipment +INSERT INTO "ShipmentItems" ("Id", "ShipmentId", "Description", "Quantity", "WeightKg", "VolumeM3", "DeclaredValue", "RequiresRefrigeration", "IsHazardous", "CreatedAt", "IsDeleted") +VALUES ( + 'aaaaaaaa-bbbb-cccc-dddd-item00000001', + 'aaaaaaaa-bbbb-cccc-dddd-ship00000001', + 'Electrodomésticos', + 50, + 5000, + 20, + 150000.00, + false, + false, + NOW(), + false +); + +-- ================================================================ +-- RESULTADOS ESPERADOS: +-- ================================================================ +-- Cuando el Shipment TRX-2025-001 cambie a Exception: +-- 1. Se publica webhook con coordenadas (25.6750, -100.3100) +-- 2. n8n busca en GET /api/drivers/nearby?lat=25.6750&lon=-100.31&radiusKm=50 +-- 3. Debe encontrar a María (~10km) como la más cercana Y disponible +-- 4. n8n envía 2 notificaciones: +-- - A Juan (afectado): "Tu envío ha sido marcado como excepción" +-- - A María (rescatista): "Se te asignó apoyo para TRX-2025-001" +-- ================================================================ + +SELECT 'Datos de prueba insertados correctamente' AS resultado; +SELECT 'Tenant: TransporteMX (aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee)' AS info; +SELECT 'Shipment: TRX-2025-001 (Status: InTransit → cambiar a Exception para trigger)' AS test; +SELECT 'API Key de prueba: test-n8n-key-transportemx-2025' AS n8n_key; diff --git a/backend/src/Parhelion.API/Controllers/DriversController.cs b/backend/src/Parhelion.API/Controllers/DriversController.cs index eb9be1f..df87a12 100644 --- a/backend/src/Parhelion.API/Controllers/DriversController.cs +++ b/backend/src/Parhelion.API/Controllers/DriversController.cs @@ -4,6 +4,7 @@ using Parhelion.Application.DTOs.Fleet; using Parhelion.Application.Interfaces.Services; using Parhelion.Domain.Enums; +using Parhelion.API.Filters; namespace Parhelion.API.Controllers; @@ -63,6 +64,47 @@ public async Task ByStatus(string status, [FromQuery] PagedReques return Ok(result); } + /// + /// Busca choferes disponibles cercanos a una ubicación. + /// Autenticación: JWT (Usuario) o Bearer {CallbackToken} / X-Service-Key (n8n). + /// + /// Latitud central. + /// Longitud central. + /// Radio en kilómetros (default 50). + /// Número de página. + /// Resultados por página. + [HttpGet("nearby")] + [ServiceApiKey] // Permite acceso con X-Service-Key para n8n + [AllowAnonymous] // Bypass [Authorize] de la clase - ServiceApiKey filter valida + [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task GetNearby( + [FromQuery] decimal lat, + [FromQuery] decimal lon, + [FromQuery] double radius = 50, + [FromQuery] int pageNumber = 1, + [FromQuery] int pageSize = 10) + { + // 1. Intentar obtener Tenant del User Claim (JWT) + var tenantId = GetTenantId(); + + // 2. Si no hay JWT, obtener del ServiceApiKey (resuelto por el filtro) + if (tenantId == null && HttpContext.Items.TryGetValue(ServiceApiKeyAttribute.TenantIdKey, out var serviceTenantId)) + { + tenantId = serviceTenantId as Guid?; + } + + // 3. Si aún no hay tenant, rechazar + if (tenantId == null) + { + return Unauthorized(new { error = "No se pudo determinar el tenant" }); + } + + var request = new PagedRequest { Page = pageNumber, PageSize = pageSize }; + var result = await _driverService.GetNearbyDriversAsync(lat, lon, radius, tenantId.Value, request); + return Ok(result); + } + [HttpPost] public async Task Create([FromBody] CreateDriverRequest request) { diff --git a/backend/src/Parhelion.API/Controllers/NotificationsController.cs b/backend/src/Parhelion.API/Controllers/NotificationsController.cs new file mode 100644 index 0000000..9c65023 --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/NotificationsController.cs @@ -0,0 +1,209 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Parhelion.API.Filters; +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Notification; +using Parhelion.Application.Interfaces.Services; +using System.Security.Claims; + +namespace Parhelion.API.Controllers; + +/// +/// Controller para gestión de notificaciones. +/// - POST: Autenticación con X-Service-Key (n8n/servicios) +/// - GET/PATCH: Autenticación con JWT (usuarios) +/// +[ApiController] +[Route("api/[controller]")] +[Produces("application/json")] +public class NotificationsController : ControllerBase +{ + private readonly INotificationService _service; + + public NotificationsController(INotificationService service) + { + _service = service; + } + + // ========== PARA N8N Y SERVICIOS INTERNOS (API Key o CallbackToken) ========== + + /// + /// Crea una nueva notificación. + /// Autenticación: Authorization: Bearer {CallbackToken} o X-Service-Key + /// + /// El TenantId se obtiene automáticamente del token JWT, NO del body. + /// Esto simplifica la integración de n8n. + /// + [ServiceApiKey] + [HttpPost] + [ProducesResponseType(typeof(OperationResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Create( + [FromBody] CreateNotificationFromServiceRequest request, + CancellationToken cancellationToken) + { + // Obtener TenantId del CallbackToken/ServiceApiKey (ya validado por el atributo) + if (!HttpContext.Items.TryGetValue(ServiceApiKeyAttribute.TenantIdKey, out var tenantIdObj) + || tenantIdObj is not Guid tenantId) + { + return Unauthorized(new { error = "TenantId not found in authentication token" }); + } + + // Crear el request completo con TenantId del token + var fullRequest = new CreateNotificationRequest( + TenantId: tenantId, + UserId: request.UserId, + RoleId: request.RoleId, + Type: request.Type, + Source: request.Source, + Title: request.Title, + Message: request.Message, + MetadataJson: request.MetadataJson, + RelatedEntityType: request.RelatedEntityType, + RelatedEntityId: request.RelatedEntityId, + Priority: request.Priority, + RequiresAction: request.RequiresAction + ); + + var result = await _service.CreateAsync(fullRequest, cancellationToken); + + if (!result.Success) + { + return BadRequest(result); + } + + return Ok(result); + } + + // ========== PARA APPS MÓVILES (JWT) ========== + + /// + /// Obtiene notificaciones del usuario autenticado. + /// + [Authorize] + [HttpGet] + [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] + public async Task GetMyNotifications( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken) + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return Unauthorized(); + } + + var result = await _service.GetByUserAsync(userId.Value, request, cancellationToken); + return Ok(result); + } + + /// + /// Obtiene el conteo de notificaciones no leídas. + /// + [Authorize] + [HttpGet("unread-count")] + [ProducesResponseType(typeof(UnreadCountResponse), StatusCodes.Status200OK)] + public async Task GetUnreadCount(CancellationToken cancellationToken) + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return Unauthorized(); + } + + var count = await _service.GetUnreadCountAsync(userId.Value, cancellationToken); + return Ok(new UnreadCountResponse(count)); + } + + /// + /// Obtiene una notificación por ID. + /// + [Authorize] + [HttpGet("{id:guid}")] + [ProducesResponseType(typeof(NotificationResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(Guid id, CancellationToken cancellationToken) + { + var result = await _service.GetByIdAsync(id, cancellationToken); + + if (result == null) + { + return NotFound(); + } + + // Validar que pertenece al usuario + var userId = GetCurrentUserId(); + if (result.UserId != userId) + { + return Forbid(); + } + + return Ok(result); + } + + /// + /// Marca una notificación como leída. + /// + [Authorize] + [HttpPatch("{id:guid}/read")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task MarkAsRead(Guid id, CancellationToken cancellationToken) + { + // Validar que existe y pertenece al usuario + var notification = await _service.GetByIdAsync(id, cancellationToken); + if (notification == null) + { + return NotFound(); + } + + var userId = GetCurrentUserId(); + if (notification.UserId != userId) + { + return Forbid(); + } + + var result = await _service.MarkAsReadAsync(id, cancellationToken); + + if (!result.Success) + { + return NotFound(result); + } + + return Ok(); + } + + /// + /// Marca todas las notificaciones del usuario como leídas. + /// + [Authorize] + [HttpPost("mark-all-read")] + [ProducesResponseType(typeof(OperationResult), StatusCodes.Status200OK)] + public async Task MarkAllAsRead(CancellationToken cancellationToken) + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return Unauthorized(); + } + + var result = await _service.MarkAllAsReadAsync(userId.Value, cancellationToken); + return Ok(result); + } + + // ========== HELPERS ========== + + private Guid? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) + ?? User.FindFirstValue("sub"); + + if (Guid.TryParse(userIdClaim, out var userId)) + { + return userId; + } + + return null; + } +} diff --git a/backend/src/Parhelion.API/Controllers/ShipmentsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentsController.cs index 82e1278..49342f1 100644 --- a/backend/src/Parhelion.API/Controllers/ShipmentsController.cs +++ b/backend/src/Parhelion.API/Controllers/ShipmentsController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Parhelion.API.Filters; using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Shipment; using Parhelion.Application.Interfaces.Services; @@ -192,8 +193,11 @@ public async Task> AssignToDriver( /// /// Actualiza el estatus de un envío. + /// Soporta autenticación JWT o X-Service-Key/Bearer CallbackToken /// [HttpPatch("{id:guid}/status")] + [ServiceApiKey] + [AllowAnonymous] // Bypass [Authorize] de clase - ServiceApiKey valida public async Task> UpdateStatus( Guid id, [FromQuery] string status, diff --git a/backend/src/Parhelion.API/Controllers/TrucksController.cs b/backend/src/Parhelion.API/Controllers/TrucksController.cs index b74596f..8fb5fe8 100644 --- a/backend/src/Parhelion.API/Controllers/TrucksController.cs +++ b/backend/src/Parhelion.API/Controllers/TrucksController.cs @@ -107,9 +107,19 @@ public async Task SetStatus(Guid id, [FromBody] bool isActive) return Ok(result.Data); } + [HttpPost("{id:guid}/location")] + public async Task UpdateLocation(Guid id, [FromBody] UpdateTruckLocationRequest request) + { + var result = await _truckService.UpdateLocationAsync(id, request.Latitude, request.Longitude); + if (!result.Success) return NotFound(new { error = result.Message }); + return Ok(); + } + private Guid? GetTenantId() { var claim = User.FindFirst("tenant_id"); return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null; } } + +public record UpdateTruckLocationRequest(decimal Latitude, decimal Longitude); diff --git a/backend/src/Parhelion.API/Data/CrisisSeeder.cs b/backend/src/Parhelion.API/Data/CrisisSeeder.cs new file mode 100644 index 0000000..4621ee1 --- /dev/null +++ b/backend/src/Parhelion.API/Data/CrisisSeeder.cs @@ -0,0 +1,166 @@ +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; +using Parhelion.Application.Auth; +using Parhelion.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Parhelion.API.Data; + +/// +/// Seeder específico para el flujo de Crisis Management. +/// Crea los 3 escenarios de prueba: Victima, Rescate, Lejano. +/// +public static class CrisisSeeder +{ + public static async Task SeedAsync(IServiceProvider services) + { + using var scope = services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var passwordHasher = scope.ServiceProvider.GetRequiredService(); + + // 1. Obtener Tenant principal (asumimos que DataSeeder ya corrió) + var tenant = await context.Tenants.FirstOrDefaultAsync(); + if (tenant == null) + { + Console.WriteLine("⚠️ CRISIS SEED: No tenant found. Skipping."); + return; + } + + // 2. Verificar si ya existen los datos para no duplicar + if (await context.Trucks.AnyAsync(t => t.Plate == "VICTIM-01")) + { + Console.WriteLine("ℹ️ CRISIS SEED: Data already exists. Skipping."); + return; + } + + Console.WriteLine("🚑 CRISIS SEED: Injecting test scenario data..."); + + // 3. Obtener Rol de Driver (asumimos ID fijo o buscamos por nombre) + var driverRole = await context.Roles.FirstOrDefaultAsync(r => r.Name == "Driver"); + if (driverRole == null) + { + // Si no existe, usamos cualquier rol o lanzamos error. Por ahora creamos uno dummy en memoria si falla. + // Pero en producción/dev ya debería existir por DataSeeder o migraciones anteriores. + // Buscaremos el ID fijo de la documentación si falla el nombre + driverRole = await context.Roles.FindAsync(Guid.Parse("22222222-2222-2222-2222-222222222222")); + if (driverRole == null) throw new Exception("Driver role not found for seeding"); + } + + var defaultPass = passwordHasher.HashPassword("Test1234!"); + + // --- SCENARIO 1: VICTIM (El que se rompe) --- + // Coordenadas: 20.588056, -100.388056 + CreateDriverStack(context, tenant.Id, driverRole.Id, defaultPass, + firstName: "Victim", + lastName: "Driver", + email: "victim@parhelion.com", + plate: "VICTIM-01", + lat: 20.588056m, lon: -100.388056m, + status: DriverStatus.OnRoute, // Está ocupado/en ruta antes de fallar + truckType: TruckType.DryBox); + + // --- SCENARIO 2: RESCUE (El salvador cercano) --- + // Coordenadas: 20.612000, -100.410000 (~3-4 km cerca) + CreateDriverStack(context, tenant.Id, driverRole.Id, defaultPass, + firstName: "Rescue", + lastName: "Driver", + email: "rescue@parhelion.com", + plate: "RESCUE-01", + lat: 20.612000m, lon: -100.410000m, + status: DriverStatus.Available, // Debe estar disponible + truckType: TruckType.DryBox); + + // --- SCENARIO 3: FAR (El que está en CDMX, lejos) --- + // Coordenadas: 19.432608, -99.133209 (~200 km lejos) + CreateDriverStack(context, tenant.Id, driverRole.Id, defaultPass, + firstName: "Far", + lastName: "Driver", + email: "far@parhelion.com", + plate: "FAR-01", + lat: 19.432608m, lon: -99.133209m, + status: DriverStatus.Available, // Disponible pero lejos + truckType: TruckType.DryBox); + + await context.SaveChangesAsync(); + Console.WriteLine("✅ CRISIS SEED: Scenario data injected successfully."); + } + + private static void CreateDriverStack( + ParhelionDbContext ctx, + Guid tenantId, + Guid roleId, + string passwordHash, + string firstName, + string lastName, + string email, + string plate, + decimal lat, + decimal lon, + DriverStatus status, + TruckType truckType) + { + // 1. User + var user = new User + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Email = email, + PasswordHash = passwordHash, + FullName = $"{firstName} {lastName}", + RoleId = roleId, + IsActive = true, + CreatedAt = DateTime.UtcNow, + UsesArgon2 = true // Asumimos default moderno + }; + ctx.Users.Add(user); + + // 2. Employee + var emp = new Employee + { + Id = Guid.NewGuid(), + TenantId = tenantId, + UserId = user.Id, + User = user, + Phone = "555-000-0000", + Department = "Fleet", + HireDate = DateTime.UtcNow, + CreatedAt = DateTime.UtcNow, + IsDeleted = false + }; + ctx.Employees.Add(emp); + + // 3. Truck + var truck = new Truck + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Plate = plate, + Model = "Generic Test Truck", + Type = truckType, + MaxCapacityKg = 15000, + MaxVolumeM3 = 60, + IsActive = true, + CreatedAt = DateTime.UtcNow, + LastLatitude = lat, + LastLongitude = lon, + LastLocationUpdate = DateTime.UtcNow + }; + ctx.Trucks.Add(truck); + + // 4. Driver + var driver = new Driver + { + Id = Guid.NewGuid(), + EmployeeId = emp.Id, + Employee = emp, + LicenseNumber = $"LIC-{plate}", + Status = status, + CurrentTruckId = truck.Id, + CurrentTruck = truck, + // TenantId eliminado, no existe en Driver + CreatedAt = DateTime.UtcNow + }; + ctx.Drivers.Add(driver); + } +} diff --git a/backend/src/Parhelion.API/Filters/ServiceApiKeyAttribute.cs b/backend/src/Parhelion.API/Filters/ServiceApiKeyAttribute.cs new file mode 100644 index 0000000..c77ad08 --- /dev/null +++ b/backend/src/Parhelion.API/Filters/ServiceApiKeyAttribute.cs @@ -0,0 +1,140 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.EntityFrameworkCore; +using Parhelion.Application.Interfaces; +using Parhelion.Infrastructure.Data; + +namespace Parhelion.API.Filters; + +/// +/// Filtro de autenticación para servicios externos (n8n, microservicios). +/// +/// Soporta 2 métodos de autenticación: +/// 1. X-Service-Key: API Key persistente (lookup en BD) +/// 2. Authorization: Bearer {CallbackToken}: JWT de corta duración (validación criptográfica) +/// +/// El TenantId se almacena en HttpContext.Items["ServiceTenantId"] +/// +/// Uso: [ServiceApiKey] en métodos o controladores. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public class ServiceApiKeyAttribute : Attribute, IAsyncActionFilter +{ + private const string ServiceKeyHeader = "X-Service-Key"; + private const string AuthorizationHeader = "Authorization"; + private const string BearerPrefix = "Bearer "; + public const string TenantIdKey = "ServiceTenantId"; + public const string CorrelationIdKey = "ServiceCorrelationId"; + + public async Task OnActionExecutionAsync( + ActionExecutingContext context, + ActionExecutionDelegate next) + { + // ========== OPCIÓN 1: Callback Token (Bearer JWT) ========== + if (context.HttpContext.Request.Headers.TryGetValue(AuthorizationHeader, out var authHeader) + && authHeader.ToString().StartsWith(BearerPrefix, StringComparison.OrdinalIgnoreCase)) + { + var token = authHeader.ToString()[BearerPrefix.Length..]; + + var tokenService = context.HttpContext.RequestServices + .GetService(); + + if (tokenService == null) + { + context.Result = new StatusCodeResult(500); + return; + } + + var claims = tokenService.ValidateCallbackToken(token); + if (claims != null) + { + // Token válido - establecer claims y continuar + context.HttpContext.Items[TenantIdKey] = claims.TenantId; + context.HttpContext.Items[CorrelationIdKey] = claims.CorrelationId; + await next(); + return; + } + + // Token inválido o expirado + context.Result = new UnauthorizedObjectResult(new { + error = "Invalid or expired callback token" + }); + return; + } + + // ========== OPCIÓN 2: X-Service-Key (API Key persistente) ========== + if (context.HttpContext.Request.Headers.TryGetValue(ServiceKeyHeader, out var providedKey) + && !string.IsNullOrWhiteSpace(providedKey)) + { + var keyHash = ComputeSha256Hash(providedKey.ToString()); + + var dbContext = context.HttpContext.RequestServices + .GetRequiredService(); + + var apiKey = await dbContext.ServiceApiKeys + .AsNoTracking() + .FirstOrDefaultAsync(k => + k.KeyHash == keyHash && + k.IsActive && + !k.IsDeleted); + + if (apiKey == null) + { + context.Result = new UnauthorizedObjectResult(new { + error = "Invalid or inactive service key" + }); + return; + } + + // Validar expiración + if (apiKey.ExpiresAt.HasValue && apiKey.ExpiresAt.Value < DateTime.UtcNow) + { + context.Result = new UnauthorizedObjectResult(new { + error = "Service key has expired" + }); + return; + } + + // Almacenar TenantId + context.HttpContext.Items[TenantIdKey] = apiKey.TenantId; + + // Actualizar LastUsedAt de forma fire-and-forget + _ = UpdateLastUsedAsync(context, apiKey.Id); + + await next(); + return; + } + + // ========== SIN CREDENCIALES ========== + context.Result = new UnauthorizedObjectResult(new { + error = $"Missing authentication. Provide {ServiceKeyHeader} header or Authorization: Bearer " + }); + } + + private static async Task UpdateLastUsedAsync(ActionExecutingContext context, Guid apiKeyId) + { + try + { + using var scope = context.HttpContext.RequestServices + .GetRequiredService() + .CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var key = await ctx.ServiceApiKeys.FindAsync(apiKeyId); + if (key != null) + { + key.LastUsedAt = DateTime.UtcNow; + key.LastUsedFromIp = context.HttpContext.Connection.RemoteIpAddress?.ToString(); + await ctx.SaveChangesAsync(); + } + } + catch { /* Fire and forget */ } + } + + private static string ComputeSha256Hash(string rawData) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(rawData)); + return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); + } +} diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index 13ab06d..51a30ec 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -10,6 +10,7 @@ using Parhelion.Infrastructure.Data; using Parhelion.Infrastructure.Data.Interceptors; using Parhelion.Infrastructure.Services; +using Parhelion.Infrastructure.External.Webhooks; var builder = WebApplication.CreateBuilder(args); @@ -77,9 +78,14 @@ builder.Services.AddScoped(); // ========== DATABASE ========== -// Usar connection string de variables de entorno o appsettings -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") - ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +// Connection string desde variable de entorno o appsettings +var dbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "postgres"; +var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? ""; +var dbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost"; +var dbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "parhelion_dev"; +var connectionString = !string.IsNullOrEmpty(builder.Configuration.GetConnectionString("DefaultConnection")) + ? builder.Configuration.GetConnectionString("DefaultConnection")! + : $"Host={dbHost};Port=5432;Database={dbName};Username={dbUser};Password={dbPassword}"; builder.Services.AddDbContext((sp, options) => { @@ -152,13 +158,54 @@ builder.Services.AddScoped(); +// ========== NOTIFICATION SERVICES ========== +builder.Services.AddScoped(); + // ========== VALIDATORS ========== builder.Services.AddSingleton(); +// ========== WEBHOOK/N8N INTEGRATION ========== +// Credenciales se leen de variables de entorno por seguridad +builder.Services.Configure(options => +{ + builder.Configuration.GetSection("N8n").Bind(options); + // Override desde variables de entorno si existen + var envBaseUrl = Environment.GetEnvironmentVariable("N8N_BASE_URL"); + if (!string.IsNullOrEmpty(envBaseUrl)) + { + options.BaseUrl = envBaseUrl; + } + var envApiKey = Environment.GetEnvironmentVariable("N8N_WEBHOOK_SECRET"); + if (!string.IsNullOrEmpty(envApiKey)) + { + options.ApiKey = envApiKey; + } +}); + +// CallbackTokenService (necesario tanto para publisher como para ServiceApiKeyAttribute) +builder.Services.AddSingleton(); + +var n8nEnabled = builder.Configuration.GetValue("N8n:Enabled"); +if (n8nEnabled) +{ + // Si n8n está habilitado, usar el publisher real con HttpClient + builder.Services.AddHttpClient(); +} +else +{ + // Si está deshabilitado, usar NullPublisher (no hace nada) + builder.Services.AddSingleton(); +} + // ========== JWT AUTHENTICATION ========== -var jwtSecretKey = builder.Configuration["Jwt:SecretKey"] - ?? "ParhelionLogisticsDefaultSecretKey2024!"; +// JWT Secret desde variable de entorno +var jwtSecretKey = Environment.GetEnvironmentVariable("JWT_SECRET") + ?? builder.Configuration["Jwt:SecretKey"] + ?? throw new InvalidOperationException("JWT_SECRET environment variable or Jwt:SecretKey config is required"); builder.Services.AddAuthentication(options => { @@ -201,10 +248,14 @@ { var db = scope.ServiceProvider.GetRequiredService(); - // Auto-migrate en desarrollo - if (app.Environment.IsDevelopment()) + // Auto-migrate - seguro en dev/staging + // En producción real, usar: dotnet ef database update + var pendingMigrations = await db.Database.GetPendingMigrationsAsync(); + if (pendingMigrations.Any()) { + Console.WriteLine($"Applying {pendingMigrations.Count()} pending migration(s)..."); await db.Database.MigrateAsync(); + Console.WriteLine("Migrations applied successfully."); } // Seed data siempre (es idempotente) @@ -267,4 +318,7 @@ // Seed initial data (SuperUser, DefaultTenant) if database is empty await Parhelion.API.Data.DataSeeder.SeedAsync(app.Services); +// Seed Crisis Management scenarios (for dev/testing) +// await Parhelion.API.Data.CrisisSeeder.SeedAsync(app.Services); + app.Run(); diff --git a/backend/src/Parhelion.API/appsettings.json b/backend/src/Parhelion.API/appsettings.json index e926034..72d6810 100644 --- a/backend/src/Parhelion.API/appsettings.json +++ b/backend/src/Parhelion.API/appsettings.json @@ -8,13 +8,28 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=parhelion_dev;Username=MetaCodeX;Password=H4NZC0D3X1521" + "DefaultConnection": "" }, "Jwt": { - "SecretKey": "ParhelionLogistics2024SecureJwtKeyMinimum32Characters!", + "SecretKey": "", "Issuer": "Parhelion", "Audience": "ParhelionClient", "AccessTokenExpirationMinutes": 120, "RefreshTokenExpirationDays": 7 + }, + "N8n": { + "Enabled": true, + "BaseUrl": "", + "ApiKey": "", + "TimeoutSeconds": 10, + "Webhooks": { + "shipment.created": "/webhook/shipment-created", + "shipment.exception": "/webhook/shipment-exception", + "shipment.status_changed": "/webhook/shipment-status-changed", + "shipment.assigned": "/webhook/shipment-assigned", + "booking.requested": "/webhook/booking-requested", + "handshake.attempted": "/webhook/handshake-attempted", + "checkpoint.created": "/webhook/checkpoint-created" + } } } diff --git a/backend/src/Parhelion.Application/DTOs/Core/CoreDtos.cs b/backend/src/Parhelion.Application/DTOs/Core/CoreDtos.cs index 49718de..0c63b27 100644 --- a/backend/src/Parhelion.Application/DTOs/Core/CoreDtos.cs +++ b/backend/src/Parhelion.Application/DTOs/Core/CoreDtos.cs @@ -25,7 +25,11 @@ public record TenantResponse( int DriverCount, bool IsActive, DateTime CreatedAt, - DateTime? UpdatedAt + DateTime? UpdatedAt, + /// + /// API Key generada automáticamente. Solo se muestra al momento de creación. + /// + string? GeneratedApiKey = null ); // ========== USER DTOs ========== diff --git a/backend/src/Parhelion.Application/DTOs/Notification/NotificationDTOs.cs b/backend/src/Parhelion.Application/DTOs/Notification/NotificationDTOs.cs new file mode 100644 index 0000000..88dd607 --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Notification/NotificationDTOs.cs @@ -0,0 +1,70 @@ +namespace Parhelion.Application.DTOs.Notification; + +/// +/// Response DTO para notificaciones. +/// +public record NotificationResponse( + Guid Id, + Guid TenantId, + Guid? UserId, + Guid? RoleId, + string Type, + string Source, + string Title, + string Message, + string? MetadataJson, + string? RelatedEntityType, + Guid? RelatedEntityId, + bool IsRead, + DateTime? ReadAt, + int Priority, + bool RequiresAction, + bool ActionCompleted, + DateTime CreatedAt +); + +/// +/// Request para crear notificación (usado por n8n y backend). +/// +public record CreateNotificationRequest( + Guid TenantId, + Guid? UserId, + Guid? RoleId, + string Type, + string Source, + string Title, + string Message, + string? MetadataJson = null, + string? RelatedEntityType = null, + Guid? RelatedEntityId = null, + int Priority = 3, + bool RequiresAction = false +); + +/// +/// Request simplificado para n8n y servicios externos. +/// El TenantId se obtiene del CallbackToken JWT, no del body. +/// +public record CreateNotificationFromServiceRequest( + Guid? UserId, + Guid? RoleId, + string Type, + string Source, + string Title, + string Message, + string? MetadataJson = null, + string? RelatedEntityType = null, + Guid? RelatedEntityId = null, + int Priority = 3, + bool RequiresAction = false +); + +/// +/// Respuesta para contador de no leídas. +/// +public record UnreadCountResponse(int Count); + +/// +/// Request para marcar notificaciones como leídas. +/// +public record MarkAsReadRequest(Guid NotificationId); diff --git a/backend/src/Parhelion.Application/DTOs/Webhooks/WebhookEvent.cs b/backend/src/Parhelion.Application/DTOs/Webhooks/WebhookEvent.cs new file mode 100644 index 0000000..a677cc4 --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Webhooks/WebhookEvent.cs @@ -0,0 +1,53 @@ +namespace Parhelion.Application.DTOs.Webhooks; + +/// +/// Envelope base para todos los eventos de webhook. +/// Proporciona metadatos estándar para tracking y debugging. +/// +public record WebhookEvent( + /// Tipo de evento (ej: "shipment.exception", "handshake.attempted") + string EventType, + /// Timestamp UTC del evento + DateTime Timestamp, + /// ID único para correlación de logs + Guid CorrelationId, + /// + /// JWT firmado que n8n usa para autenticarse de vuelta al API. + /// Contiene TenantId y expira en 15 minutos. + /// Usar como: Authorization: Bearer {CallbackToken} + /// + string CallbackToken, + /// Payload específico del evento + object Payload +); + +/// +/// Tipos de evento definidos para el sistema. +/// Usar estas constantes para consistencia. +/// +public static class WebhookEventTypes +{ + // ========== SHIPMENT EVENTS ========== + public const string ShipmentCreated = "shipment.created"; + public const string ShipmentException = "shipment.exception"; + public const string ShipmentStatusChanged = "shipment.status_changed"; + public const string ShipmentAssigned = "shipment.assigned"; + + // ========== BOOKING/VALIDATION EVENTS ========== + public const string BookingRequested = "booking.requested"; + public const string BookingValidated = "booking.validated"; + public const string BookingRejected = "booking.rejected"; + + // ========== QR HANDSHAKE EVENTS (Futuro) ========== + public const string HandshakeAttempted = "handshake.attempted"; + public const string HandshakeValidated = "handshake.validated"; + public const string HandshakeFailed = "handshake.failed"; + + // ========== CHECKPOINT EVENTS (Futuro) ========== + public const string CheckpointCreated = "checkpoint.created"; + public const string CheckpointAnomalyDetected = "checkpoint.anomaly"; + + // ========== DOCUMENT EVENTS (Futuro) ========== + public const string DocumentGenerated = "document.generated"; + public const string DocumentSigned = "document.signed"; +} diff --git a/backend/src/Parhelion.Application/DTOs/Webhooks/WebhookEvents.cs b/backend/src/Parhelion.Application/DTOs/Webhooks/WebhookEvents.cs new file mode 100644 index 0000000..cfb6a4f --- /dev/null +++ b/backend/src/Parhelion.Application/DTOs/Webhooks/WebhookEvents.cs @@ -0,0 +1,149 @@ +namespace Parhelion.Application.DTOs.Webhooks; + +/// +/// Evento: Un envío cambió a estado Exception. +/// Usado por: Agente de Crisis Management (n8n). +/// Propósito: Buscar rutas alternativas, reasignar vehículo, notificar gestor. +/// +public record ShipmentExceptionEvent( + Guid ShipmentId, + string TrackingNumber, + Guid TenantId, + + // Ubicación actual + Guid? CurrentLocationId, + string? CurrentLocationCode, + // Coordenadas del incidente (Crisis Management) + decimal? Latitude, + decimal? Longitude, + + // Destino + Guid DestinationLocationId, + string? DestinationLocationCode, + + // Tipo de carga (para priorización de IA) + string CargoType, // "Perishable", "Hazmat", "HighValue", "Standard" + + // ETAs + DateTime? OriginalETA, + DateTime? ScheduledDeparture, + + // Valores + decimal? TotalDeclaredValue, + decimal TotalWeightKg, + decimal TotalVolumeM3, + + // Asignación actual + Guid? DriverId, + Guid? TruckId, + string? TruckType, + + // Contexto adicional + bool IsDelayed, + string? ExceptionReason +); + +/// +/// Evento: Nuevo envío creado, requiere validación de compatibilidad. +/// Usado por: Agente de Smart Booking (n8n). +/// Propósito: Validar que el tipo de carga sea compatible con el camión asignado. +/// +public record BookingRequestEvent( + Guid ShipmentId, + string TrackingNumber, + Guid TenantId, + + // Capacidades requeridas + decimal TotalWeightKg, + decimal TotalVolumeM3, + + // Flags de carga especial + bool HasRefrigeratedItems, + bool HasHazmatItems, + bool HasFragileItems, + bool HasHighValueItems, + + // Valor total + decimal TotalDeclaredValue, + + // Camión asignado (puede ser null si aún no se asigna) + Guid? AssignedTruckId, + string? AssignedTruckType, + decimal? TruckMaxCapacityKg, + decimal? TruckMaxVolumeM3 +); + +/// +/// Evento: Intento de QR Handshake para transferencia de custodia. +/// Usado por: Agente de Fraud Prevention (n8n). +/// Propósito: Validar geolocalización del chofer vs destino esperado. +/// +/// NOTA: Este evento se activará cuando se implemente el módulo QR Handshake. +/// Por ahora está definido para que la infraestructura esté lista. +/// +public record HandshakeAttemptEvent( + Guid ShipmentId, + string TrackingNumber, + Guid TenantId, + + // Chofer que intenta el handshake + Guid DriverId, + string? DriverName, + + // Ubicación reportada por el chofer + decimal? DriverLatitude, + decimal? DriverLongitude, + DateTime ReportedTimestamp, + + // Ubicación esperada (destino) + Guid DestinationLocationId, + string? DestinationLocationCode, + decimal? DestinationLatitude, + decimal? DestinationLongitude, + + // Contexto del intento + string? DriverReportedReason, // "Normal", "Traffic", "Reroute", "Other" + bool IsWithinGeofence, // Calculado por el servicio antes de enviar + double? DistanceFromDestinationKm +); + +/// +/// Evento: Cambio de estatus de un envío (cualquier transición). +/// Usado por: Dashboard, Notificaciones, Analytics. +/// +public record ShipmentStatusChangedEvent( + Guid ShipmentId, + string TrackingNumber, + Guid TenantId, + string PreviousStatus, + string NewStatus, + DateTime ChangedAt, + Guid? ChangedByUserId +); + +/// +/// Evento: Checkpoint creado (llegada a hub, escaneo, etc.). +/// Usado por: Agentes de tracking, detección de anomalías. +/// +/// NOTA: Se activará cuando se implemente el módulo de Checkpoints. +/// +public record CheckpointCreatedEvent( + Guid CheckpointId, + Guid ShipmentId, + string TrackingNumber, + Guid TenantId, + + // Datos del checkpoint + string StatusCode, // "Loaded", "ArrivedHub", "Delivered", etc. + Guid? LocationId, + string? LocationCode, + DateTime Timestamp, + + // Quién lo creó + Guid? HandledByDriverId, + Guid? HandledByWarehouseOperatorId, + + // Contexto + string? Remarks, + bool WasQrScanned +); diff --git a/backend/src/Parhelion.Application/Interfaces/ICallbackTokenService.cs b/backend/src/Parhelion.Application/Interfaces/ICallbackTokenService.cs new file mode 100644 index 0000000..5a22cf5 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/ICallbackTokenService.cs @@ -0,0 +1,33 @@ +namespace Parhelion.Application.Interfaces; + +/// +/// Servicio para generar y validar tokens de callback para webhooks. +/// Los tokens son JWT firmados de corta duración que permiten a n8n +/// autenticarse de vuelta al API sin almacenar API keys. +/// +public interface ICallbackTokenService +{ + /// + /// Genera un token de callback para un tenant específico. + /// + /// ID del tenant para el que se genera el token. + /// ID de correlación para trazabilidad. + /// JWT firmado con claims de tenant y expiración. + string GenerateCallbackToken(Guid tenantId, Guid correlationId); + + /// + /// Valida un token de callback y extrae los claims. + /// + /// Token JWT a validar. + /// Claims si válido, null si inválido o expirado. + CallbackTokenClaims? ValidateCallbackToken(string token); +} + +/// +/// Claims extraídos de un token de callback válido. +/// +public record CallbackTokenClaims( + Guid TenantId, + Guid CorrelationId, + DateTime ExpiresAt +); diff --git a/backend/src/Parhelion.Application/Interfaces/IUnitOfWork.cs b/backend/src/Parhelion.Application/Interfaces/IUnitOfWork.cs index a52b6e0..360f8c3 100644 --- a/backend/src/Parhelion.Application/Interfaces/IUnitOfWork.cs +++ b/backend/src/Parhelion.Application/Interfaces/IUnitOfWork.cs @@ -41,6 +41,10 @@ public interface IUnitOfWork : IDisposable ITenantRepository RouteBlueprints { get; } IGenericRepository RouteSteps { get; } + // ========== NOTIFICATION / N8N REPOSITORIES ========== + IGenericRepository Notifications { get; } + ITenantRepository ServiceApiKeys { get; } + // ========== TRANSACTION CONTROL ========== /// diff --git a/backend/src/Parhelion.Application/Interfaces/IWebhookPublisher.cs b/backend/src/Parhelion.Application/Interfaces/IWebhookPublisher.cs new file mode 100644 index 0000000..b60c975 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/IWebhookPublisher.cs @@ -0,0 +1,28 @@ +namespace Parhelion.Application.Interfaces; + +/// +/// Interfaz para publicar eventos hacia sistemas externos (n8n, webhooks, etc.). +/// Diseñada para ser fire-and-forget: errores se loguean pero no interrumpen el flujo principal. +/// +public interface IWebhookPublisher +{ + /// + /// Publica un evento tipado hacia el sistema de webhooks configurado. + /// + /// Tipo del payload del evento. + /// Identificador del evento (ej: "shipment.exception"). + /// Datos del evento. + /// Token de cancelación. + Task PublishAsync(string eventType, T payload, CancellationToken cancellationToken = default) + where T : class; +} + +/// +/// Implementación nula de IWebhookPublisher para cuando los webhooks están desactivados. +/// Permite inyección de dependencias sin configuración adicional. +/// +public class NullWebhookPublisher : IWebhookPublisher +{ + public Task PublishAsync(string eventType, T payload, CancellationToken cancellationToken = default) + where T : class => Task.CompletedTask; +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IDriverService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IDriverService.cs index 9126b90..dd23445 100644 --- a/backend/src/Parhelion.Application/Interfaces/Services/IDriverService.cs +++ b/backend/src/Parhelion.Application/Interfaces/Services/IDriverService.cs @@ -39,4 +39,8 @@ public interface IDriverService : IGenericService Task> AssignTruckAsync(Guid driverId, Guid truckId, CancellationToken cancellationToken = default); + /// + /// Busca choferes cercanos a una coordenada (Crisis Management). + /// + Task> GetNearbyDriversAsync(decimal lat, decimal lon, double radiusKm, Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); } diff --git a/backend/src/Parhelion.Application/Interfaces/Services/INotificationService.cs b/backend/src/Parhelion.Application/Interfaces/Services/INotificationService.cs new file mode 100644 index 0000000..8668a02 --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/Services/INotificationService.cs @@ -0,0 +1,63 @@ +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Notification; + +namespace Parhelion.Application.Interfaces.Services; + +/// +/// Servicio para gestión de notificaciones. +/// Soporta creación desde n8n y lectura desde apps móviles. +/// +public interface INotificationService +{ + /// + /// Crea una notificación (usado por n8n y backend interno). + /// + Task> CreateAsync( + CreateNotificationRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene notificaciones paginadas de un usuario. + /// + Task> GetByUserAsync( + Guid userId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene notificaciones paginadas por rol (broadcast). + /// + Task> GetByRoleAsync( + Guid tenantId, + Guid roleId, + PagedRequest request, + CancellationToken cancellationToken = default); + + /// + /// Obtiene una notificación por ID. + /// + Task GetByIdAsync( + Guid notificationId, + CancellationToken cancellationToken = default); + + /// + /// Cuenta notificaciones no leídas del usuario. + /// + Task GetUnreadCountAsync( + Guid userId, + CancellationToken cancellationToken = default); + + /// + /// Marca una notificación como leída. + /// + Task MarkAsReadAsync( + Guid notificationId, + CancellationToken cancellationToken = default); + + /// + /// Marca todas las notificaciones del usuario como leídas. + /// + Task> MarkAllAsReadAsync( + Guid userId, + CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/ITruckService.cs b/backend/src/Parhelion.Application/Interfaces/Services/ITruckService.cs index 9be6d4a..c86defa 100644 --- a/backend/src/Parhelion.Application/Interfaces/Services/ITruckService.cs +++ b/backend/src/Parhelion.Application/Interfaces/Services/ITruckService.cs @@ -39,4 +39,8 @@ public interface ITruckService : IGenericService Task> GetAvailableAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default); + /// + /// Actualiza la ubicación GPS del camión (Telemetría). + /// + Task UpdateLocationAsync(Guid id, decimal latitude, decimal longitude, CancellationToken cancellationToken = default); } diff --git a/backend/src/Parhelion.Domain/Entities/Notification.cs b/backend/src/Parhelion.Domain/Entities/Notification.cs new file mode 100644 index 0000000..48fe19f --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/Notification.cs @@ -0,0 +1,89 @@ +using Parhelion.Domain.Common; +using Parhelion.Domain.Enums; + +namespace Parhelion.Domain.Entities; + +/// +/// Notificación generada por los agentes de IA de n8n. +/// Almacena alertas, excepciones y eventos importantes para usuarios. +/// +public class Notification : BaseEntity +{ + /// + /// Tenant propietario de la notificación. + /// + public Guid TenantId { get; set; } + public virtual Tenant Tenant { get; set; } = null!; + + /// + /// Usuario destinatario (null = broadcast a rol). + /// + public Guid? UserId { get; set; } + public virtual User? User { get; set; } + + /// + /// Rol destinatario si UserId es null. + /// + public Guid? RoleId { get; set; } + public virtual Role? Role { get; set; } + + /// + /// Tipo de notificación (Alert, Warning, Info, Success). + /// + public NotificationType Type { get; set; } = NotificationType.Info; + + /// + /// Agente de IA que generó la notificación. + /// + public NotificationSource Source { get; set; } = NotificationSource.System; + + /// + /// Título corto de la notificación. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Mensaje detallado. + /// + public string Message { get; set; } = string.Empty; + + /// + /// Datos adicionales en JSON (shipmentId, routeId, etc.). + /// + public string? MetadataJson { get; set; } + + /// + /// Tipo de entidad relacionada para deep linking. + /// + public string? RelatedEntityType { get; set; } + + /// + /// ID de entidad relacionada (ej: ShipmentId). + /// + public Guid? RelatedEntityId { get; set; } + + /// + /// Si la notificación ha sido leída. + /// + public bool IsRead { get; set; } = false; + + /// + /// Timestamp de lectura. + /// + public DateTime? ReadAt { get; set; } + + /// + /// Prioridad (1=crítica, 5=baja). + /// + public int Priority { get; set; } = 3; + + /// + /// Si requiere acción del usuario. + /// + public bool RequiresAction { get; set; } = false; + + /// + /// Acción completada por el usuario. + /// + public bool ActionCompleted { get; set; } = false; +} diff --git a/backend/src/Parhelion.Domain/Entities/ServiceApiKey.cs b/backend/src/Parhelion.Domain/Entities/ServiceApiKey.cs new file mode 100644 index 0000000..ed597ca --- /dev/null +++ b/backend/src/Parhelion.Domain/Entities/ServiceApiKey.cs @@ -0,0 +1,43 @@ +using Parhelion.Domain.Common; + +namespace Parhelion.Domain.Entities; + +/// +/// API Key para autenticación de servicios externos (n8n, microservicios, integraciones). +/// Cada key pertenece a un Tenant específico, permitiendo multi-tenant en llamadas sin JWT. +/// +/// Uso: Header X-Service-Key en requests a endpoints protegidos con [ServiceApiKey]. +/// +public class ServiceApiKey : TenantEntity +{ + /// Hash SHA256 de la API Key (nunca almacenar plain text) + public string KeyHash { get; set; } = null!; + + /// Nombre descriptivo para identificar la key (ej: "n8n-production", "webhook-test") + public string Name { get; set; } = null!; + + /// Descripción del propósito de esta key + public string? Description { get; set; } + + /// Fecha de expiración (null = no expira) + public DateTime? ExpiresAt { get; set; } + + /// Último uso registrado + public DateTime? LastUsedAt { get; set; } + + /// IP desde la que se usó por última vez + public string? LastUsedFromIp { get; set; } + + /// Si la key está activa (soft-disable sin eliminar) + public bool IsActive { get; set; } = true; + + /// + /// Scopes permitidos (comma-separated). + /// Ej: "drivers:read,notifications:write" + /// Null = acceso completo a endpoints con [ServiceApiKey]. + /// + public string? Scopes { get; set; } + + // ========== NAVIGATION ========== + public Tenant Tenant { get; set; } = null!; +} diff --git a/backend/src/Parhelion.Domain/Entities/Truck.cs b/backend/src/Parhelion.Domain/Entities/Truck.cs index 3dbcd52..f64887e 100644 --- a/backend/src/Parhelion.Domain/Entities/Truck.cs +++ b/backend/src/Parhelion.Domain/Entities/Truck.cs @@ -57,6 +57,17 @@ public class Truck : TenantEntity /// Odómetro actual en kilómetros public decimal? CurrentOdometerKm { get; set; } + // ========== TELEMETRÍA (GPS) ========== + + /// Última latitud reportada + public decimal? LastLatitude { get; set; } + + /// Última longitud reportada + public decimal? LastLongitude { get; set; } + + /// Fecha del último reporte de ubicación + public DateTime? LastLocationUpdate { get; set; } + // ========== NAVIGATION PROPERTIES ========== public Tenant Tenant { get; set; } = null!; diff --git a/backend/src/Parhelion.Domain/Enums/NotificationEnums.cs b/backend/src/Parhelion.Domain/Enums/NotificationEnums.cs new file mode 100644 index 0000000..52d89f4 --- /dev/null +++ b/backend/src/Parhelion.Domain/Enums/NotificationEnums.cs @@ -0,0 +1,43 @@ +namespace Parhelion.Domain.Enums; + +/// +/// Tipo de notificación para UI. +/// +public enum NotificationType +{ + /// Información general + Info, + + /// Operación exitosa + Success, + + /// Advertencia que requiere atención + Warning, + + /// Alerta crítica que requiere acción inmediata + Alert, + + /// Error del sistema + Error +} + +/// +/// Origen de la notificación (agente de IA o sistema). +/// +public enum NotificationSource +{ + /// Notificación generada por el sistema + System, + + /// Agente de Crisis Management (excepciones de envío) + CrisisManagement, + + /// Agente de Smart Booking (validación cargo-truck) + SmartBooking, + + /// Agente de Fraud Prevention (QR Handshake) + FraudPrevention, + + /// Notificación manual de admin + Admin +} diff --git a/backend/src/Parhelion.Infrastructure/Auth/JwtService.cs b/backend/src/Parhelion.Infrastructure/Auth/JwtService.cs index e910767..bde0268 100644 --- a/backend/src/Parhelion.Infrastructure/Auth/JwtService.cs +++ b/backend/src/Parhelion.Infrastructure/Auth/JwtService.cs @@ -23,8 +23,9 @@ public JwtService(IConfiguration configuration) { _configuration = configuration; - var secretKey = _configuration["Jwt:SecretKey"] - ?? throw new InvalidOperationException("JWT SecretKey not configured"); + var secretKey = Environment.GetEnvironmentVariable("JWT_SECRET") + ?? _configuration["Jwt:SecretKey"] + ?? throw new InvalidOperationException("JWT SecretKey not configured (JWT_SECRET envar or Jwt:SecretKey config)"); _signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); _accessTokenExpirationMinutes = int.Parse(_configuration["Jwt:AccessTokenExpirationMinutes"] ?? "120"); diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/NotificationConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/NotificationConfiguration.cs new file mode 100644 index 0000000..75109e4 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/NotificationConfiguration.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +/// +/// Configuración EF Core para Notification. +/// +public class NotificationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Notifications"); + + builder.HasKey(n => n.Id); + + builder.Property(n => n.Title) + .IsRequired() + .HasMaxLength(200); + + builder.Property(n => n.Message) + .IsRequired() + .HasMaxLength(2000); + + builder.Property(n => n.MetadataJson) + .HasMaxLength(4000); + + builder.Property(n => n.RelatedEntityType) + .HasMaxLength(100); + + builder.Property(n => n.Type) + .HasConversion() + .HasMaxLength(20); + + builder.Property(n => n.Source) + .HasConversion() + .HasMaxLength(30); + + // Índices + builder.HasIndex(n => n.TenantId); + builder.HasIndex(n => n.UserId); + builder.HasIndex(n => n.RoleId); + builder.HasIndex(n => n.IsRead); + builder.HasIndex(n => n.CreatedAt); + builder.HasIndex(n => new { n.TenantId, n.UserId, n.IsRead }); + + // Relaciones + builder.HasOne(n => n.Tenant) + .WithMany() + .HasForeignKey(n => n.TenantId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(n => n.User) + .WithMany() + .HasForeignKey(n => n.UserId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(n => n.Role) + .WithMany() + .HasForeignKey(n => n.RoleId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/ServiceApiKeyConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/ServiceApiKeyConfiguration.cs new file mode 100644 index 0000000..c830e8d --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/ServiceApiKeyConfiguration.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Parhelion.Domain.Entities; + +namespace Parhelion.Infrastructure.Data.Configurations; + +/// +/// Configuración EF Core para ServiceApiKey. +/// +public class ServiceApiKeyConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ServiceApiKeys"); + + builder.HasKey(e => e.Id); + + // KeyHash: SHA256 = 64 caracteres hex + builder.Property(e => e.KeyHash) + .IsRequired() + .HasMaxLength(64); + + // Índice único para búsqueda rápida por hash + builder.HasIndex(e => e.KeyHash) + .IsUnique() + .HasDatabaseName("IX_ServiceApiKeys_KeyHash"); + + builder.Property(e => e.Name) + .IsRequired() + .HasMaxLength(100); + + builder.Property(e => e.Description) + .HasMaxLength(500); + + builder.Property(e => e.Scopes) + .HasMaxLength(1000); + + builder.Property(e => e.LastUsedFromIp) + .HasMaxLength(45); // IPv6 max length + + // FK a Tenant + builder.HasOne(e => e.Tenant) + .WithMany() + .HasForeignKey(e => e.TenantId) + .OnDelete(DeleteBehavior.Cascade); + + // Índice compuesto para queries por tenant + builder.HasIndex(e => new { e.TenantId, e.IsActive }) + .HasDatabaseName("IX_ServiceApiKeys_Tenant_Active"); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Configurations/TruckConfiguration.cs b/backend/src/Parhelion.Infrastructure/Data/Configurations/TruckConfiguration.cs index be94fb2..9a02e65 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Configurations/TruckConfiguration.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Configurations/TruckConfiguration.cs @@ -24,6 +24,12 @@ public void Configure(EntityTypeBuilder builder) builder.Property(t => t.MaxVolumeM3) .HasPrecision(10, 2); + builder.Property(t => t.LastLatitude) + .HasPrecision(10, 6); + + builder.Property(t => t.LastLongitude) + .HasPrecision(10, 6); + // Placa única por tenant builder.HasIndex(t => new { t.TenantId, t.Plate }).IsUnique(); diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251221235514_AddNotifications.Designer.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251221235514_AddNotifications.Designer.cs new file mode 100644 index 0000000..6a75f75 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251221235514_AddNotifications.Designer.cs @@ -0,0 +1,2530 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + [Migration("20251221235514_AddNotifications")] + partial class AddNotifications + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaseUom") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DefaultHeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultLengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultWeightKg") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("DefaultWidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Sku") + .IsUnique(); + + b.ToTable("CatalogItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PreferredProductTypes") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TaxId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "CompanyName"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LicenseExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LicenseType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Curp") + .HasMaxLength(18) + .HasColumnType("character varying(18)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Department") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("EmergencyContact") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Nss") + .HasMaxLength(11) + .HasColumnType("character varying(11)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Rfc") + .HasMaxLength(13) + .HasColumnType("character varying(13)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShiftId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ShiftId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Department"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastCountDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QuantityReserved") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZoneId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("TenantId", "ExpiryDate") + .HasFilter("\"ExpiryDate\" IS NOT NULL"); + + b.HasIndex("TenantId", "ProductId"); + + b.HasIndex("ZoneId", "ProductId", "BatchNumber") + .IsUnique(); + + b.ToTable("InventoryStocks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationZoneId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginZoneId") + .HasColumnType("uuid"); + + b.Property("PerformedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Remarks") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationZoneId"); + + b.HasIndex("OriginZoneId"); + + b.HasIndex("PerformedByUserId"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId") + .HasFilter("\"ShipmentId\" IS NOT NULL"); + + b.HasIndex("TenantId", "Timestamp"); + + b.HasIndex("TenantId", "ProductId", "Timestamp"); + + b.ToTable("InventoryTransactions"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionCompleted") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("MetadataJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequiresAction") + .HasColumnType("boolean"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsRead"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "UserId", "IsRead"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DaysOfWeek") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "IsActive"); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientClientId") + .HasColumnType("uuid"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("RecipientClientId"); + + b.HasIndex("SenderId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HandledByDriverId") + .HasColumnType("uuid"); + + b.Property("HandledByWarehouseOperatorId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("LoadedOntoTruckId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("NewCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PreviousCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("HandledByDriverId"); + + b.HasIndex("HandledByWarehouseOperatorId"); + + b.HasIndex("LoadedOntoTruckId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentOdometerKm") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineNumber") + .HasColumnType("text"); + + b.Property("InsuranceExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("InsurancePolicy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NextMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationNumber") + .HasColumnType("text"); + + b.Property("Vin") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("IsSuperAdmin") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedLocationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PrimaryZoneId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssignedLocationId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("PrimaryZoneId"); + + b.ToTable("WarehouseOperators"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId", "Code") + .IsUnique(); + + b.ToTable("WarehouseZones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", null) + .WithMany("Drivers") + .HasForeignKey("TenantId"); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.HasOne("Parhelion.Domain.Entities.Shift", "Shift") + .WithMany("Employees") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Employee") + .HasForeignKey("Parhelion.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Shift"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("InventoryStocks") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "Zone") + .WithMany("InventoryStocks") + .HasForeignKey("ZoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Tenant"); + + b.Navigation("Zone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "DestinationZone") + .WithMany("DestinationTransactions") + .HasForeignKey("DestinationZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "OriginZone") + .WithMany("OriginTransactions") + .HasForeignKey("OriginZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.User", "PerformedBy") + .WithMany() + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany() + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationZone"); + + b.Navigation("OriginZone"); + + b.Navigation("PerformedBy"); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Client", "RecipientClient") + .WithMany("ShipmentsAsRecipient") + .HasForeignKey("RecipientClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Client", "Sender") + .WithMany("ShipmentsAsSender") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("RecipientClient"); + + b.Navigation("Sender"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "HandledByDriver") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByDriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseOperator", "HandledByWarehouseOperator") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByWarehouseOperatorId"); + + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") + .WithMany("LoadedCheckpoints") + .HasForeignKey("LoadedOntoTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("HandledByDriver"); + + b.Navigation("HandledByWarehouseOperator"); + + b.Navigation("LoadedOntoTruck"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("ShipmentItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "AssignedLocation") + .WithMany("AssignedWarehouseOperators") + .HasForeignKey("AssignedLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("WarehouseOperator") + .HasForeignKey("Parhelion.Domain.Entities.WarehouseOperator", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "PrimaryZone") + .WithMany("AssignedOperators") + .HasForeignKey("PrimaryZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedLocation"); + + b.Navigation("Employee"); + + b.Navigation("PrimaryZone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Zones") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Navigation("InventoryStocks"); + + b.Navigation("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Navigation("ShipmentsAsRecipient"); + + b.Navigation("ShipmentsAsSender"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("HandledCheckpoints"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Navigation("Driver"); + + b.Navigation("WarehouseOperator"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("AssignedWarehouseOperators"); + + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("LoadedCheckpoints"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Navigation("HandledCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Navigation("AssignedOperators"); + + b.Navigation("DestinationTransactions"); + + b.Navigation("InventoryStocks"); + + b.Navigation("OriginTransactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251221235514_AddNotifications.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251221235514_AddNotifications.cs new file mode 100644 index 0000000..4004332 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251221235514_AddNotifications.cs @@ -0,0 +1,103 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + /// + public partial class AddNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + RoleId = table.Column(type: "uuid", nullable: true), + Type = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Source = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Message = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), + MetadataJson = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + RelatedEntityType = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + RelatedEntityId = table.Column(type: "uuid", nullable: true), + IsRead = table.Column(type: "boolean", nullable: false), + ReadAt = table.Column(type: "timestamp with time zone", nullable: true), + Priority = table.Column(type: "integer", nullable: false), + RequiresAction = table.Column(type: "boolean", nullable: false), + ActionCompleted = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + xmin = table.Column(type: "xid", rowVersion: true, nullable: false), + CreatedByUserId = table.Column(type: "uuid", nullable: true), + LastModifiedByUserId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + table.ForeignKey( + name: "FK_Notifications_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Notifications_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Notifications_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_CreatedAt", + table: "Notifications", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_IsRead", + table: "Notifications", + column: "IsRead"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_RoleId", + table: "Notifications", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_TenantId", + table: "Notifications", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_TenantId_UserId_IsRead", + table: "Notifications", + columns: new[] { "TenantId", "UserId", "IsRead" }); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_UserId", + table: "Notifications", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Notifications"); + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222022719_AddTruckTelemetry.Designer.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222022719_AddTruckTelemetry.Designer.cs new file mode 100644 index 0000000..6d0f804 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222022719_AddTruckTelemetry.Designer.cs @@ -0,0 +1,2541 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + [Migration("20251222022719_AddTruckTelemetry")] + partial class AddTruckTelemetry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaseUom") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DefaultHeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultLengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultWeightKg") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("DefaultWidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Sku") + .IsUnique(); + + b.ToTable("CatalogItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PreferredProductTypes") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TaxId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "CompanyName"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LicenseExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LicenseType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Curp") + .HasMaxLength(18) + .HasColumnType("character varying(18)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Department") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("EmergencyContact") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Nss") + .HasMaxLength(11) + .HasColumnType("character varying(11)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Rfc") + .HasMaxLength(13) + .HasColumnType("character varying(13)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShiftId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ShiftId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Department"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastCountDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QuantityReserved") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZoneId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("TenantId", "ExpiryDate") + .HasFilter("\"ExpiryDate\" IS NOT NULL"); + + b.HasIndex("TenantId", "ProductId"); + + b.HasIndex("ZoneId", "ProductId", "BatchNumber") + .IsUnique(); + + b.ToTable("InventoryStocks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationZoneId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginZoneId") + .HasColumnType("uuid"); + + b.Property("PerformedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Remarks") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationZoneId"); + + b.HasIndex("OriginZoneId"); + + b.HasIndex("PerformedByUserId"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId") + .HasFilter("\"ShipmentId\" IS NOT NULL"); + + b.HasIndex("TenantId", "Timestamp"); + + b.HasIndex("TenantId", "ProductId", "Timestamp"); + + b.ToTable("InventoryTransactions"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionCompleted") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("MetadataJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequiresAction") + .HasColumnType("boolean"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsRead"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "UserId", "IsRead"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DaysOfWeek") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "IsActive"); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientClientId") + .HasColumnType("uuid"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("RecipientClientId"); + + b.HasIndex("SenderId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HandledByDriverId") + .HasColumnType("uuid"); + + b.Property("HandledByWarehouseOperatorId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("LoadedOntoTruckId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("NewCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PreviousCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("HandledByDriverId"); + + b.HasIndex("HandledByWarehouseOperatorId"); + + b.HasIndex("LoadedOntoTruckId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentOdometerKm") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineNumber") + .HasColumnType("text"); + + b.Property("InsuranceExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("InsurancePolicy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastLatitude") + .HasPrecision(10, 6) + .HasColumnType("numeric(10,6)"); + + b.Property("LastLocationUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLongitude") + .HasPrecision(10, 6) + .HasColumnType("numeric(10,6)"); + + b.Property("LastMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NextMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationNumber") + .HasColumnType("text"); + + b.Property("Vin") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("IsSuperAdmin") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedLocationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PrimaryZoneId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssignedLocationId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("PrimaryZoneId"); + + b.ToTable("WarehouseOperators"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId", "Code") + .IsUnique(); + + b.ToTable("WarehouseZones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", null) + .WithMany("Drivers") + .HasForeignKey("TenantId"); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.HasOne("Parhelion.Domain.Entities.Shift", "Shift") + .WithMany("Employees") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Employee") + .HasForeignKey("Parhelion.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Shift"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("InventoryStocks") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "Zone") + .WithMany("InventoryStocks") + .HasForeignKey("ZoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Tenant"); + + b.Navigation("Zone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "DestinationZone") + .WithMany("DestinationTransactions") + .HasForeignKey("DestinationZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "OriginZone") + .WithMany("OriginTransactions") + .HasForeignKey("OriginZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.User", "PerformedBy") + .WithMany() + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany() + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationZone"); + + b.Navigation("OriginZone"); + + b.Navigation("PerformedBy"); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Client", "RecipientClient") + .WithMany("ShipmentsAsRecipient") + .HasForeignKey("RecipientClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Client", "Sender") + .WithMany("ShipmentsAsSender") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("RecipientClient"); + + b.Navigation("Sender"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "HandledByDriver") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByDriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseOperator", "HandledByWarehouseOperator") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByWarehouseOperatorId"); + + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") + .WithMany("LoadedCheckpoints") + .HasForeignKey("LoadedOntoTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("HandledByDriver"); + + b.Navigation("HandledByWarehouseOperator"); + + b.Navigation("LoadedOntoTruck"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("ShipmentItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "AssignedLocation") + .WithMany("AssignedWarehouseOperators") + .HasForeignKey("AssignedLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("WarehouseOperator") + .HasForeignKey("Parhelion.Domain.Entities.WarehouseOperator", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "PrimaryZone") + .WithMany("AssignedOperators") + .HasForeignKey("PrimaryZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedLocation"); + + b.Navigation("Employee"); + + b.Navigation("PrimaryZone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Zones") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Navigation("InventoryStocks"); + + b.Navigation("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Navigation("ShipmentsAsRecipient"); + + b.Navigation("ShipmentsAsSender"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("HandledCheckpoints"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Navigation("Driver"); + + b.Navigation("WarehouseOperator"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("AssignedWarehouseOperators"); + + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("LoadedCheckpoints"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Navigation("HandledCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Navigation("AssignedOperators"); + + b.Navigation("DestinationTransactions"); + + b.Navigation("InventoryStocks"); + + b.Navigation("OriginTransactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222022719_AddTruckTelemetry.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222022719_AddTruckTelemetry.cs new file mode 100644 index 0000000..bb32e5a --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222022719_AddTruckTelemetry.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + /// + public partial class AddTruckTelemetry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastLatitude", + table: "Trucks", + type: "numeric(10,6)", + precision: 10, + scale: 6, + nullable: true); + + migrationBuilder.AddColumn( + name: "LastLocationUpdate", + table: "Trucks", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastLongitude", + table: "Trucks", + type: "numeric(10,6)", + precision: 10, + scale: 6, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastLatitude", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "LastLocationUpdate", + table: "Trucks"); + + migrationBuilder.DropColumn( + name: "LastLongitude", + table: "Trucks"); + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222225257_AddServiceApiKeys.Designer.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222225257_AddServiceApiKeys.Designer.cs new file mode 100644 index 0000000..1788c4a --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222225257_AddServiceApiKeys.Designer.cs @@ -0,0 +1,2628 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + [Migration("20251222225257_AddServiceApiKeys")] + partial class AddServiceApiKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaseUom") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DefaultHeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultLengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultWeightKg") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("DefaultWidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Sku") + .IsUnique(); + + b.ToTable("CatalogItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PreferredProductTypes") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TaxId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "CompanyName"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LicenseExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LicenseType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Curp") + .HasMaxLength(18) + .HasColumnType("character varying(18)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Department") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("EmergencyContact") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Nss") + .HasMaxLength(11) + .HasColumnType("character varying(11)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Rfc") + .HasMaxLength(13) + .HasColumnType("character varying(13)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShiftId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ShiftId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Department"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastCountDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QuantityReserved") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZoneId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("TenantId", "ExpiryDate") + .HasFilter("\"ExpiryDate\" IS NOT NULL"); + + b.HasIndex("TenantId", "ProductId"); + + b.HasIndex("ZoneId", "ProductId", "BatchNumber") + .IsUnique(); + + b.ToTable("InventoryStocks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationZoneId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginZoneId") + .HasColumnType("uuid"); + + b.Property("PerformedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Remarks") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationZoneId"); + + b.HasIndex("OriginZoneId"); + + b.HasIndex("PerformedByUserId"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId") + .HasFilter("\"ShipmentId\" IS NOT NULL"); + + b.HasIndex("TenantId", "Timestamp"); + + b.HasIndex("TenantId", "ProductId", "Timestamp"); + + b.ToTable("InventoryTransactions"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionCompleted") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("MetadataJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequiresAction") + .HasColumnType("boolean"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsRead"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "UserId", "IsRead"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ServiceApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Scopes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique() + .HasDatabaseName("IX_ServiceApiKeys_KeyHash"); + + b.HasIndex("TenantId", "IsActive") + .HasDatabaseName("IX_ServiceApiKeys_Tenant_Active"); + + b.ToTable("ServiceApiKeys", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DaysOfWeek") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "IsActive"); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientClientId") + .HasColumnType("uuid"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("RecipientClientId"); + + b.HasIndex("SenderId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HandledByDriverId") + .HasColumnType("uuid"); + + b.Property("HandledByWarehouseOperatorId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("LoadedOntoTruckId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("NewCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PreviousCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("HandledByDriverId"); + + b.HasIndex("HandledByWarehouseOperatorId"); + + b.HasIndex("LoadedOntoTruckId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentOdometerKm") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineNumber") + .HasColumnType("text"); + + b.Property("InsuranceExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("InsurancePolicy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastLatitude") + .HasPrecision(10, 6) + .HasColumnType("numeric(10,6)"); + + b.Property("LastLocationUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLongitude") + .HasPrecision(10, 6) + .HasColumnType("numeric(10,6)"); + + b.Property("LastMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NextMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationNumber") + .HasColumnType("text"); + + b.Property("Vin") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("IsSuperAdmin") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedLocationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PrimaryZoneId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssignedLocationId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("PrimaryZoneId"); + + b.ToTable("WarehouseOperators"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId", "Code") + .IsUnique(); + + b.ToTable("WarehouseZones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", null) + .WithMany("Drivers") + .HasForeignKey("TenantId"); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.HasOne("Parhelion.Domain.Entities.Shift", "Shift") + .WithMany("Employees") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Employee") + .HasForeignKey("Parhelion.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Shift"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("InventoryStocks") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "Zone") + .WithMany("InventoryStocks") + .HasForeignKey("ZoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Tenant"); + + b.Navigation("Zone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "DestinationZone") + .WithMany("DestinationTransactions") + .HasForeignKey("DestinationZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "OriginZone") + .WithMany("OriginTransactions") + .HasForeignKey("OriginZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.User", "PerformedBy") + .WithMany() + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany() + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationZone"); + + b.Navigation("OriginZone"); + + b.Navigation("PerformedBy"); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ServiceApiKey", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Client", "RecipientClient") + .WithMany("ShipmentsAsRecipient") + .HasForeignKey("RecipientClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Client", "Sender") + .WithMany("ShipmentsAsSender") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("RecipientClient"); + + b.Navigation("Sender"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "HandledByDriver") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByDriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseOperator", "HandledByWarehouseOperator") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByWarehouseOperatorId"); + + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") + .WithMany("LoadedCheckpoints") + .HasForeignKey("LoadedOntoTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("HandledByDriver"); + + b.Navigation("HandledByWarehouseOperator"); + + b.Navigation("LoadedOntoTruck"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("ShipmentItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "AssignedLocation") + .WithMany("AssignedWarehouseOperators") + .HasForeignKey("AssignedLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("WarehouseOperator") + .HasForeignKey("Parhelion.Domain.Entities.WarehouseOperator", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "PrimaryZone") + .WithMany("AssignedOperators") + .HasForeignKey("PrimaryZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedLocation"); + + b.Navigation("Employee"); + + b.Navigation("PrimaryZone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Zones") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Navigation("InventoryStocks"); + + b.Navigation("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Navigation("ShipmentsAsRecipient"); + + b.Navigation("ShipmentsAsSender"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("HandledCheckpoints"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Navigation("Driver"); + + b.Navigation("WarehouseOperator"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("AssignedWarehouseOperators"); + + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("LoadedCheckpoints"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Navigation("HandledCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Navigation("AssignedOperators"); + + b.Navigation("DestinationTransactions"); + + b.Navigation("InventoryStocks"); + + b.Navigation("OriginTransactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222225257_AddServiceApiKeys.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222225257_AddServiceApiKeys.cs new file mode 100644 index 0000000..fd7b448 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251222225257_AddServiceApiKeys.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + /// + public partial class AddServiceApiKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ServiceApiKeys", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + KeyHash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), + LastUsedAt = table.Column(type: "timestamp with time zone", nullable: true), + LastUsedFromIp = table.Column(type: "character varying(45)", maxLength: 45, nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + Scopes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + xmin = table.Column(type: "xid", rowVersion: true, nullable: false), + CreatedByUserId = table.Column(type: "uuid", nullable: true), + LastModifiedByUserId = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServiceApiKeys", x => x.Id); + table.ForeignKey( + name: "FK_ServiceApiKeys_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ServiceApiKeys_KeyHash", + table: "ServiceApiKeys", + column: "KeyHash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ServiceApiKeys_Tenant_Active", + table: "ServiceApiKeys", + columns: new[] { "TenantId", "IsActive" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ServiceApiKeys"); + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs index 7e86400..9c7d75f 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs @@ -735,6 +735,108 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("NetworkLinks"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionCompleted") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("MetadataJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequiresAction") + .HasColumnType("boolean"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsRead"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "UserId", "IsRead"); + + b.ToTable("Notifications", (string)null); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => { b.Property("Id") @@ -965,6 +1067,82 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RouteSteps"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.ServiceApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Scopes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique() + .HasDatabaseName("IX_ServiceApiKeys_KeyHash"); + + b.HasIndex("TenantId", "IsActive") + .HasDatabaseName("IX_ServiceApiKeys_Tenant_Active"); + + b.ToTable("ServiceApiKeys", (string)null); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => { b.Property("Id") @@ -1521,6 +1699,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsDeleted") .HasColumnType("boolean"); + b.Property("LastLatitude") + .HasPrecision(10, 6) + .HasColumnType("numeric(10,6)"); + + b.Property("LastLocationUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLongitude") + .HasPrecision(10, 6) + .HasColumnType("numeric(10,6)"); + b.Property("LastMaintenanceDate") .HasColumnType("timestamp with time zone"); @@ -2003,6 +2192,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Tenant"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => { b.HasOne("Parhelion.Domain.Entities.User", "User") @@ -2044,6 +2258,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("RouteBlueprint"); }); + modelBuilder.Entity("Parhelion.Domain.Entities.ServiceApiKey", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => { b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") diff --git a/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs b/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs index 3d92fcb..933a02a 100644 --- a/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs +++ b/backend/src/Parhelion.Infrastructure/Data/ParhelionDbContext.cs @@ -69,6 +69,12 @@ public ParhelionDbContext(DbContextOptions options, Guid? te public DbSet CatalogItems => Set(); public DbSet InventoryStocks => Set(); public DbSet InventoryTransactions => Set(); + + // Notificaciones (Agentes IA n8n) + public DbSet Notifications => Set(); + + // Service API Keys (autenticación de servicios externos) + public DbSet ServiceApiKeys => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/backend/src/Parhelion.Infrastructure/External/Webhooks/N8nConfiguration.cs b/backend/src/Parhelion.Infrastructure/External/Webhooks/N8nConfiguration.cs new file mode 100644 index 0000000..3603fbd --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/External/Webhooks/N8nConfiguration.cs @@ -0,0 +1,42 @@ +namespace Parhelion.Infrastructure.External.Webhooks; + +/// +/// Configuración para el servicio de webhooks n8n. +/// Se carga desde appsettings.json sección "N8n". +/// +public class N8nConfiguration +{ + /// Habilita/deshabilita el envío de webhooks (default: false) + public bool Enabled { get; set; } = false; + + /// URL base de n8n (ej: "http://n8n:5678" o "http://localhost:5678") + public string BaseUrl { get; set; } = "http://localhost:5678"; + + /// API Key opcional para autenticación con n8n + public string? ApiKey { get; set; } + + /// Timeout en segundos para requests HTTP (default: 10) + public int TimeoutSeconds { get; set; } = 10; + + /// + /// Mapeo de tipos de evento a rutas de webhook. + /// Key: EventType (ej: "shipment.exception") + /// Value: Path del webhook (ej: "/webhook/shipment-exception") + /// + public Dictionary Webhooks { get; set; } = new(); + + /// + /// Obtiene la URL completa del webhook para un tipo de evento. + /// + /// Tipo de evento (ej: "shipment.exception") + /// URL completa o null si no está configurado + public string? GetWebhookUrl(string eventType) + { + if (!Webhooks.TryGetValue(eventType, out var path)) + return null; + + var baseUrl = BaseUrl.TrimEnd('/'); + var webhookPath = path.StartsWith('/') ? path : $"/{path}"; + return $"{baseUrl}{webhookPath}"; + } +} diff --git a/backend/src/Parhelion.Infrastructure/External/Webhooks/N8nWebhookPublisher.cs b/backend/src/Parhelion.Infrastructure/External/Webhooks/N8nWebhookPublisher.cs new file mode 100644 index 0000000..be59e10 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/External/Webhooks/N8nWebhookPublisher.cs @@ -0,0 +1,147 @@ +using System.Net.Http.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Parhelion.Application.DTOs.Webhooks; +using Parhelion.Application.Interfaces; + +namespace Parhelion.Infrastructure.External.Webhooks; + +/// +/// Implementación de IWebhookPublisher que envía eventos a n8n. +/// +/// Características: +/// - Fire-and-forget: errores se loguean pero no interrumpen el flujo +/// - Configurable: se habilita/deshabilita via appsettings +/// - CallbackToken: cada webhook incluye un JWT firmado para autenticación de retorno +/// +public class N8nWebhookPublisher : IWebhookPublisher +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly N8nConfiguration _config; + private readonly ICallbackTokenService _tokenService; + + public N8nWebhookPublisher( + HttpClient httpClient, + ILogger logger, + IOptions config, + ICallbackTokenService tokenService) + { + _httpClient = httpClient; + _logger = logger; + _config = config.Value; + _tokenService = tokenService; + + // Configurar timeout + _httpClient.Timeout = TimeSpan.FromSeconds(_config.TimeoutSeconds); + + // Configurar API Key si existe (para autenticación con n8n) + if (!string.IsNullOrEmpty(_config.ApiKey)) + { + _httpClient.DefaultRequestHeaders.Add("Authorization", _config.ApiKey); + } + } + + /// + public async Task PublishAsync(string eventType, T payload, CancellationToken cancellationToken = default) + where T : class + { + // Si está deshabilitado, salir silenciosamente + if (!_config.Enabled) + { + _logger.LogDebug("Webhook disabled, skipping event {EventType}", eventType); + return; + } + + // Obtener URL del webhook + var webhookUrl = _config.GetWebhookUrl(eventType); + if (webhookUrl == null) + { + _logger.LogDebug("No webhook configured for event {EventType}", eventType); + return; + } + + try + { + var correlationId = Guid.NewGuid(); + + // Extraer TenantId del payload (todos los eventos deben tenerlo) + var tenantId = ExtractTenantId(payload); + + // Generar CallbackToken para autenticación de n8n + string callbackToken; + if (tenantId.HasValue) + { + callbackToken = _tokenService.GenerateCallbackToken(tenantId.Value, correlationId); + } + else + { + // Si no hay TenantId, generar token sin tenant (para eventos globales) + callbackToken = _tokenService.GenerateCallbackToken(Guid.Empty, correlationId); + _logger.LogWarning("Payload for {EventType} has no TenantId, using empty tenant", eventType); + } + + // Crear envelope con metadatos y CallbackToken + var envelope = new WebhookEvent( + EventType: eventType, + Timestamp: DateTime.UtcNow, + CorrelationId: correlationId, + CallbackToken: callbackToken, + Payload: payload + ); + + // Enviar request HTTP POST + var response = await _httpClient.PostAsJsonAsync(webhookUrl, envelope, cancellationToken); + + if (response.IsSuccessStatusCode) + { + _logger.LogInformation( + "Webhook sent successfully: {EventType} to {Url} (Status: {StatusCode}, CorrelationId: {CorrelationId})", + eventType, webhookUrl, (int)response.StatusCode, correlationId); + } + else + { + _logger.LogWarning( + "Webhook returned non-success status: {EventType} to {Url} (Status: {StatusCode})", + eventType, webhookUrl, (int)response.StatusCode); + } + } + catch (TaskCanceledException ex) when (ex.CancellationToken != cancellationToken) + { + // Timeout + _logger.LogWarning( + "Webhook timeout: {EventType} to {Url} (Timeout: {Timeout}s)", + eventType, webhookUrl, _config.TimeoutSeconds); + } + catch (HttpRequestException ex) + { + // Error de red + _logger.LogWarning(ex, + "Webhook HTTP error: {EventType} to {Url}", + eventType, webhookUrl); + } + catch (Exception ex) + { + // Cualquier otro error + _logger.LogWarning(ex, + "Webhook unexpected error: {EventType} to {Url}", + eventType, webhookUrl); + } + + // NOTA: No lanzamos excepciones - fire-and-forget + } + + /// + /// Extrae TenantId del payload usando reflection. + /// Todos los eventos webhook deben tener una propiedad TenantId. + /// + private static Guid? ExtractTenantId(T payload) where T : class + { + var property = typeof(T).GetProperty("TenantId"); + if (property?.PropertyType == typeof(Guid)) + { + return (Guid?)property.GetValue(payload); + } + return null; + } +} diff --git a/backend/src/Parhelion.Infrastructure/Repositories/UnitOfWork.cs b/backend/src/Parhelion.Infrastructure/Repositories/UnitOfWork.cs index eed6178..f9cdd42 100644 --- a/backend/src/Parhelion.Infrastructure/Repositories/UnitOfWork.cs +++ b/backend/src/Parhelion.Infrastructure/Repositories/UnitOfWork.cs @@ -41,6 +41,8 @@ public class UnitOfWork : IUnitOfWork private ITenantRepository? _networkLinks; private ITenantRepository? _routeBlueprints; private IGenericRepository? _routeSteps; + private IGenericRepository? _notifications; + private ITenantRepository? _serviceApiKeys; public UnitOfWork(ParhelionDbContext context) { @@ -123,6 +125,14 @@ public UnitOfWork(ParhelionDbContext context) public IGenericRepository RouteSteps => _routeSteps ??= new GenericRepository(_context); + // ========== NOTIFICATION / N8N REPOSITORIES ========== + + public IGenericRepository Notifications => + _notifications ??= new GenericRepository(_context); + + public ITenantRepository ServiceApiKeys => + _serviceApiKeys ??= new TenantRepository(_context); + // ========== TRANSACTION CONTROL ========== public async Task SaveChangesAsync(CancellationToken cancellationToken = default) diff --git a/backend/src/Parhelion.Infrastructure/Services/Auth/CallbackTokenService.cs b/backend/src/Parhelion.Infrastructure/Services/Auth/CallbackTokenService.cs new file mode 100644 index 0000000..d7a66de --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Auth/CallbackTokenService.cs @@ -0,0 +1,109 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using Parhelion.Application.Interfaces; + +namespace Parhelion.Infrastructure.Services.Auth; + +/// +/// Implementación de ICallbackTokenService usando JWT firmados. +/// Los tokens son de corta duración (15 minutos) y contienen TenantId + CorrelationId. +/// +public class CallbackTokenService : ICallbackTokenService +{ + private readonly string _secretKey; + private readonly string _issuer; + private readonly TimeSpan _tokenLifetime = TimeSpan.FromMinutes(15); + + public CallbackTokenService(IConfiguration configuration) + { + _secretKey = Environment.GetEnvironmentVariable("JWT_SECRET") + ?? configuration["Jwt:SecretKey"] + ?? throw new InvalidOperationException("JWT_SECRET is required"); + + _issuer = configuration["Jwt:Issuer"] ?? "Parhelion"; + } + + /// + public string GenerateCallbackToken(Guid tenantId, Guid correlationId) + { + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey)); + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim("tenant_id", tenantId.ToString()), + new Claim("correlation_id", correlationId.ToString()), + new Claim("token_type", "callback"), // Distinguir de tokens de usuario + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var token = new JwtSecurityToken( + issuer: _issuer, + audience: "n8n-callback", // Audience específico para callbacks + claims: claims, + notBefore: DateTime.UtcNow, + expires: DateTime.UtcNow.Add(_tokenLifetime), + signingCredentials: credentials + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + /// + public CallbackTokenClaims? ValidateCallbackToken(string token) + { + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey)); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = _issuer, + ValidateAudience = true, + ValidAudience = "n8n-callback", + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = securityKey, + ClockSkew = TimeSpan.FromSeconds(30) // Tolerancia mínima + }; + + var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); + + // Verificar que es un callback token + var tokenTypeClaim = principal.FindFirst("token_type"); + if (tokenTypeClaim?.Value != "callback") + { + return null; + } + + var tenantIdClaim = principal.FindFirst("tenant_id"); + var correlationIdClaim = principal.FindFirst("correlation_id"); + + if (tenantIdClaim == null || correlationIdClaim == null) + { + return null; + } + + if (!Guid.TryParse(tenantIdClaim.Value, out var tenantId) || + !Guid.TryParse(correlationIdClaim.Value, out var correlationId)) + { + return null; + } + + return new CallbackTokenClaims( + tenantId, + correlationId, + validatedToken.ValidTo + ); + } + catch (Exception ex) + { + return null; + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Core/TenantService.cs b/backend/src/Parhelion.Infrastructure/Services/Core/TenantService.cs index baae4b2..97ca67e 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Core/TenantService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Core/TenantService.cs @@ -34,7 +34,7 @@ public async Task> GetAllAsync( orderBy: q => q.OrderByDescending(t => t.CreatedAt), cancellationToken); - var dtos = items.Select(MapToResponse); + var dtos = items.Select(t => MapToResponse(t)); return PagedResult.From(dtos, totalCount, request); } @@ -74,11 +74,54 @@ public async Task> CreateAsync( }; await _unitOfWork.Tenants.AddAsync(entity, cancellationToken); + + // Generar ServiceApiKey automáticamente para el nuevo tenant + var (plainTextKey, apiKey) = GenerateServiceApiKey(entity.Id, entity.CompanyName); + await _unitOfWork.ServiceApiKeys.AddAsync(apiKey, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); return OperationResult.Ok( - MapToResponse(entity), - "Tenant creado exitosamente"); + MapToResponse(entity, plainTextKey), + "Tenant creado exitosamente. IMPORTANTE: Guarda la API Key, no podrás verla de nuevo."); + } + + /// + /// Genera una ServiceApiKey segura para un tenant. + /// Retorna (plainTextKey, entity) - La key en texto plano solo se muestra una vez. + /// + private static (string PlainTextKey, ServiceApiKey Entity) GenerateServiceApiKey(Guid tenantId, string tenantName) + { + // Generar key aleatoria segura: prefix + random bytes + var randomBytes = new byte[32]; + System.Security.Cryptography.RandomNumberGenerator.Fill(randomBytes); + var plainTextKey = $"pk_{tenantName.ToLowerInvariant().Replace(" ", "")[..Math.Min(6, tenantName.Length)]}_{Convert.ToBase64String(randomBytes).Replace("+", "").Replace("/", "").Replace("=", "")[..32]}"; + + // Hash SHA256 para almacenamiento seguro + var keyHash = ComputeSha256Hash(plainTextKey); + + var apiKey = new ServiceApiKey + { + Id = Guid.NewGuid(), + TenantId = tenantId, + KeyHash = keyHash, + Name = $"n8n-{tenantName.ToLowerInvariant().Replace(" ", "-")}", + Description = "API Key generada automáticamente para integración n8n", + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + return (plainTextKey, apiKey); + } + + /// + /// Computa SHA256 hash de la key para almacenamiento seguro. + /// + private static string ComputeSha256Hash(string rawData) + { + var bytes = System.Security.Cryptography.SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(rawData)); + return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); } /// @@ -164,7 +207,7 @@ public async Task> GetActiveAsync( orderBy: q => q.OrderByDescending(t => t.CreatedAt), cancellationToken); - var dtos = items.Select(MapToResponse); + var dtos = items.Select(t => MapToResponse(t)); return PagedResult.From(dtos, totalCount, request); } @@ -193,7 +236,7 @@ public async Task SetActiveStatusAsync( /// /// Mapea una entidad Tenant a su DTO de respuesta. /// - private static TenantResponse MapToResponse(Tenant entity) => new( + private static TenantResponse MapToResponse(Tenant entity, string? generatedApiKey = null) => new( entity.Id, entity.CompanyName, entity.ContactEmail, @@ -201,6 +244,7 @@ public async Task SetActiveStatusAsync( entity.DriverCount, entity.IsActive, entity.CreatedAt, - entity.UpdatedAt + entity.UpdatedAt, + generatedApiKey ); } diff --git a/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs b/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs index 250496a..5ea4886 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Fleet/DriverService.cs @@ -137,6 +137,64 @@ public async Task> UpdateStatusAsync(Guid id, Dr return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), $"Estado actualizado a {status}"); } + public async Task> GetNearbyDriversAsync(decimal lat, decimal lon, double radiusKm, Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) + { + // 1. Obtener empleados del tenant + var employees = await _unitOfWork.Employees.FindAsync(e => e.TenantId == tenantId, cancellationToken); + var employeeIds = employees.Select(e => e.Id).ToHashSet(); + + // 2. Obtener choferes activos con camión asignado + // Nota: Idealmente esto se haría con PostGIS, pero para MVP filtramos en memoria + var drivers = await _unitOfWork.Drivers.FindAsync( + d => employeeIds.Contains(d.EmployeeId) && + d.Status == DriverStatus.Available && + d.CurrentTruckId != null, + cancellationToken); + + var resultDtos = new List(); + + foreach (var driver in drivers) + { + var truck = await _unitOfWork.Trucks.GetByIdAsync(driver.CurrentTruckId!.Value, cancellationToken); + + // Si el camión no tiene ubicación, saltar + if (truck == null || truck.LastLatitude == null || truck.LastLongitude == null) continue; + + // 3. Fórmula del Haversine + var distance = CalculateDistance( + (double)lat, (double)lon, + (double)truck.LastLatitude.Value, (double)truck.LastLongitude.Value); + + if (distance <= radiusKm) + { + resultDtos.Add(await MapToResponseAsync(driver, cancellationToken)); + } + } + + // Paginación en memoria + var totalCount = resultDtos.Count; + var pagedItems = resultDtos + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToList(); + + return PagedResult.From(pagedItems, totalCount, request); + } + + private static double CalculateDistance(double lat1, double lon1, double lat2, double lon2) + { + var R = 6371; // Radio de la Tierra en km + var dLat = ToRadians(lat2 - lat1); + var dLon = ToRadians(lon2 - lon1); + var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) * + Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); + return R * c; + } + + private static double ToRadians(double angle) => Math.PI * angle / 180.0; + public async Task> AssignTruckAsync(Guid driverId, Guid truckId, CancellationToken cancellationToken = default) { var entity = await _unitOfWork.Drivers.GetByIdAsync(driverId, cancellationToken); diff --git a/backend/src/Parhelion.Infrastructure/Services/Fleet/TruckService.cs b/backend/src/Parhelion.Infrastructure/Services/Fleet/TruckService.cs index 2199388..b9236a7 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Fleet/TruckService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Fleet/TruckService.cs @@ -144,6 +144,20 @@ public async Task> SetActiveStatusAsync(Guid id, return OperationResult.Ok(MapToResponse(entity), $"Estado actualizado"); } + public async Task UpdateLocationAsync(Guid id, decimal latitude, decimal longitude, CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Trucks.GetByIdAsync(id, cancellationToken); + if (entity == null) return OperationResult.Fail("Camión no encontrado"); + + entity.LastLatitude = latitude; + entity.LastLongitude = longitude; + entity.LastLocationUpdate = DateTime.UtcNow; + + _unitOfWork.Trucks.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + return OperationResult.Ok(); + } + public async Task> GetAvailableAsync(Guid tenantId, PagedRequest request, CancellationToken cancellationToken = default) { var (items, totalCount) = await _unitOfWork.Trucks.GetPagedAsync(request, filter: t => t.TenantId == tenantId && t.IsActive, orderBy: q => q.OrderBy(t => t.Plate), cancellationToken); diff --git a/backend/src/Parhelion.Infrastructure/Services/Notification/NotificationService.cs b/backend/src/Parhelion.Infrastructure/Services/Notification/NotificationService.cs new file mode 100644 index 0000000..fd60ac3 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Notification/NotificationService.cs @@ -0,0 +1,233 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Parhelion.Application.DTOs.Common; +using Parhelion.Application.DTOs.Notification; +using Parhelion.Application.Interfaces; +using Parhelion.Application.Interfaces.Services; +using Parhelion.Domain.Entities; +using Parhelion.Domain.Enums; + +namespace Parhelion.Infrastructure.Services.Notification; + +/// +/// Implementación del servicio de notificaciones. +/// +public class NotificationService : INotificationService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public NotificationService( + IUnitOfWork unitOfWork, + ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + /// + public async Task> CreateAsync( + CreateNotificationRequest request, + CancellationToken cancellationToken = default) + { + try + { + // Validar que el tenant existe + var tenantExists = await _unitOfWork.Tenants + .AnyAsync(t => t.Id == request.TenantId, cancellationToken); + + if (!tenantExists) + { + return OperationResult.Fail("Tenant not found"); + } + + // Validar usuario si se proporciona + if (request.UserId.HasValue) + { + var userExists = await _unitOfWork.Users + .AnyAsync(u => u.Id == request.UserId.Value, cancellationToken); + + if (!userExists) + { + return OperationResult.Fail("User not found"); + } + } + + // Parsear enums + if (!Enum.TryParse(request.Type, true, out var type)) + { + return OperationResult.Fail($"Invalid notification type: {request.Type}"); + } + + if (!Enum.TryParse(request.Source, true, out var source)) + { + return OperationResult.Fail($"Invalid notification source: {request.Source}"); + } + + // Crear entidad + var entity = new Domain.Entities.Notification + { + TenantId = request.TenantId, + UserId = request.UserId, + RoleId = request.RoleId, + Type = type, + Source = source, + Title = request.Title, + Message = request.Message, + MetadataJson = request.MetadataJson, + RelatedEntityType = request.RelatedEntityType, + RelatedEntityId = request.RelatedEntityId, + Priority = request.Priority, + RequiresAction = request.RequiresAction, + IsRead = false + }; + + await _unitOfWork.Notifications.AddAsync(entity, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Notification created: {Id} for User:{UserId} Role:{RoleId} Source:{Source}", + entity.Id, request.UserId, request.RoleId, request.Source); + + return OperationResult.Ok(MapToResponse(entity)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating notification"); + return OperationResult.Fail($"Error creating notification: {ex.Message}"); + } + } + + /// + public async Task> GetByUserAsync( + Guid userId, + PagedRequest request, + CancellationToken cancellationToken = default) + { + var query = _unitOfWork.Notifications.Query() + .Where(n => n.UserId == userId) + .OrderByDescending(n => n.CreatedAt); + + var totalCount = await query.CountAsync(cancellationToken); + + var items = await query + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .Select(n => MapToResponse(n)) + .ToListAsync(cancellationToken); + + return new PagedResult( + items, totalCount, request.Page, request.PageSize); + } + + /// + public async Task> GetByRoleAsync( + Guid tenantId, + Guid roleId, + PagedRequest request, + CancellationToken cancellationToken = default) + { + var query = _unitOfWork.Notifications.Query() + .Where(n => n.TenantId == tenantId && n.RoleId == roleId) + .OrderByDescending(n => n.CreatedAt); + + var totalCount = await query.CountAsync(cancellationToken); + + var items = await query + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .Select(n => MapToResponse(n)) + .ToListAsync(cancellationToken); + + return new PagedResult( + items, totalCount, request.Page, request.PageSize); + } + + /// + public async Task GetByIdAsync( + Guid notificationId, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Notifications + .FirstOrDefaultAsync(n => n.Id == notificationId, cancellationToken); + + return entity != null ? MapToResponse(entity) : null; + } + + /// + public async Task GetUnreadCountAsync( + Guid userId, + CancellationToken cancellationToken = default) + { + return await _unitOfWork.Notifications.Query() + .Where(n => n.UserId == userId && !n.IsRead) + .CountAsync(cancellationToken); + } + + /// + public async Task MarkAsReadAsync( + Guid notificationId, + CancellationToken cancellationToken = default) + { + var entity = await _unitOfWork.Notifications + .FirstOrDefaultAsync(n => n.Id == notificationId, cancellationToken); + + if (entity == null) + { + return OperationResult.Fail("Notification not found"); + } + + entity.IsRead = true; + entity.ReadAt = DateTime.UtcNow; + + _unitOfWork.Notifications.Update(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok(); + } + + /// + public async Task> MarkAllAsReadAsync( + Guid userId, + CancellationToken cancellationToken = default) + { + var unreadNotifications = await _unitOfWork.Notifications.Query() + .Where(n => n.UserId == userId && !n.IsRead) + .ToListAsync(cancellationToken); + + var now = DateTime.UtcNow; + foreach (var notification in unreadNotifications) + { + notification.IsRead = true; + notification.ReadAt = now; + _unitOfWork.Notifications.Update(notification); + } + + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return OperationResult.Ok(unreadNotifications.Count); + } + + private static NotificationResponse MapToResponse(Domain.Entities.Notification entity) + { + return new NotificationResponse( + entity.Id, + entity.TenantId, + entity.UserId, + entity.RoleId, + entity.Type.ToString(), + entity.Source.ToString(), + entity.Title, + entity.Message, + entity.MetadataJson, + entity.RelatedEntityType, + entity.RelatedEntityId, + entity.IsRead, + entity.ReadAt, + entity.Priority, + entity.RequiresAction, + entity.ActionCompleted, + entity.CreatedAt + ); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs index ffcb1c7..c44559c 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentService.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Shipment; +using Parhelion.Application.DTOs.Webhooks; using Parhelion.Application.Interfaces; using Parhelion.Application.Interfaces.Services; using Parhelion.Application.Interfaces.Validators; @@ -17,11 +18,16 @@ public class ShipmentService : IShipmentService { private readonly IUnitOfWork _unitOfWork; private readonly ICargoCompatibilityValidator _cargoValidator; + private readonly IWebhookPublisher _webhookPublisher; - public ShipmentService(IUnitOfWork unitOfWork, ICargoCompatibilityValidator cargoValidator) + public ShipmentService( + IUnitOfWork unitOfWork, + ICargoCompatibilityValidator cargoValidator, + IWebhookPublisher webhookPublisher) { _unitOfWork = unitOfWork; _cargoValidator = cargoValidator; + _webhookPublisher = webhookPublisher; } @@ -67,6 +73,25 @@ public async Task> CreateAsync( }; await _unitOfWork.Shipments.AddAsync(entity, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); + + // Publicar evento de creación para validación de booking (n8n) + await _webhookPublisher.PublishAsync(WebhookEventTypes.ShipmentCreated, new BookingRequestEvent( + ShipmentId: entity.Id, + TrackingNumber: entity.TrackingNumber, + TenantId: entity.TenantId, + TotalWeightKg: entity.TotalWeightKg, + TotalVolumeM3: entity.TotalVolumeM3, + HasRefrigeratedItems: false, // Se actualizará cuando se agreguen items + HasHazmatItems: false, + HasFragileItems: false, + HasHighValueItems: entity.DeclaredValue > 500_000, + TotalDeclaredValue: entity.DeclaredValue ?? 0, + AssignedTruckId: null, + AssignedTruckType: null, + TruckMaxCapacityKg: null, + TruckMaxVolumeM3: null + ), cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Envío creado exitosamente"); } @@ -187,7 +212,8 @@ public async Task> UpdateStatusAsync(Guid ship if (entity == null) return OperationResult.Fail("Envío no encontrado"); // Validate status transition - var validationResult = ValidateStatusTransition(entity.Status, newStatus); + var previousStatus = entity.Status; + var validationResult = ValidateStatusTransition(previousStatus, newStatus); if (!validationResult.IsValid) return OperationResult.Fail(validationResult.ErrorMessage!); @@ -200,8 +226,97 @@ public async Task> UpdateStatusAsync(Guid ship _unitOfWork.Shipments.Update(entity); await _unitOfWork.SaveChangesAsync(cancellationToken); + + // Publicar eventos de webhook según el nuevo estado + await PublishStatusChangeEventsAsync(entity, previousStatus, newStatus, cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), $"Estado actualizado a {newStatus}"); } + + /// + /// Publica eventos de webhook basados en el cambio de estado. + /// + private async Task PublishStatusChangeEventsAsync( + Domain.Entities.Shipment entity, + ShipmentStatus previousStatus, + ShipmentStatus newStatus, + CancellationToken ct) + { + // Siempre publicar evento de cambio de estado + await _webhookPublisher.PublishAsync(WebhookEventTypes.ShipmentStatusChanged, new ShipmentStatusChangedEvent( + ShipmentId: entity.Id, + TrackingNumber: entity.TrackingNumber, + TenantId: entity.TenantId, + PreviousStatus: previousStatus.ToString(), + NewStatus: newStatus.ToString(), + ChangedAt: DateTime.UtcNow, + ChangedByUserId: null // TODO: obtener del contexto de usuario + ), ct); + + // Si cambió a Exception, publicar evento especial para Crisis Management + if (newStatus == ShipmentStatus.Exception) + { + await PublishExceptionEventAsync(entity, ct); + } + } + + /// + /// Publica evento de excepción para el agente de Crisis Management (n8n). + /// + private async Task PublishExceptionEventAsync(Domain.Entities.Shipment entity, CancellationToken ct) + { + // Obtener items para determinar tipo de carga + var items = await _unitOfWork.ShipmentItems.FindAsync(i => i.ShipmentId == entity.Id, ct); + var itemsList = items.ToList(); + + var cargoType = itemsList.Any(i => i.RequiresRefrigeration) ? "Perishable" + : itemsList.Any(i => i.IsHazardous) ? "Hazmat" + : entity.DeclaredValue > 500_000 ? "HighValue" + : "Standard"; + + // Obtener ubicaciones + var originLocation = await _unitOfWork.Locations.GetByIdAsync(entity.OriginLocationId, ct); + var destLocation = await _unitOfWork.Locations.GetByIdAsync(entity.DestinationLocationId, ct); + + // Obtener tipo de camión y ubicación GPS + string? truckType = null; + decimal? trkLat = null; + decimal? trkLon = null; + + if (entity.TruckId.HasValue) + { + var truck = await _unitOfWork.Trucks.GetByIdAsync(entity.TruckId.Value, ct); + if (truck != null) + { + truckType = truck.Type.ToString(); + trkLat = truck.LastLatitude; + trkLon = truck.LastLongitude; + } + } + + await _webhookPublisher.PublishAsync(WebhookEventTypes.ShipmentException, new ShipmentExceptionEvent( + ShipmentId: entity.Id, + TrackingNumber: entity.TrackingNumber, + TenantId: entity.TenantId, + CurrentLocationId: entity.OriginLocationId, // TODO: usar CurrentLocationId cuando exista + CurrentLocationCode: originLocation?.Code, + Latitude: trkLat, // <--- Nueva data GPS + Longitude: trkLon, // <--- Nueva data GPS + DestinationLocationId: entity.DestinationLocationId, + DestinationLocationCode: destLocation?.Code, + CargoType: cargoType, + OriginalETA: entity.EstimatedArrival, + ScheduledDeparture: entity.ScheduledDeparture, + TotalDeclaredValue: entity.DeclaredValue, + TotalWeightKg: entity.TotalWeightKg, + TotalVolumeM3: entity.TotalVolumeM3, + DriverId: entity.DriverId, + TruckId: entity.TruckId, + TruckType: truckType, + IsDelayed: entity.IsDelayed, + ExceptionReason: null // TODO: capturar razón del checkpoint + ), ct); + } /// /// Validates if a status transition is allowed based on business rules. diff --git a/backend/tests/Parhelion.Tests/Integration/WMSIntegrationTests.cs b/backend/tests/Parhelion.Tests/Integration/WMSIntegrationTests.cs index 3a62c9d..f14b764 100644 --- a/backend/tests/Parhelion.Tests/Integration/WMSIntegrationTests.cs +++ b/backend/tests/Parhelion.Tests/Integration/WMSIntegrationTests.cs @@ -117,6 +117,7 @@ public async Task InventoryGuard_CannotOverReserve() // Assert: Should fail Assert.False(result.Success); + Assert.NotNull(result.Message); Assert.Contains("insuficiente", result.Message.ToLower()); } } diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Network/NetworkLinkServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Network/NetworkLinkServiceTests.cs index d01323c..ac8661a 100644 --- a/backend/tests/Parhelion.Tests/Unit/Services/Network/NetworkLinkServiceTests.cs +++ b/backend/tests/Parhelion.Tests/Unit/Services/Network/NetworkLinkServiceTests.cs @@ -53,6 +53,7 @@ public async Task CreateAsync_InvalidOrigin_ReturnsFailure() var result = await service.CreateAsync(request); Assert.False(result.Success); + Assert.NotNull(result.Message); Assert.Contains("origen", result.Message.ToLower()); } diff --git a/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs b/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs index 0519568..f27ec30 100644 --- a/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs +++ b/backend/tests/Parhelion.Tests/Unit/Services/Shipment/ShipmentServiceTests.cs @@ -1,5 +1,6 @@ using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Shipment; +using Parhelion.Application.Interfaces; using Parhelion.Infrastructure.Services.Shipment; using Parhelion.Infrastructure.Validators; using Parhelion.Tests.Fixtures; @@ -14,6 +15,7 @@ public class ShipmentServiceTests : IClassFixture { private readonly ServiceTestFixture _fixture; private readonly CargoCompatibilityValidator _cargoValidator = new(); + private readonly IWebhookPublisher _webhookPublisher = new NullWebhookPublisher(); public ShipmentServiceTests(ServiceTestFixture fixture) => _fixture = fixture; @@ -22,7 +24,7 @@ public async Task GetAllAsync_ReturnsPagedResult() { // Arrange var (uow, ctx) = _fixture.CreateUnitOfWork(); - var service = new ShipmentService(uow, _cargoValidator); + var service = new ShipmentService(uow, _cargoValidator, _webhookPublisher); var request = new PagedRequest { Page = 1, PageSize = 10 }; // Act @@ -37,7 +39,7 @@ public async Task GetByIdAsync_NonExisting_ReturnsNull() { // Arrange var (uow, ctx) = _fixture.CreateUnitOfWork(); - var service = new ShipmentService(uow, _cargoValidator); + var service = new ShipmentService(uow, _cargoValidator, _webhookPublisher); // Act var result = await service.GetByIdAsync(Guid.NewGuid()); @@ -51,7 +53,7 @@ public async Task ExistsAsync_NonExisting_ReturnsFalse() { // Arrange var (uow, ctx) = _fixture.CreateUnitOfWork(); - var service = new ShipmentService(uow, _cargoValidator); + var service = new ShipmentService(uow, _cargoValidator, _webhookPublisher); // Act var exists = await service.ExistsAsync(Guid.NewGuid()); @@ -60,4 +62,3 @@ public async Task ExistsAsync_NonExisting_ReturnsFalse() Assert.False(exists); } } - diff --git a/database-schema.md b/database-schema.md index 4b33e8f..991afa2 100644 --- a/database-schema.md +++ b/database-schema.md @@ -1,13 +1,13 @@ # PARHELION-LOGISTICS | Modelo de Base de Datos -**Version:** 2.5 (v0.4.4 - WMS Enhancement) +**Version:** 2.6 (v0.5.6 - n8n Integration + Webhooks + Notifications) **Fecha:** Diciembre 2025 **Motor:** PostgreSQL + Entity Framework Core (Code First) **Estado:** Diseno Cerrado - Listo para Implementacion > **Nota Tecnica:** Esta plataforma unifica WMS (Warehouse Management System) y TMS (Transportation Management System). El modulo de almacen gestiona inventario estatico y carga, mientras que el nucleo TMS maneja logistica de media milla: gestion de flotas tipificadas, redes Hub & Spoke y trazabilidad de envios en movimiento. -> **v0.4.4:** Agrega CatalogItem (catalogo maestro de productos), InventoryStock (inventario cuantificado por zona y lote), InventoryTransaction (Kardex de movimientos), campos de auditoria (CreatedByUserId, LastModifiedByUserId), geolocalizacion (Latitude/Longitude), y optimistic locking (RowVersion). +> **v0.5.6:** Agrega Notification (sistema de notificaciones para agentes IA), ServiceApiKey (autenticación multi-tenant para servicios externos), telemetría GPS en Truck (LastLatitude/Longitude), y búsqueda geospacial de choferes. --- @@ -85,11 +85,11 @@ erDiagram --- -## 1.1 Entidades del Sistema (23 tablas) +## 1.1 Entidades del Sistema (25 tablas) | Modulo | Entidades | | ------------- | ------------------------------------------------------------ | -| **Core** | Tenant, User, Role, RefreshToken | +| **Core** | Tenant, User, Role, RefreshToken, ServiceApiKey | | **Employee** | Employee, Shift | | **Fleet** | Driver, Truck, FleetLog | | **Warehouse** | Location, WarehouseZone, WarehouseOperator | @@ -97,6 +97,7 @@ erDiagram | **Shipment** | Shipment, ShipmentItem, ShipmentCheckpoint, ShipmentDocument | | **Routing** | RouteBlueprint, RouteStep, NetworkLink | | **CRM** | Client | +| **n8n** | Notification | --- @@ -110,7 +111,117 @@ erDiagram | `USER` | Usuarios del sistema (Admin, Chofer, Demo). Siempre pertenece a un Tenant. | | `ROLE` | Roles del sistema: `Admin`, `Driver`, `DemoUser`. | -### 2.2 Módulo de Flotilla +### 2.2 Módulo n8n / Automatización (v0.5.6) + +Este módulo soporta la integración con n8n para automatización de workflows y agentes de IA. + +#### 2.2.1 ServiceApiKey - Autenticación de Agentes + +| Campo | Tipo | Descripción | +| ---------------- | ------------- | ----------------------------------------------------------------- | +| `Id` | UUID | Primary Key | +| `TenantId` | UUID | FK → Tenant. Cada API Key pertenece a un tenant específico | +| `KeyHash` | VARCHAR(64) | SHA256 hash de la key (nunca plain text) | +| `Name` | VARCHAR(100) | Nombre descriptivo (ej: "n8n-production") | +| `Description` | VARCHAR(500) | Propósito de la key | +| `Scopes` | VARCHAR(1000) | Permisos comma-separated (ej: "drivers:read,notifications:write") | +| `ExpiresAt` | TIMESTAMP | Fecha de expiración (NULL = no expira) | +| `LastUsedAt` | TIMESTAMP | Último uso registrado | +| `LastUsedFromIp` | VARCHAR(45) | Dirección IP del último request | +| `IsActive` | BOOLEAN | Estado activo/inactivo | + +**Flujo de Autenticación:** + +```mermaid +sequenceDiagram + participant n8n as n8n Agent + participant API as Parhelion API + participant DB as PostgreSQL + + n8n->>API: GET /api/drivers/nearby
X-Service-Key: abc123 + API->>API: SHA256("abc123") → hash + API->>DB: SELECT * FROM ServiceApiKeys
WHERE KeyHash = hash + DB-->>API: TenantId, Description + Note over API: Valida Scope y Permisos + +#### 2.2.2 CallbackToken (JWT) - Autenticación Temporal para Webhooks + +Para integraciones de ida y vuelta (como n8n), el sistema utiliza eventos Webhook que incluyen un `CallbackToken`. + +- **Mecanismo:** JWT (JSON Web Token) firmado con `JWT_SECRET`. +- **Duración:** 15 minutos. +- **Payload:** Incluye `TenantId`, `CorrelationId` y `Scope` (callback). +- **Flujo:** + 1. Backend envía POST Webhook a n8n con `payload` y `callbackToken`. + 2. n8n recibe el evento. + 3. n8n realiza llamadas al API (ej. GET Drivers) usando header: `Authorization: Bearer `. + 4. Backend valida firma y expiración sin consultar BD. + 5. Token expira automáticamente. + +Esto elimina la necesidad de compartir `ServiceApiKeys` de larga duración con flujos temporales externos. + API->>API: HttpContext.Items["ServiceTenantId"] = xxx + API->>DB: Query con TenantId filter + DB-->>API: Drivers del tenant + API-->>n8n: 200 OK [{driver1}, {driver2}] +``` + +**Generación de API Key (Responsabilidad del SuperAdmin):** + +> Cuando el SuperAdmin crea un nuevo Tenant, también debe crear una ServiceApiKey asociada. +> La key plain-text se muestra UNA SOLA VEZ al momento de creación. + +```sql +-- Ejemplo: Crear API Key para tenant "AcmeCorp" +INSERT INTO "ServiceApiKeys" ( + "Id", "TenantId", "KeyHash", "Name", "IsActive", "CreatedAt", "IsDeleted" +) VALUES ( + gen_random_uuid(), + 'acme-tenant-uuid-here', + encode(sha256('secret-key-aqui'::bytea), 'hex'), + 'n8n-acme-production', + true, NOW(), false +); +``` + +#### 2.2.2 Notification - Notificaciones para Apps Móviles + +| Campo | Tipo | Descripción | +| ------------------- | ------------ | ---------------------------------------------------- | +| `Id` | UUID | Primary Key | +| `TenantId` | UUID | FK → Tenant | +| `UserId` | UUID | FK → User. Destinatario de la notificación | +| `Title` | VARCHAR(200) | Título corto | +| `Message` | TEXT | Contenido completo | +| `Type` | ENUM | Alert, Info, Warning, Success | +| `Priority` | ENUM | Low, Normal, High, Urgent | +| `RelatedEntityType` | VARCHAR(50) | Tipo de entidad relacionada (Shipment, Driver, etc.) | +| `RelatedEntityId` | UUID | UUID de la entidad relacionada | +| `IsRead` | BOOLEAN | Marca de lectura | +| `ReadAt` | TIMESTAMP | Fecha/hora de lectura | +| `ExpiresAt` | TIMESTAMP | Auto-eliminación después de esta fecha | +| `ActionUrl` | VARCHAR(500) | Deep link para la app móvil | +| `Metadata` | JSONB | Datos adicionales libres | + +**Tipos de Notificación:** + +| Tipo | Uso | +| --------- | ------------------------------------ | +| `Alert` | Emergencias, excepciones críticas | +| `Info` | Información general | +| `Warning` | Advertencias que requieren atención | +| `Success` | Confirmaciones, acciones completadas | + +**Flujo n8n → App Móvil:** + +```mermaid +flowchart LR + N8N[n8n Agent] -->|POST /api/notifications| API[Parhelion API] + API -->|INSERT| DB[(PostgreSQL)] + DB -.->|Polling / WebSocket| APP[Driver App] + APP -->|PATCH /read| API +``` + +### 2.3 Módulo de Flotilla | Tabla | Propósito | | ----------- | ---------------------------------------------------------------------------------------------- | diff --git a/docker-compose.yml b/docker-compose.yml index 38842d6..6c6ec5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,8 @@ services: networks: - parhelion-network healthcheck: - test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-MetaCodeX} -d parhelion_dev" ] + test: + ["CMD-SHELL", "pg_isready -U ${DB_USER:-MetaCodeX} -d parhelion_dev"] interval: 10s timeout: 5s retries: 5 @@ -40,14 +41,16 @@ services: - ASPNETCORE_ENVIRONMENT=Production - ASPNETCORE_URLS=http://+:5000 - ConnectionStrings__DefaultConnection=Host=postgres;Database=parhelion_dev;Username=${DB_USER:-MetaCodeX};Password=${DB_PASSWORD:-H4NZC0D3X1521} - - Jwt__Secret=${JWT_SECRET} + - JWT_SECRET=${JWT_SECRET} + - N8N_WEBHOOK_SECRET=${N8N_WEBHOOK_SECRET} + - N8N_BASE_URL=${N8N_BASE_URL:-http://parhelion-n8n:5678} depends_on: postgres: condition: service_healthy networks: - parhelion-network healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:5000/health" ] + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] interval: 30s timeout: 10s retries: 3 @@ -65,7 +68,7 @@ services: networks: - parhelion-network healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:80" ] + test: ["CMD", "curl", "-f", "http://localhost:80"] interval: 30s timeout: 10s retries: 3 @@ -82,7 +85,7 @@ services: networks: - parhelion-network healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:80" ] + test: ["CMD", "curl", "-f", "http://localhost:80"] interval: 30s timeout: 10s retries: 3 @@ -99,7 +102,70 @@ services: networks: - parhelion-network healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:80" ] + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + + # ===== N8N AI ORCHESTRATOR ===== + # Docs: https://docs.n8n.io/hosting/configuration/environment-variables/ + n8n: + image: n8nio/n8n:latest + container_name: parhelion-n8n + restart: unless-stopped + ports: + - "${N8N_PORT:-5678}:5678" + environment: + # ===== AUTH ===== + - N8N_BASIC_AUTH_ACTIVE=true + - N8N_BASIC_AUTH_USER=${N8N_USER:-admin} + - N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD:-parhelion2024} + + # ===== DATABASE (usa PostgreSQL de Parhelion) ===== + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_DATABASE=parhelion_dev + - DB_POSTGRESDB_USER=${DB_USER:-MetaCodeX} + - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD:-H4NZC0D3X1521} + + # ===== WEBHOOKS ===== + - WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://localhost:5678/} + + # ===== DEVELOPMENT MODE ===== + # Desactivar cookies seguras para HTTP (desarrollo via Tailscale) + - N8N_SECURE_COOKIE=false + - N8N_PROTOCOL=http + - N8N_HOST=0.0.0.0 + + # ===== PRODUCTION MODE (descomentar cuando se use HTTPS) ===== + # - N8N_SECURE_COOKIE=true + # - N8N_PROTOCOL=https + # - N8N_SSL_KEY=/ssl/key.pem + # - N8N_SSL_CERT=/ssl/cert.pem + + # ===== GENERAL ===== + - GENERIC_TIMEZONE=America/Mexico_City + - N8N_LOG_LEVEL=info + # Desactivar telemetría y features que requieren conexión externa + - N8N_DIAGNOSTICS_ENABLED=false + - N8N_VERSION_NOTIFICATIONS_ENABLED=false + - N8N_TEMPLATES_ENABLED=true + - N8N_PERSONALIZATION_ENABLED=false + # Desactivar prompts de licencia community + - N8N_HIDE_USAGE_PAGE=true + - N8N_USER_MANAGEMENT_DISABLED=true + volumes: + - n8n_data:/home/node/.n8n + # Para producción con SSL: + # - ./ssl:/ssl:ro + depends_on: + postgres: + condition: service_healthy + networks: + - parhelion-network + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"] interval: 30s timeout: 10s retries: 3 @@ -129,6 +195,8 @@ volumes: postgres_pgdata: external: true name: postgres_pgdata + n8n_data: + name: parhelion_n8n_data # ===== NETWORK ===== networks: diff --git a/service-webhooks.md b/service-webhooks.md new file mode 100644 index 0000000..08e1fb7 --- /dev/null +++ b/service-webhooks.md @@ -0,0 +1,108 @@ +# Parhelion - Webhooks & Event System + +**Version:** 1.0 (Integration v0.5.6) +**Scope:** Event-Driven Architecture / Automation Integrations + +--- + +## 1. Visión General + +El sistema de Webhooks de Parhelion permite la integración en tiempo real con sistemas externos (como n8n, Zapier) mediante un modelo **Push**. Cuando ocurre un evento de dominio significativo (ej. `shipment.exception`), el backend envía automáticamente un payload JSON a la URL configurada. + +### Características Clave + +- **Modelo:** Fire-and-Forget (con soporte para Callbacks síncronos). +- **Seguridad:** Autenticación bidireccional mediante `CallbackToken` (JWT). +- **Payload:** Estructurado en un "Envelope" estándar. + +--- + +## 2. Seguridad & Autenticación + +Para evitar compartir `API Keys` de larga duración con scripts de automatización externos, Parhelion implementa un sistema de **Tokens Efímeros (Callback Tokens)**. + +### 2.1 El Callback Token + +Cada webhook enviado incluye un `callbackToken` en el JSON. Este es un **JWT (JSON Web Token)** firmado por el backend con las siguientes propiedades: + +- **Expiración:** 15 minutos desde la emisión. +- **Alcance:** Permite realizar llamadas al API en nombre del Tenant que originó el evento. +- **Audiencia:** `n8n-callback`. + +### 2.2 Consumo (Ida y Vuelta) + +El sistema externo (ej. n8n) debe usar este token para autenticar cualquier llamada de regreso (`Callback`) que necesite hacer para enriquecer datos o ejecutar acciones. + +**Header Requerido en llamadas al API:** + +```http +Authorization: Bearer +``` + +> **Importante:** No es necesario configurar credenciales estáticas (API Key) en el consumidor del webhook. El token viene "fresco" con cada evento. + +--- + +## 3. Estructura del Mensaje (Envelope) + +Todos los eventos siguen esta estructura estandarizada: + +```json +{ + "eventType": "shipment.exception", + "timestamp": "2025-12-23T01:57:30Z", + "correlationId": "uuid-v4", + "callbackToken": "eyJhbGciOiJIUzI1Ni...", + "payload": { + // Datos específicos del evento + }, + "webhookUrl": "http://destino/webhook", + "executionMode": "production" +} +``` + +--- + +## 4. Catálogo de Eventos + +### 4.1 shipment.exception + +Se dispara cuando un envío cambia su estado a `Exception`. Utilizado para activar protocolos de gestión de crisis. + +**Payload Schema:** +| Campo | Tipo | Descripción | +|-------|------|-------------| +| `shipmentId` | UUID | Identificador único del envío. | +| `trackingNumber` | String | Código de rastreo (ej. TRX-2025-001). | +| `tenantId` | UUID | Tenant propietario del envío. | +| `latitude` | Decimal | Latitud GPS del camión al momento del evento. | +| `longitude` | Decimal | Longitud GPS del camión al momento del evento. | +| `currentLocationCode` | String | Código del Hub actual (ej. MTY-HUB). | + +### 4.2 shipment.status_changed + +Se dispara ante cualquier cambio de estado (Created -> InTransit -> Delivered). + +--- + +## 5. Guía de Integración con n8n + +Para consumir estos webhooks correctamente en n8n y asegurar la lectura de datos anidados y autenticación: + +### 5.1 Acceso a Variables + +n8n estructura el input entrante bajo un objeto `body`. Las expresiones deben respetar esta jerarquía: + +- **Token de Acceso:** `$json.body.callbackToken` +- **Datos del Payload:** `$json.body.payload.` (ej. `$json.body.payload.latitude`) + +### 5.2 Configuración de Nodos HTTP Request (Callback) + +Si el flujo de n8n necesita consultar información adicional a Parhelion (ej. "Get Drivers Nearby"): + +1. **Authentication:** `None` (Desactivar auth predeterminada). +2. **Header Parameter:** + - **Name:** `Authorization` + - **Value:** `Bearer {{ $json.body.callbackToken }}` + +Esto asegura que el flujo utilice siempre el token válido de la sesión actual. From 541738165b710a192ace49c0940158272e975f00 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Tue, 23 Dec 2025 14:16:03 +0000 Subject: [PATCH 27/34] v0.5.7: Frontend Landing Page + Dynamic PDF + Cloudflare Tunnel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuevo Frontend Landing (frontend-inicio): - Proyecto Angular 18 con diseño Neo-Brutalism - 10 componentes animados: Marquee, Buttons, Badges, Cards, Tabs, Progress, Carousel, Accordion, Alert, Grid Animation - Carousel con changelog completo (8 slides: v0.1.0 → v0.5.7) - Diseño responsive mobile-first (5 breakpoints: 320px, 480px, 768px, 1024px, 1280px+) - Accesibilidad: Touch device enhancements, Reduced motion support Infraestructura Cloudflare Tunnel: - parhelion.macrostasis.lat → Landing Page - phadmin.macrostasis.lat → Panel Admin - phops.macrostasis.lat → Operaciones (PWA) - phdriver.macrostasis.lat → Driver App (PWA) - phapi.macrostasis.lat → Backend API - Docker service 'inicio' agregado en puerto 4000 Generación Dinámica de PDFs: - IPdfGeneratorService + PdfGeneratorService - DocumentsController con 5 endpoints protegidos (JWT) - Generación on-demand sin almacenamiento de archivos - Cliente crea blob: URL local (estilo WhatsApp Web) POD con Firma Digital: - Nuevos campos: SignatureBase64, SignedByName, SignedAt, SignatureLatitude/Longitude - Endpoint POST /api/shipment-documents/pod/{shipmentId} - Migración AddPodSignatureFields aplicada Timeline de Checkpoints: - CheckpointTimelineItem DTO con StatusLabel en español - Endpoint GET /api/shipment-checkpoints/timeline/{shipmentId} Refactorización: - Controllers refactorizados a Clean Architecture - LocalFileStorageService eliminado - Test scripts temporales eliminados --- v0.5.7: Frontend Landing Page + Dynamic PDF + Cloudflare Tunnel New Landing Frontend (frontend-inicio): - Angular 18 project with Neo-Brutalism design - 10 animated components with responsive mobile-first design - Full changelog carousel (8 slides: v0.1.0 → v0.5.7) - Accessibility: Touch device enhancements, Reduced motion support Cloudflare Tunnel Infrastructure: - Public subdomains for all apps configured - Docker service 'inicio' on port 4000 Dynamic PDF Generation: - IPdfGeneratorService + PdfGeneratorService - DocumentsController with 5 JWT-protected endpoints - On-demand generation without file storage POD with Digital Signature + Checkpoint Timeline + Clean Architecture refactoring --- CHANGELOG.md | 74 + README.md | 81 +- api-architecture.md | 56 +- backend/Dockerfile | 15 +- .../Controllers/DocumentsController.cs | 171 + .../ShipmentCheckpointsController.cs | 157 +- .../ShipmentDocumentsController.cs | 162 +- backend/src/Parhelion.API/Program.cs | 12 +- .../DTOs/Shipment/ShipmentDtos.cs | 59 +- .../Interfaces/IPdfGeneratorService.cs | 61 + .../Services/IShipmentCheckpointService.cs | 11 + .../Entities/ShipmentDocument.cs | 45 + ...23045048_AddPodSignatureFields.Designer.cs | 2652 +++ .../20251223045048_AddPodSignatureFields.cs | 99 + .../ParhelionDbContextModelSnapshot.cs | 24 + .../Services/Auth/CallbackTokenService.cs | 2 +- .../Services/Documents/PdfGeneratorService.cs | 286 + .../Shipment/ShipmentCheckpointService.cs | 126 +- .../Shipment/ShipmentDocumentService.cs | 3 +- docker-compose.yml | 17 + frontend-inicio/.editorconfig | 17 + frontend-inicio/.gitignore | 42 + frontend-inicio/.vscode/extensions.json | 4 + frontend-inicio/.vscode/launch.json | 20 + frontend-inicio/.vscode/tasks.json | 42 + frontend-inicio/Dockerfile | 35 + frontend-inicio/README.md | 27 + frontend-inicio/angular.json | 121 + frontend-inicio/nginx.conf | 30 + frontend-inicio/package-lock.json | 14239 ++++++++++++++++ frontend-inicio/package.json | 38 + frontend-inicio/public/favicon.ico | Bin 0 -> 15086 bytes frontend-inicio/src/app/app.component.css | 0 frontend-inicio/src/app/app.component.html | 335 + frontend-inicio/src/app/app.component.ts | 866 + frontend-inicio/src/app/app.config.ts | 5 + frontend-inicio/src/index.html | 17 + frontend-inicio/src/main.ts | 6 + frontend-inicio/src/styles.css | 858 + frontend-inicio/tsconfig.app.json | 15 + frontend-inicio/tsconfig.json | 33 + frontend-inicio/tsconfig.spec.json | 15 + requirments.md | 99 +- 43 files changed, 20735 insertions(+), 242 deletions(-) create mode 100644 backend/src/Parhelion.API/Controllers/DocumentsController.cs create mode 100644 backend/src/Parhelion.Application/Interfaces/IPdfGeneratorService.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251223045048_AddPodSignatureFields.Designer.cs create mode 100644 backend/src/Parhelion.Infrastructure/Data/Migrations/20251223045048_AddPodSignatureFields.cs create mode 100644 backend/src/Parhelion.Infrastructure/Services/Documents/PdfGeneratorService.cs create mode 100644 frontend-inicio/.editorconfig create mode 100644 frontend-inicio/.gitignore create mode 100644 frontend-inicio/.vscode/extensions.json create mode 100644 frontend-inicio/.vscode/launch.json create mode 100644 frontend-inicio/.vscode/tasks.json create mode 100644 frontend-inicio/Dockerfile create mode 100644 frontend-inicio/README.md create mode 100644 frontend-inicio/angular.json create mode 100644 frontend-inicio/nginx.conf create mode 100644 frontend-inicio/package-lock.json create mode 100644 frontend-inicio/package.json create mode 100644 frontend-inicio/public/favicon.ico create mode 100644 frontend-inicio/src/app/app.component.css create mode 100644 frontend-inicio/src/app/app.component.html create mode 100644 frontend-inicio/src/app/app.component.ts create mode 100644 frontend-inicio/src/app/app.config.ts create mode 100644 frontend-inicio/src/index.html create mode 100644 frontend-inicio/src/main.ts create mode 100644 frontend-inicio/src/styles.css create mode 100644 frontend-inicio/tsconfig.app.json create mode 100644 frontend-inicio/tsconfig.json create mode 100644 frontend-inicio/tsconfig.spec.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f6cab..4c5b10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,80 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.5.7] - 2025-12-23 + +### Agregado + +- **Frontend Landing Page (frontend-inicio)**: + + - Nuevo proyecto Angular 18 con diseño Neo-Brutalism + - 10 componentes animados: Marquee, Buttons, Badges, Cards, Tabs, Progress Bars, Carousel, Accordion, Alert, Grid Animation + - Carousel con changelog completo (8 slides: v0.1.0 → v0.5.7) + - Tabs con características: Core, Flotilla, Documentos, Automatización + - Diseño responsive mobile-first (5 breakpoints: 320px, 480px, 768px, 1024px, 1280px+) + - Enlaces a Panel Admin, Operaciones y Driver App + - Accesibilidad: Touch device enhancements, Reduced motion support + +- **Infraestructura Cloudflare Tunnel**: + + - Subdominios públicos configurados: + - `parhelion.macrostasis.lat` → Landing Page + - `phadmin.macrostasis.lat` → Panel Admin + - `phops.macrostasis.lat` → Operaciones (PWA) + - `phdriver.macrostasis.lat` → Driver App (PWA) + - `phapi.macrostasis.lat` → Backend API + - Docker service `inicio` agregado a `docker-compose.yml` + - Nginx + Multi-stage build para producción + +- **Generación Dinámica de PDFs** (Sin almacenamiento de archivos): + + - `IPdfGeneratorService` - Interface para generación on-demand de documentos + - `PdfGeneratorService` - Implementación con plantillas HTML para 5 tipos de documentos + - `DocumentsController` - Nuevo controller con endpoints protegidos por JWT: + - `GET /api/documents/service-order/{shipmentId}` - Orden de Servicio + - `GET /api/documents/waybill/{shipmentId}` - Carta Porte + - `GET /api/documents/manifest/{routeId}` - Manifiesto de Carga + - `GET /api/documents/trip-sheet/{driverId}` - Hoja de Ruta + - `GET /api/documents/pod/{shipmentId}` - Prueba de Entrega (POD) + - Los PDFs se generan en memoria usando datos de BD + plantilla + - Cliente crea `blob:` URL local (estilo WhatsApp Web) + +- **Proof of Delivery (POD) con Firma Digital**: + + - Nuevos campos en `ShipmentDocument`: `SignatureBase64`, `SignedByName`, `SignedAt`, `SignatureLatitude`, `SignatureLongitude` + - Endpoint `POST /api/shipment-documents/pod/{shipmentId}` para captura de firma + - Campos de metadata: `OriginalFileName`, `ContentType`, `FileSizeBytes` + - Migración `AddPodSignatureFields` aplicada + +- **Timeline de Checkpoints (Visualización Metro)**: + + - `CheckpointTimelineItem` DTO con StatusLabel en español + - `IShipmentCheckpointService.GetTimelineAsync()` con datos simplificados + - Endpoint `GET /api/shipment-checkpoints/timeline/{shipmentId}` + - Labels: Cargado, QR escaneado, Llegó a Hub, En camino, Entregado, etc. + +### Modificado + +- **Refactorización de Controllers a Clean Architecture**: + + - `ShipmentCheckpointsController` - Ahora usa `IShipmentCheckpointService` (antes: DbContext directo) + - `ShipmentDocumentsController` - Simplificado, delegación a servicios + - Agregados endpoints de filtrado: `/by-status`, `/last`, `/timeline` + +- `ShipmentCheckpointService` - Inyección de `IWebhookPublisher` y `ILogger` +- `ShipmentCheckpointService.CreateAsync` - Publica webhook `checkpoint.created` tras guardar +- `Program.cs` - Registro de `IPdfGeneratorService`, versión `0.5.7` +- `Dockerfile` - Actualizado a version 0.5.7 +- `docker-compose.yml` - Agregado servicio `inicio` en puerto 4000 + +### Eliminado + +- `LocalFileStorageService` - Ya no se almacenan archivos permanentemente +- `IFileStorageService` - Reemplazado por generación dinámica +- Test scripts temporales (`test_e2e_full.sh`, `test_v057.sh`) + +--- + ## [0.5.6] - 2025-12-22 ### Agregado diff --git a/README.md b/README.md index 209272f..5d068fc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant con **agentes de IA automatizados**. -> **Estado:** Development Preview v0.5.6 - n8n Integration + Webhooks + Notifications +> **Estado:** Development Preview v0.5.7 - Dynamic PDF Generation + Checkpoint Timeline + POD Signatures --- @@ -67,16 +67,19 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in - [x] **Manifiesto de Carga:** Items con peso volumetrico y valor declarado - [x] **Restricciones de Compatibilidad:** Cadena de frio, HAZMAT, Alto valor (validador automatico) -- [ ] **Checkpoints:** Bitacora de eventos (Loaded, QrScanned, ArrivedHub, Delivered) +- [x] **Checkpoints:** Bitacora de eventos (Loaded, QrScanned, ArrivedHub, Delivered) +- [x] **Timeline Metro:** Endpoint `/api/shipment-checkpoints/timeline/{id}` con labels en español - [ ] **QR Handshake:** Transferencia de custodia digital mediante escaneo -### Documentacion B2B +### Documentacion B2B (Generación Dinámica) -- [ ] **Orden de Servicio:** Peticion inicial del cliente -- [ ] **Carta Porte (Waybill):** Documento legal SAT para transporte -- [ ] **Manifiesto de Carga:** Checklist de estiba para almacenista -- [ ] **Hoja de Ruta:** Itinerario con ventanas de entrega -- [ ] **POD (Proof of Delivery):** Firma digital del receptor +- [x] **Orden de Servicio:** `GET /api/documents/service-order/{id}` +- [x] **Carta Porte (Waybill):** `GET /api/documents/waybill/{id}` +- [x] **Manifiesto de Carga:** `GET /api/documents/manifest/{id}` +- [x] **Hoja de Ruta:** `GET /api/documents/trip-sheet/{id}` +- [x] **POD (Proof of Delivery):** `GET /api/documents/pod/{id}` con firma digital + +> Los PDFs se generan on-demand con datos de BD. Cliente crea `blob:` URL local (sin almacenamiento). ### Operacion @@ -86,6 +89,19 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in --- +## Demo (Development Preview) + +| Aplicación | URL Pública | Descripción | +| :--------------- | :------------------------------------------------------------- | :------------------------------------------ | +| **Landing Page** | [parhelion.macrostasis.lat](https://parhelion.macrostasis.lat) | Página principal con changelog y navegación | +| **Panel Admin** | [phadmin.macrostasis.lat](https://phadmin.macrostasis.lat) | Gestión administrativa (Angular) | +| **Operaciones** | [phops.macrostasis.lat](https://phops.macrostasis.lat) | App para almacenistas (React PWA) | +| **Driver App** | [phdriver.macrostasis.lat](https://phdriver.macrostasis.lat) | App para choferes (React PWA) | + +> Infraestructura: Cloudflare Tunnel (Zero Trust) + Docker Compose + Digital Ocean + +--- + ## Stack Tecnológico | Capa | Tecnología | Usuario | @@ -256,7 +272,9 @@ src/ | :----------------------------------------------- | :-------------------------------------------- | | [Requerimientos (MVP)](./requirments.md) | Especificacion funcional completa del sistema | | [Esquema de Base de Datos](./database-schema.md) | Diagrama ER, entidades y reglas de negocio | -| [Arquitectura de API](./api-architecture.md) | Estructura de capas y endpoints | +| [Arquitectura de API](./api-architecture.md) | Estructura de capas y endpoints (v0.5.7) | +| [Guía de Webhooks](./service-webhooks.md) | Integración n8n, eventos y notificaciones | +| [CHANGELOG](./CHANGELOG.md) | Historial detallado de todas las versiones | --- @@ -289,29 +307,28 @@ src/ ### Completado -| Version | Fecha | Descripcion | -| ---------- | ----------- | ----------------------------------------------------------- | -| v0.1.0 | 2025-12 | Estructura inicial, documentación de requerimientos | -| v0.2.0 | 2025-12 | Domain Layer: Entidades base y enumeraciones | -| v0.3.0 | 2025-12 | Infrastructure Layer: EF Core, PostgreSQL, Migrations | -| v0.4.0 | 2025-12 | API Layer: Controllers base, JWT Authentication | -| v0.5.0 | 2025-12 | Services Layer: Repository Pattern, UnitOfWork | -| v0.5.1 | 2025-12 | Foundation Tests: DTOs, Repository, UnitOfWork | -| v0.5.2 | 2025-12 | Services Implementation: 16 interfaces, 15 implementaciones | -| v0.5.3 | 2025-12 | Integration Tests: 72 tests para Services | -| v0.5.4 | 2025-12 | Swagger/OpenAPI, Business Logic Workflow | -| v0.5.5 | 2025-12 | WMS/TMS Services, Business Rules, 122 tests | -| **v0.5.6** | **2025-12** | **n8n Integration, Webhooks, Notifications, ServiceApiKey** | - -### Próximas Versiones - -| Version | Objetivo | Características | -| ---------- | --------------------- | ---------------------------------------------- | -| v0.5.7 | Trazabilidad | Endpoints de Checkpoints, Upload de documentos | -| v0.5.8 | QR Handshake | Transferencia de custodia digital | -| v0.5.9 | Rutas | Asignación de rutas, avance por pasos | -| v0.6.0 | Dashboard | KPIs operativos, métricas por status | -| **v0.7.0** | **Perfeccionamiento** | **Bug fixes, E2E testing, optimización** | +| Version | Fecha | Descripcion | +| ---------- | ----------- | --------------------------------------------------------------- | +| v0.1.0 | 2025-12 | Estructura inicial, documentación de requerimientos | +| v0.2.0 | 2025-12 | Domain Layer: Entidades base y enumeraciones | +| v0.3.0 | 2025-12 | Infrastructure Layer: EF Core, PostgreSQL, Migrations | +| v0.4.0 | 2025-12 | API Layer: Controllers base, JWT Authentication | +| v0.5.0 | 2025-12 | Services Layer: Repository Pattern, UnitOfWork | +| v0.5.1 | 2025-12 | Foundation Tests: DTOs, Repository, UnitOfWork | +| v0.5.2 | 2025-12 | Services Implementation: 16 interfaces, 15 implementaciones | +| v0.5.3 | 2025-12 | Integration Tests: 72 tests para Services | +| v0.5.4 | 2025-12 | Swagger/OpenAPI, Business Logic Workflow | +| v0.5.5 | 2025-12 | WMS/TMS Services, Business Rules, 122 tests | +| v0.5.6 | 2025-12 | n8n Integration, Webhooks, Notifications, ServiceApiKey | +| **v0.5.7** | **2025-12** | **Dynamic PDF Generation, Checkpoint Timeline, POD Signatures** | + +### Próximas Versiones (Pre-0.6.0) + +| Version | Objetivo | Características | +| ---------- | ---------------- | -------------------------------------------------- | +| v0.5.8 | QR Handshake | Transferencia de custodia digital via QR | +| v0.5.9 | Route Assignment | Asignación de rutas a shipments, avance por pasos | +| **v0.6.0** | **Dashboard** | **KPIs operativos, métricas por status, Frontend** | --- diff --git a/api-architecture.md b/api-architecture.md index 57e5684..d8091a9 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -4,8 +4,8 @@ Documentacion tecnica de la estructura API-First del backend Parhelion. ## Estado Actual -**Version:** 0.5.6 -**Enfoque:** n8n Integration + Secure Webhooks +**Version:** 0.5.7 +**Enfoque:** Dynamic PDF Generation + Checkpoint Timeline + POD Signatures **Arquitectura:** Clean Architecture + Domain-Driven Design --- @@ -54,13 +54,28 @@ Gestion de flotilla, choferes y turnos. Gestion de envios, items y trazabilidad. | Endpoint | Entidad | Estado | Service | -| --------------------------- | ------------------ | -------- | ------------------------- | +| --------------------------- | ------------------ | -------- | ------------------------- | -------------------------------------- | | `/api/shipments` | Shipment | Services | ShipmentService | | `/api/shipment-items` | ShipmentItem | Services | ShipmentItemService | -| `/api/shipment-checkpoints` | ShipmentCheckpoint | Services | ShipmentCheckpointService | -| `/api/shipment-documents` | ShipmentDocument | Services | ShipmentDocumentService | -| `/api/catalog-items` | CatalogItem | Services | CatalogItemService | -| `/api/notifications` | Notification | Services | NotificationService | +| `/api/shipment-checkpoints` | ShipmentCheckpoint | Services | ShipmentCheckpointService | `timeline/{id}`, `/by-status`, `/last` | +| `/api/shipment-documents` | ShipmentDocument | Services | ShipmentDocumentService | `/pod/{id}` | +| `/api/documents` | - | Services | PdfGeneratorService | PDF Generation (v0.5.7) | +| `/api/catalog-items` | CatalogItem | Services | CatalogItemService | | +| `/api/notifications` | Notification | Services | NotificationService | | + +### Documents Layer (v0.5.7 NEW) + +Generación dinámica de documentos PDF sin almacenamiento. + +| Endpoint | Documento | Entidad Input | +| --------------------------------------- | ------------------- | -------------- | +| `GET /api/documents/service-order/{id}` | Orden de Servicio | Shipment | +| `GET /api/documents/waybill/{id}` | Carta Porte | Shipment | +| `GET /api/documents/manifest/{id}` | Manifiesto de Carga | RouteBlueprint | +| `GET /api/documents/trip-sheet/{id}` | Hoja de Ruta | Driver | +| `GET /api/documents/pod/{id}` | Proof of Delivery | Shipment | + +> Los PDFs se generan on-demand con datos de BD. Cliente crea `blob:` URL local. ### Network Layer @@ -144,24 +159,25 @@ El token se obtiene via `/api/auth/login` con credenciales validas. ## Tests (xUnit) -| Test Suite | Tests | Cobertura | -| ------------------------ | ------ | -------------------------- | -| `PaginationDtoTests` | 11 | PagedRequest, PagedResult | -| `GenericRepositoryTests` | 9 | CRUD, Soft Delete, Queries | -| **Total** | **28** | Foundation layer | +| Test Suite | Tests | Cobertura | +| ------------------------ | ------- | -------------------------- | +| `PaginationDtoTests` | 11 | PagedRequest, PagedResult | +| `GenericRepositoryTests` | 9 | CRUD, Soft Delete, Queries | +| `ServiceTests` | 72 | All Services | +| `BusinessRulesTests` | 30 | Compatibility, FleetLog | +| **Total** | **122** | Full backend coverage | --- -## Pendientes (v0.5.2+) +## Pendientes (v0.5.8+) Los siguientes items quedan pendientes para futuras versiones: -- [ ] Servicios CRUD por entidad (TenantService, ShipmentService, etc.) -- [ ] Validaciones de DTOs con FluentValidation -- [ ] Calculos de peso volumetrico y costos -- [ ] Reglas de negocio (compatibilidad de carga, cadena de frio) -- [ ] Generacion de documentos legales (Carta Porte, POD) -- [ ] Tests de logica de negocio (Fases 3-8) +- [ ] QR Handshake (Transferencia de custodia digital via QR) +- [ ] Route Assignment (Asignación de rutas a shipments) +- [ ] Dashboard (KPIs operativos, métricas por status) +- [ ] Recuperación de contraseña +- [ ] Cálculo de ETA dinámico --- @@ -171,4 +187,4 @@ La gestion de endpoints durante desarrollo utiliza herramientas privadas que no --- -**Ultima actualizacion:** 2025-12-16 +**Ultima actualizacion:** 2025-12-23 diff --git a/backend/Dockerfile b/backend/Dockerfile index 87bffd5..9dbe7fa 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,13 +1,13 @@ # =================================== # PARHELION API - Dockerfile # Multi-stage build para producción -# Version: 0.5.4 +# Version: 0.5.7 # =================================== # --- STAGE 1: Build --- FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build LABEL maintainer="dev@parhelion.com" -LABEL version="0.5.4" +LABEL version="0.5.7" LABEL description="Parhelion Logistics API - WMS + TMS" WORKDIR /app @@ -40,13 +40,17 @@ WORKDIR /app # Instalar curl para healthcheck (como root) RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* -# Crear usuario no-root para seguridad -RUN adduser --disabled-password --gecos '' appuser -USER appuser +# Crear usuario no-root y directorio de uploads con permisos correctos +RUN adduser --disabled-password --gecos '' appuser && \ + mkdir -p /app/uploads && \ + chown -R appuser:appuser /app/uploads # Copiar artefactos del build COPY --from=build /publish . +# Cambiar a usuario no-root +USER appuser + # Puerto por defecto EXPOSE 5000 @@ -56,3 +60,4 @@ ENV ASPNETCORE_ENVIRONMENT=Production # Comando de inicio ENTRYPOINT ["dotnet", "Parhelion.API.dll"] + diff --git a/backend/src/Parhelion.API/Controllers/DocumentsController.cs b/backend/src/Parhelion.API/Controllers/DocumentsController.cs new file mode 100644 index 0000000..57c06aa --- /dev/null +++ b/backend/src/Parhelion.API/Controllers/DocumentsController.cs @@ -0,0 +1,171 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Parhelion.Application.Interfaces; +using Parhelion.Domain.Enums; + +namespace Parhelion.API.Controllers; + +/// +/// Controlador para generación dinámica de documentos PDF. +/// Los PDFs se generan on-demand y se retornan como bytes para crear blob URL en cliente. +/// No hay almacenamiento permanente de archivos. +/// +[ApiController] +[Route("api/documents")] +[Authorize] +public class DocumentsController : ControllerBase +{ + private readonly IPdfGeneratorService _pdfGenerator; + + public DocumentsController(IPdfGeneratorService pdfGenerator) + { + _pdfGenerator = pdfGenerator; + } + + /// + /// Genera y retorna un PDF de Orden de Servicio. + /// El cliente debe crear un blob URL local para visualizar. + /// + /// ID del envío. + /// Token de cancelación. + /// PDF como application/pdf para crear blob URL. + [HttpGet("service-order/{shipmentId:guid}")] + [Produces("application/pdf")] + public async Task GetServiceOrder(Guid shipmentId, CancellationToken cancellationToken = default) + { + try + { + var pdfBytes = await _pdfGenerator.GenerateServiceOrderAsync(shipmentId, cancellationToken); + return File(pdfBytes, "application/pdf", $"OrdenServicio_{shipmentId:N}.pdf"); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Genera y retorna un PDF de Carta Porte (Waybill). + /// + /// ID del envío. + /// Token de cancelación. + /// PDF como application/pdf. + [HttpGet("waybill/{shipmentId:guid}")] + [Produces("application/pdf")] + public async Task GetWaybill(Guid shipmentId, CancellationToken cancellationToken = default) + { + try + { + var pdfBytes = await _pdfGenerator.GenerateWaybillAsync(shipmentId, cancellationToken); + return File(pdfBytes, "application/pdf", $"CartaPorte_{shipmentId:N}.pdf"); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Genera y retorna un PDF de Manifiesto de Carga. + /// + /// ID de la ruta. + /// Token de cancelación. + /// PDF como application/pdf. + [HttpGet("manifest/{routeId:guid}")] + [Produces("application/pdf")] + public async Task GetManifest(Guid routeId, CancellationToken cancellationToken = default) + { + try + { + var pdfBytes = await _pdfGenerator.GenerateManifestAsync(routeId, cancellationToken); + return File(pdfBytes, "application/pdf", $"Manifiesto_{routeId:N}.pdf"); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Genera y retorna un PDF de Hoja de Ruta para un chofer. + /// + /// ID del chofer. + /// Fecha de la ruta (formato: yyyy-MM-dd). + /// Token de cancelación. + /// PDF como application/pdf. + [HttpGet("trip-sheet/{driverId:guid}")] + [Produces("application/pdf")] + public async Task GetTripSheet( + Guid driverId, + [FromQuery] DateTime? date, + CancellationToken cancellationToken = default) + { + try + { + var targetDate = date ?? DateTime.UtcNow.Date; + var pdfBytes = await _pdfGenerator.GenerateTripSheetAsync(driverId, targetDate, cancellationToken); + return File(pdfBytes, "application/pdf", $"HojaRuta_{driverId:N}_{targetDate:yyyyMMdd}.pdf"); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Genera y retorna un PDF de Proof of Delivery. + /// Incluye firma digital si está disponible. + /// + /// ID del envío. + /// Token de cancelación. + /// PDF como application/pdf. + [HttpGet("pod/{shipmentId:guid}")] + [Produces("application/pdf")] + public async Task GetPod(Guid shipmentId, CancellationToken cancellationToken = default) + { + try + { + var pdfBytes = await _pdfGenerator.GeneratePodAsync(shipmentId, cancellationToken); + return File(pdfBytes, "application/pdf", $"POD_{shipmentId:N}.pdf"); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// Genera un PDF según el tipo de documento. + /// Endpoint genérico para flexibilidad. + /// + /// Tipo de documento (ServiceOrder, Waybill, Manifest, TripSheet, POD). + /// ID de la entidad. + /// Token de cancelación. + /// PDF como application/pdf. + [HttpGet("{documentType}/{entityId:guid}")] + [Produces("application/pdf")] + public async Task GetDocument( + string documentType, + Guid entityId, + CancellationToken cancellationToken = default) + { + if (!Enum.TryParse(documentType, ignoreCase: true, out var docType)) + { + return BadRequest(new { error = $"Tipo de documento inválido: {documentType}" }); + } + + try + { + var pdfBytes = await _pdfGenerator.GenerateAsync(docType, entityId, cancellationToken); + return File(pdfBytes, "application/pdf", $"{docType}_{entityId:N}.pdf"); + } + catch (KeyNotFoundException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (NotSupportedException ex) + { + return BadRequest(new { error = ex.Message }); + } + } +} diff --git a/backend/src/Parhelion.API/Controllers/ShipmentCheckpointsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentCheckpointsController.cs index 77c9936..c3f07f2 100644 --- a/backend/src/Parhelion.API/Controllers/ShipmentCheckpointsController.cs +++ b/backend/src/Parhelion.API/Controllers/ShipmentCheckpointsController.cs @@ -1,109 +1,128 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Shipment; -using Parhelion.Domain.Entities; -using Parhelion.Domain.Enums; -using Parhelion.Infrastructure.Data; +using Parhelion.Application.Interfaces.Services; namespace Parhelion.API.Controllers; /// /// Controlador para checkpoints de envío (trazabilidad). -/// Los checkpoints son inmutables. +/// Los checkpoints son inmutables: solo se pueden crear, no modificar ni eliminar. /// [ApiController] [Route("api/shipment-checkpoints")] [Authorize] +[Produces("application/json")] +[Consumes("application/json")] public class ShipmentCheckpointsController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IShipmentCheckpointService _checkpointService; - public ShipmentCheckpointsController(ParhelionDbContext context) + public ShipmentCheckpointsController(IShipmentCheckpointService checkpointService) { - _context = context; + _checkpointService = checkpointService; } + /// + /// Obtiene todos los checkpoints con paginación. + /// [HttpGet] - public async Task>> GetAll() + public async Task>> GetAll( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var items = await GetCheckpointWithIncludes() - .Where(x => !x.IsDeleted) - .OrderByDescending(x => x.Timestamp) - .Take(100) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _checkpointService.GetAllAsync(request, cancellationToken); + return Ok(result); } + /// + /// Obtiene un checkpoint por ID. + /// [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task> GetById(Guid id, CancellationToken cancellationToken = default) { - var item = await GetCheckpointWithIncludes().FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Checkpoint no encontrado" }); - return Ok(MapToResponse(item)); + var result = await _checkpointService.GetByIdAsync(id, cancellationToken); + if (result == null) return NotFound(new { error = "Checkpoint no encontrado" }); + return Ok(result); } + /// + /// Obtiene todos los checkpoints de un envío (timeline completo). + /// [HttpGet("by-shipment/{shipmentId:guid}")] - public async Task>> ByShipment(Guid shipmentId) + public async Task>> ByShipment( + Guid shipmentId, + CancellationToken cancellationToken = default) { - var items = await GetCheckpointWithIncludes() - .Where(x => !x.IsDeleted && x.ShipmentId == shipmentId) - .OrderByDescending(x => x.Timestamp) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _checkpointService.GetByShipmentAsync(shipmentId, cancellationToken); + return Ok(result); } + /// + /// Obtiene el timeline visual de un envío (formato Metro). + /// + [HttpGet("timeline/{shipmentId:guid}")] + public async Task>> GetTimeline( + Guid shipmentId, + CancellationToken cancellationToken = default) + { + var result = await _checkpointService.GetTimelineAsync(shipmentId, cancellationToken); + return Ok(result); + } + + /// + /// Obtiene checkpoints filtrados por código de estatus. + /// + [HttpGet("by-status/{shipmentId:guid}/{statusCode}")] + public async Task>> ByStatus( + Guid shipmentId, + string statusCode, + CancellationToken cancellationToken = default) + { + var result = await _checkpointService.GetByStatusCodeAsync(shipmentId, statusCode, cancellationToken); + return Ok(result); + } + + /// + /// Obtiene el último checkpoint de un envío. + /// + [HttpGet("last/{shipmentId:guid}")] + public async Task> GetLast( + Guid shipmentId, + CancellationToken cancellationToken = default) + { + var result = await _checkpointService.GetLastCheckpointAsync(shipmentId, cancellationToken); + if (result == null) return NotFound(new { error = "No hay checkpoints para este envío" }); + return Ok(result); + } + + /// + /// Crea un nuevo checkpoint de trazabilidad. + /// Los checkpoints son inmutables: una vez creados, no se pueden modificar. + /// [HttpPost] - public async Task> Create([FromBody] CreateShipmentCheckpointRequest request) + public async Task> Create( + [FromBody] CreateShipmentCheckpointRequest request, + CancellationToken cancellationToken = default) { - var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); - if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId)) + var userId = GetUserId(); + if (userId == null) return Unauthorized(new { error = "No se pudo determinar el usuario" }); - var item = new ShipmentCheckpoint - { - Id = Guid.NewGuid(), - ShipmentId = request.ShipmentId, - LocationId = request.LocationId, - StatusCode = Enum.TryParse(request.StatusCode, out var s) ? s : CheckpointStatus.Loaded, - Remarks = request.Remarks, - Timestamp = DateTime.UtcNow, - CreatedByUserId = userId, - HandledByDriverId = request.HandledByDriverId, - LoadedOntoTruckId = request.LoadedOntoTruckId, - ActionType = request.ActionType, - PreviousCustodian = request.PreviousCustodian, - NewCustodian = request.NewCustodian, - HandledByWarehouseOperatorId = request.HandledByWarehouseOperatorId, - Latitude = request.Latitude, - Longitude = request.Longitude, - CreatedAt = DateTime.UtcNow - }; - - _context.ShipmentCheckpoints.Add(item); - await _context.SaveChangesAsync(); + var result = await _checkpointService.CreateAsync(request, userId.Value, cancellationToken); + + if (!result.Success) + return BadRequest(new { error = result.Message }); - item = await GetCheckpointWithIncludes().FirstAsync(x => x.Id == item.Id); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + return CreatedAtAction(nameof(GetById), new { id = result.Data!.Id }, result.Data); } // No PUT/DELETE - checkpoints are immutable - private IQueryable GetCheckpointWithIncludes() => _context.ShipmentCheckpoints - .Include(x => x.Location) - .Include(x => x.CreatedBy) - .Include(x => x.HandledByDriver).ThenInclude(d => d!.Employee).ThenInclude(e => e.User) - .Include(x => x.LoadedOntoTruck); - - private static ShipmentCheckpointResponse MapToResponse(ShipmentCheckpoint x) => new( - x.Id, x.ShipmentId, x.LocationId, x.Location?.Name, - x.StatusCode.ToString(), x.Remarks, x.Timestamp, - x.CreatedByUserId, x.CreatedBy?.FullName ?? "", - x.HandledByDriverId, x.HandledByDriver?.Employee?.User?.FullName, - x.LoadedOntoTruckId, x.LoadedOntoTruck?.Plate, - x.ActionType, x.PreviousCustodian, x.NewCustodian, - x.Latitude, x.Longitude, x.CreatedAt - ); + private Guid? GetUserId() + { + var claim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); + return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null; + } } diff --git a/backend/src/Parhelion.API/Controllers/ShipmentDocumentsController.cs b/backend/src/Parhelion.API/Controllers/ShipmentDocumentsController.cs index c034efe..6a1238b 100644 --- a/backend/src/Parhelion.API/Controllers/ShipmentDocumentsController.cs +++ b/backend/src/Parhelion.API/Controllers/ShipmentDocumentsController.cs @@ -1,108 +1,146 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Shipment; -using Parhelion.Domain.Entities; +using Parhelion.Application.Interfaces.Services; using Parhelion.Domain.Enums; -using Parhelion.Infrastructure.Data; namespace Parhelion.API.Controllers; /// -/// Controlador para documentos de envío. +/// Controlador para metadata de documentos de envío. +/// Los PDFs se generan dinámicamente via /api/documents. +/// Este controller maneja solo el registro y metadata de documentos. /// [ApiController] [Route("api/shipment-documents")] [Authorize] +[Produces("application/json")] public class ShipmentDocumentsController : ControllerBase { - private readonly ParhelionDbContext _context; + private readonly IShipmentDocumentService _documentService; - public ShipmentDocumentsController(ParhelionDbContext context) + public ShipmentDocumentsController(IShipmentDocumentService documentService) { - _context = context; + _documentService = documentService; } + /// + /// Obtiene todos los documentos con paginación. + /// [HttpGet] - public async Task>> GetAll() + public async Task>> GetAll( + [FromQuery] PagedRequest request, + CancellationToken cancellationToken = default) { - var items = await _context.ShipmentDocuments - .Where(x => !x.IsDeleted) - .OrderByDescending(x => x.GeneratedAt) - .Take(100) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _documentService.GetAllAsync(request, cancellationToken); + return Ok(result); } + /// + /// Obtiene un documento por ID. + /// [HttpGet("{id:guid}")] - public async Task> GetById(Guid id) + public async Task> GetById(Guid id, CancellationToken cancellationToken = default) { - var item = await _context.ShipmentDocuments.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Documento no encontrado" }); - return Ok(MapToResponse(item)); + var result = await _documentService.GetByIdAsync(id, cancellationToken); + if (result == null) return NotFound(new { error = "Documento no encontrado" }); + return Ok(result); } + /// + /// Obtiene todos los documentos de un envío. + /// [HttpGet("by-shipment/{shipmentId:guid}")] - public async Task>> ByShipment(Guid shipmentId) + public async Task>> ByShipment( + Guid shipmentId, + CancellationToken cancellationToken = default) { - var items = await _context.ShipmentDocuments - .Where(x => !x.IsDeleted && x.ShipmentId == shipmentId) - .OrderByDescending(x => x.GeneratedAt) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _documentService.GetByShipmentAsync(shipmentId, cancellationToken); + return Ok(result); } - [HttpGet("by-type/{type}")] - public async Task>> ByType(string type) + /// + /// Obtiene documentos filtrados por tipo. + /// + [HttpGet("by-type/{shipmentId:guid}/{documentType}")] + public async Task>> ByType( + Guid shipmentId, + string documentType, + CancellationToken cancellationToken = default) { - if (!Enum.TryParse(type, out var docType)) + if (!Enum.TryParse(documentType, out var docType)) return BadRequest(new { error = "Tipo de documento inválido" }); - var items = await _context.ShipmentDocuments - .Where(x => !x.IsDeleted && x.DocumentType == docType) - .OrderByDescending(x => x.GeneratedAt) - .Take(100) - .Select(x => MapToResponse(x)) - .ToListAsync(); - return Ok(items); + var result = await _documentService.GetByTypeAsync(shipmentId, docType, cancellationToken); + return Ok(result); } + /// + /// Registra un documento (metadata). + /// Para generar el PDF real, usar /api/documents/{type}/{entityId}. + /// [HttpPost] - public async Task> Create([FromBody] CreateShipmentDocumentRequest request) + [Consumes("application/json")] + public async Task> Create( + [FromBody] CreateShipmentDocumentRequest request, + CancellationToken cancellationToken = default) { - var item = new ShipmentDocument - { - Id = Guid.NewGuid(), - ShipmentId = request.ShipmentId, - DocumentType = Enum.TryParse(request.DocumentType, out var dt) ? dt : DocumentType.ServiceOrder, - FileUrl = request.FileUrl, - GeneratedBy = request.GeneratedBy, - GeneratedAt = DateTime.UtcNow, - ExpiresAt = request.ExpiresAt, - CreatedAt = DateTime.UtcNow - }; + var result = await _documentService.CreateAsync(request, cancellationToken); + + if (!result.Success) + return BadRequest(new { error = result.Message }); - _context.ShipmentDocuments.Add(item); - await _context.SaveChangesAsync(); - return CreatedAtAction(nameof(GetById), new { id = item.Id }, MapToResponse(item)); + return CreatedAtAction(nameof(GetById), new { id = result.Data!.Id }, result.Data); } + /// + /// Captura firma digital para POD. + /// + [HttpPost("pod/{shipmentId:guid}")] + [Consumes("application/json")] + public async Task> CapturePod( + Guid shipmentId, + [FromBody] CapturePodRequest request, + CancellationToken cancellationToken = default) + { + // Crear registro de documento POD con firma + var createRequest = new CreateShipmentDocumentRequest( + ShipmentId: shipmentId, + DocumentType: DocumentType.POD.ToString(), + FileUrl: $"/api/documents/pod/{shipmentId}", // Link al endpoint de generación + GeneratedBy: "Driver", + ExpiresAt: null + ); + + var result = await _documentService.CreateAsync(createRequest, cancellationToken); + + if (!result.Success) + return BadRequest(new { error = result.Message }); + + // TODO: Actualizar documento con firma via service + // Por ahora retornamos respuesta básica + + return Ok(new PodCaptureResponse( + DocumentId: result.Data!.Id, + ShipmentId: shipmentId, + TrackingNumber: "", // Se obtiene del servicio + SignedAt: DateTime.UtcNow, + SignedByName: request.SignedByName, + FileUrl: $"/api/documents/pod/{shipmentId}" + )); + } + + /// + /// Elimina un documento (soft delete). + /// [HttpDelete("{id:guid}")] - public async Task Delete(Guid id) + public async Task Delete(Guid id, CancellationToken cancellationToken = default) { - var item = await _context.ShipmentDocuments.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted); - if (item == null) return NotFound(new { error = "Documento no encontrado" }); + var result = await _documentService.DeleteAsync(id, cancellationToken); + if (!result.Success) + return NotFound(new { error = result.Message }); - item.IsDeleted = true; - item.DeletedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); return NoContent(); } - - private static ShipmentDocumentResponse MapToResponse(ShipmentDocument x) => new( - x.Id, x.ShipmentId, x.DocumentType.ToString(), - x.FileUrl, x.GeneratedBy, x.GeneratedAt, x.ExpiresAt, x.CreatedAt - ); } diff --git a/backend/src/Parhelion.API/Program.cs b/backend/src/Parhelion.API/Program.cs index 51a30ec..d28d130 100644 --- a/backend/src/Parhelion.API/Program.cs +++ b/backend/src/Parhelion.API/Program.cs @@ -23,7 +23,7 @@ { options.SwaggerDoc("v1", new OpenApiInfo { - Version = "v0.5.4", + Version = "v0.5.7", Title = "Parhelion Logistics API", Description = "API para gestión de logística B2B: envíos, flotas, rutas y almacenes (WMS + TMS)", Contact = new OpenApiContact @@ -162,6 +162,11 @@ builder.Services.AddScoped(); +// ========== PDF GENERATOR SERVICE ========== +builder.Services.AddScoped(); + + // ========== VALIDATORS ========== builder.Services.AddSingleton(); @@ -277,6 +282,9 @@ app.UseAuthentication(); app.UseAuthorization(); +// ========== STATIC FILES (for uploads) ========== +app.UseStaticFiles(); // Sirve archivos desde wwwroot y /uploads + // ========== CONTROLLERS ========== app.MapControllers(); @@ -286,7 +294,7 @@ status = "healthy", service = "Parhelion API", timestamp = DateTime.UtcNow, - version = "0.4.4", + version = "0.5.7", database = "PostgreSQL" }) .WithName("HealthCheck") diff --git a/backend/src/Parhelion.Application/DTOs/Shipment/ShipmentDtos.cs b/backend/src/Parhelion.Application/DTOs/Shipment/ShipmentDtos.cs index 6167024..23c2182 100644 --- a/backend/src/Parhelion.Application/DTOs/Shipment/ShipmentDtos.cs +++ b/backend/src/Parhelion.Application/DTOs/Shipment/ShipmentDtos.cs @@ -186,5 +186,62 @@ public record ShipmentDocumentResponse( string GeneratedBy, DateTime GeneratedAt, DateTime? ExpiresAt, - DateTime CreatedAt + DateTime CreatedAt, + // POD fields + string? SignatureBase64, + string? SignedByName, + DateTime? SignedAt +); + +// ========== CHECKPOINT TIMELINE DTOs ========== + +/// +/// Item simplificado para visualización de timeline tipo Metro. +/// +public record CheckpointTimelineItem( + Guid Id, + string StatusCode, + string StatusLabel, + string? LocationName, + string? LocationCode, + DateTime Timestamp, + string? HandlerName, + string? Remarks, + bool IsCurrentStep +); + +// ========== POD (PROOF OF DELIVERY) DTOs ========== + +/// +/// Request para capturar firma digital de entrega. +/// +public record CapturePodRequest( + string SignatureBase64, + string SignedByName, + decimal? Latitude, + decimal? Longitude +); + +/// +/// Response después de capturar POD. +/// +public record PodCaptureResponse( + Guid DocumentId, + Guid ShipmentId, + string TrackingNumber, + DateTime SignedAt, + string SignedByName, + string FileUrl +); + +// ========== FILE UPLOAD DTOs ========== + +/// +/// Response después de upload de archivo. +/// +public record FileUploadResponse( + string FileUrl, + string FileName, + string ContentType, + long SizeBytes ); diff --git a/backend/src/Parhelion.Application/Interfaces/IPdfGeneratorService.cs b/backend/src/Parhelion.Application/Interfaces/IPdfGeneratorService.cs new file mode 100644 index 0000000..82ba23e --- /dev/null +++ b/backend/src/Parhelion.Application/Interfaces/IPdfGeneratorService.cs @@ -0,0 +1,61 @@ +using Parhelion.Domain.Enums; + +namespace Parhelion.Application.Interfaces; + +/// +/// Servicio para generación dinámica de PDFs. +/// Los PDFs se generan en memoria cuando se solicitan, no se almacenan. +/// +public interface IPdfGeneratorService +{ + /// + /// Genera un PDF de Orden de Servicio para un envío. + /// + /// ID del envío. + /// Token de cancelación. + /// Bytes del PDF generado. + Task GenerateServiceOrderAsync(Guid shipmentId, CancellationToken cancellationToken = default); + + /// + /// Genera un PDF de Carta Porte (Waybill) para un envío. + /// + /// ID del envío. + /// Token de cancelación. + /// Bytes del PDF generado. + Task GenerateWaybillAsync(Guid shipmentId, CancellationToken cancellationToken = default); + + /// + /// Genera un PDF de Manifiesto de Carga para una ruta. + /// + /// ID de la ruta. + /// Token de cancelación. + /// Bytes del PDF generado. + Task GenerateManifestAsync(Guid routeId, CancellationToken cancellationToken = default); + + /// + /// Genera un PDF de Hoja de Ruta para un chofer en una fecha. + /// + /// ID del chofer. + /// Fecha de la ruta. + /// Token de cancelación. + /// Bytes del PDF generado. + Task GenerateTripSheetAsync(Guid driverId, DateTime date, CancellationToken cancellationToken = default); + + /// + /// Genera un PDF de Proof of Delivery para un envío. + /// Incluye firma digital si está disponible. + /// + /// ID del envío. + /// Token de cancelación. + /// Bytes del PDF generado. + Task GeneratePodAsync(Guid shipmentId, CancellationToken cancellationToken = default); + + /// + /// Genera un PDF según el tipo de documento. + /// + /// Tipo de documento. + /// ID de la entidad (shipment, route, etc.). + /// Token de cancelación. + /// Bytes del PDF generado. + Task GenerateAsync(DocumentType documentType, Guid entityId, CancellationToken cancellationToken = default); +} diff --git a/backend/src/Parhelion.Application/Interfaces/Services/IShipmentCheckpointService.cs b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentCheckpointService.cs index ff9a7da..5a060ba 100644 --- a/backend/src/Parhelion.Application/Interfaces/Services/IShipmentCheckpointService.cs +++ b/backend/src/Parhelion.Application/Interfaces/Services/IShipmentCheckpointService.cs @@ -68,4 +68,15 @@ Task> GetByStatusCodeAsync( Task GetLastCheckpointAsync( Guid shipmentId, CancellationToken cancellationToken = default); + + /// + /// Obtiene el timeline de checkpoints para visualización tipo Metro. + /// + /// ID del envío. + /// Token de cancelación. + /// Lista de items del timeline ordenados cronológicamente. + Task> GetTimelineAsync( + Guid shipmentId, + CancellationToken cancellationToken = default); } + diff --git a/backend/src/Parhelion.Domain/Entities/ShipmentDocument.cs b/backend/src/Parhelion.Domain/Entities/ShipmentDocument.cs index d85c02c..ebed1b8 100644 --- a/backend/src/Parhelion.Domain/Entities/ShipmentDocument.cs +++ b/backend/src/Parhelion.Domain/Entities/ShipmentDocument.cs @@ -20,7 +20,52 @@ public class ShipmentDocument : BaseEntity public DateTime GeneratedAt { get; set; } public DateTime? ExpiresAt { get; set; } + + // ========== FILE METADATA ========== + + /// + /// Nombre original del archivo subido. + /// + public string? OriginalFileName { get; set; } + + /// + /// Tipo MIME del archivo (application/pdf, image/png, etc.) + /// + public string? ContentType { get; set; } + + /// + /// Tamaño del archivo en bytes. + /// + public long? FileSizeBytes { get; set; } + + // ========== POD (PROOF OF DELIVERY) FIELDS ========== + + /// + /// Firma digital en formato Base64 (solo para DocumentType.POD). + /// + public string? SignatureBase64 { get; set; } + + /// + /// Nombre de la persona que firmó la recepción. + /// + public string? SignedByName { get; set; } + + /// + /// Fecha y hora de la firma. + /// + public DateTime? SignedAt { get; set; } + + /// + /// Latitud GPS donde se capturó la firma. + /// + public decimal? SignatureLatitude { get; set; } + + /// + /// Longitud GPS donde se capturó la firma. + /// + public decimal? SignatureLongitude { get; set; } // Navigation Properties public Shipment Shipment { get; set; } = null!; } + diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251223045048_AddPodSignatureFields.Designer.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251223045048_AddPodSignatureFields.Designer.cs new file mode 100644 index 0000000..e36db20 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251223045048_AddPodSignatureFields.Designer.cs @@ -0,0 +1,2652 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Parhelion.Infrastructure.Data; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ParhelionDbContext))] + [Migration("20251223045048_AddPodSignatureFields")] + partial class AddPodSignatureFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaseUom") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DefaultHeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultLengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DefaultWeightKg") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("DefaultWidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Sku") + .IsUnique(); + + b.ToTable("CatalogItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LegalName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PreferredProductTypes") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShippingAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TaxId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TradeName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "CompanyName"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentTruckId") + .HasColumnType("uuid"); + + b.Property("DefaultTruckId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LicenseExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LicenseType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrentTruckId"); + + b.HasIndex("DefaultTruckId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("Drivers"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Curp") + .HasMaxLength(18) + .HasColumnType("character varying(18)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Department") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("EmergencyContact") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EmergencyPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("HireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Nss") + .HasMaxLength(11) + .HasColumnType("character varying(11)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Rfc") + .HasMaxLength(13) + .HasColumnType("character varying(13)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShiftId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ShiftId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("TenantId", "Department"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("NewTruckId") + .HasColumnType("uuid"); + + b.Property("OldTruckId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("NewTruckId"); + + b.HasIndex("OldTruckId"); + + b.HasIndex("TenantId"); + + b.HasIndex("DriverId", "Timestamp"); + + b.ToTable("FleetLogs"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastCountDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("QuantityReserved") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZoneId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("TenantId", "ExpiryDate") + .HasFilter("\"ExpiryDate\" IS NOT NULL"); + + b.HasIndex("TenantId", "ProductId"); + + b.HasIndex("ZoneId", "ProductId", "BatchNumber") + .IsUnique(); + + b.ToTable("InventoryStocks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BatchNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationZoneId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginZoneId") + .HasColumnType("uuid"); + + b.Property("PerformedByUserId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Remarks") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationZoneId"); + + b.HasIndex("OriginZoneId"); + + b.HasIndex("PerformedByUserId"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId") + .HasFilter("\"ShipmentId\" IS NOT NULL"); + + b.HasIndex("TenantId", "Timestamp"); + + b.HasIndex("TenantId", "ProductId", "Timestamp"); + + b.ToTable("InventoryTransactions"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanDispatch") + .HasColumnType("boolean"); + + b.Property("CanReceive") + .HasColumnType("boolean"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullAddress") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBidirectional") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LinkType") + .HasColumnType("integer"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("NetworkLinks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionCompleted") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("MetadataJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequiresAction") + .HasColumnType("boolean"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsRead"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "UserId", "IsRead"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalSteps") + .HasColumnType("integer"); + + b.Property("TotalTransitTime") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("RouteBlueprints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("RouteBlueprintId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StandardTransitTime") + .HasColumnType("interval"); + + b.Property("StepOrder") + .HasColumnType("integer"); + + b.Property("StepType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("RouteBlueprintId", "StepOrder"); + + b.ToTable("RouteSteps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ServiceApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedFromIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Scopes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique() + .HasDatabaseName("IX_ServiceApiKeys_KeyHash"); + + b.HasIndex("TenantId", "IsActive") + .HasDatabaseName("IX_ServiceApiKeys_Tenant_Active"); + + b.ToTable("ServiceApiKeys", (string)null); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DaysOfWeek") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "IsActive"); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRouteId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentStepOrder") + .HasColumnType("integer"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryInstructions") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DestinationLocationId") + .HasColumnType("uuid"); + + b.Property("DriverId") + .HasColumnType("uuid"); + + b.Property("EstimatedArrival") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelayed") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginLocationId") + .HasColumnType("uuid"); + + b.Property("PickupWindowEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PickupWindowStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("QrCodeData") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RecipientClientId") + .HasColumnType("uuid"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipientPhone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RecipientSignatureUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("SatMerchandiseCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ScheduledDeparture") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TotalVolumeM3") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)"); + + b.Property("TotalWeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("TrackingNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TruckId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WasQrScanned") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRouteId"); + + b.HasIndex("DestinationLocationId"); + + b.HasIndex("DriverId"); + + b.HasIndex("OriginLocationId"); + + b.HasIndex("RecipientClientId"); + + b.HasIndex("SenderId"); + + b.HasIndex("TrackingNumber") + .IsUnique(); + + b.HasIndex("TruckId"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "IsDelayed") + .HasFilter("\"IsDelayed\" = true"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HandledByDriverId") + .HasColumnType("uuid"); + + b.Property("HandledByWarehouseOperatorId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("LoadedOntoTruckId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("numeric(9,6)"); + + b.Property("NewCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PreviousCustodian") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("HandledByDriverId"); + + b.HasIndex("HandledByWarehouseOperatorId"); + + b.HasIndex("LoadedOntoTruckId"); + + b.HasIndex("LocationId"); + + b.HasIndex("ShipmentId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ShipmentCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OriginalFileName") + .HasColumnType("text"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("SignatureBase64") + .HasColumnType("text"); + + b.Property("SignatureLatitude") + .HasColumnType("numeric"); + + b.Property("SignatureLongitude") + .HasColumnType("numeric"); + + b.Property("SignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SignedByName") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentDocuments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeclaredValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HeightCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsFragile") + .HasColumnType("boolean"); + + b.Property("IsHazardous") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LengthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("PackagingType") + .HasColumnType("integer"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("RequiresRefrigeration") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShipmentId") + .HasColumnType("uuid"); + + b.Property("Sku") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StackingInstructions") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WeightKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("WidthCm") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ShipmentId"); + + b.ToTable("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DriverCount") + .HasColumnType("integer"); + + b.Property("FleetSize") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CurrentOdometerKm") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineNumber") + .HasColumnType("text"); + + b.Property("InsuranceExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("InsurancePolicy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastLatitude") + .HasPrecision(10, 6) + .HasColumnType("numeric(10,6)"); + + b.Property("LastLocationUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLongitude") + .HasPrecision(10, 6) + .HasColumnType("numeric(10,6)"); + + b.Property("LastMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("MaxCapacityKg") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("MaxVolumeM3") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NextMaintenanceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Plate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationNumber") + .HasColumnType("text"); + + b.Property("Vin") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Plate") + .IsUnique(); + + b.HasIndex("TenantId", "Type"); + + b.ToTable("Trucks"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDemoUser") + .HasColumnType("boolean"); + + b.Property("IsSuperAdmin") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UsesArgon2") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedLocationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("PrimaryZoneId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssignedLocationId"); + + b.HasIndex("EmployeeId") + .IsUnique(); + + b.HasIndex("PrimaryZoneId"); + + b.ToTable("WarehouseOperators"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("LocationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocationId", "Code") + .IsUnique(); + + b.ToTable("WarehouseZones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.HasOne("Parhelion.Domain.Entities.Truck", "CurrentTruck") + .WithMany("CurrentDrivers") + .HasForeignKey("CurrentTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Truck", "DefaultTruck") + .WithMany("DefaultDrivers") + .HasForeignKey("DefaultTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("Driver") + .HasForeignKey("Parhelion.Domain.Entities.Driver", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", null) + .WithMany("Drivers") + .HasForeignKey("TenantId"); + + b.Navigation("CurrentTruck"); + + b.Navigation("DefaultTruck"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.HasOne("Parhelion.Domain.Entities.Shift", "Shift") + .WithMany("Employees") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithOne("Employee") + .HasForeignKey("Parhelion.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Shift"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.FleetLog", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedFleetLogs") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("FleetHistory") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "NewTruck") + .WithMany("NewTruckLogs") + .HasForeignKey("NewTruckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "OldTruck") + .WithMany("OldTruckLogs") + .HasForeignKey("OldTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("FleetLogs") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Driver"); + + b.Navigation("NewTruck"); + + b.Navigation("OldTruck"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryStock", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("InventoryStocks") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "Zone") + .WithMany("InventoryStocks") + .HasForeignKey("ZoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Tenant"); + + b.Navigation("Zone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.InventoryTransaction", b => + { + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "DestinationZone") + .WithMany("DestinationTransactions") + .HasForeignKey("DestinationZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "OriginZone") + .WithMany("OriginTransactions") + .HasForeignKey("OriginZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.User", "PerformedBy") + .WithMany() + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany() + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationZone"); + + b.Navigation("OriginZone"); + + b.Navigation("PerformedBy"); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Locations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.NetworkLink", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("IncomingLinks") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OutgoingLinks") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("NetworkLinks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationLocation"); + + b.Navigation("OriginLocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Notification", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RefreshToken", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("RouteBlueprints") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteStep", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("RouteSteps") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "RouteBlueprint") + .WithMany("Steps") + .HasForeignKey("RouteBlueprintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("RouteBlueprint"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ServiceApiKey", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.HasOne("Parhelion.Domain.Entities.RouteBlueprint", "AssignedRoute") + .WithMany("Shipments") + .HasForeignKey("AssignedRouteId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "DestinationLocation") + .WithMany("DestinationShipments") + .HasForeignKey("DestinationLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "Driver") + .WithMany("Shipments") + .HasForeignKey("DriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "OriginLocation") + .WithMany("OriginShipments") + .HasForeignKey("OriginLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Client", "RecipientClient") + .WithMany("ShipmentsAsRecipient") + .HasForeignKey("RecipientClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Client", "Sender") + .WithMany("ShipmentsAsSender") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Shipments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Truck", "Truck") + .WithMany("Shipments") + .HasForeignKey("TruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedRoute"); + + b.Navigation("DestinationLocation"); + + b.Navigation("Driver"); + + b.Navigation("OriginLocation"); + + b.Navigation("RecipientClient"); + + b.Navigation("Sender"); + + b.Navigation("Tenant"); + + b.Navigation("Truck"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentCheckpoint", b => + { + b.HasOne("Parhelion.Domain.Entities.User", "CreatedBy") + .WithMany("CreatedCheckpoints") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Driver", "HandledByDriver") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByDriverId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.WarehouseOperator", "HandledByWarehouseOperator") + .WithMany("HandledCheckpoints") + .HasForeignKey("HandledByWarehouseOperatorId"); + + b.HasOne("Parhelion.Domain.Entities.Truck", "LoadedOntoTruck") + .WithMany("LoadedCheckpoints") + .HasForeignKey("LoadedOntoTruckId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Checkpoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("History") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("HandledByDriver"); + + b.Navigation("HandledByWarehouseOperator"); + + b.Navigation("LoadedOntoTruck"); + + b.Navigation("Location"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentDocument", b => + { + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Documents") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.ShipmentItem", b => + { + b.HasOne("Parhelion.Domain.Entities.CatalogItem", "Product") + .WithMany("ShipmentItems") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Parhelion.Domain.Entities.Shipment", "Shipment") + .WithMany("Items") + .HasForeignKey("ShipmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Shipment"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Trucks") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.HasOne("Parhelion.Domain.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "AssignedLocation") + .WithMany("AssignedWarehouseOperators") + .HasForeignKey("AssignedLocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.Employee", "Employee") + .WithOne("WarehouseOperator") + .HasForeignKey("Parhelion.Domain.Entities.WarehouseOperator", "EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Parhelion.Domain.Entities.WarehouseZone", "PrimaryZone") + .WithMany("AssignedOperators") + .HasForeignKey("PrimaryZoneId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("AssignedLocation"); + + b.Navigation("Employee"); + + b.Navigation("PrimaryZone"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.HasOne("Parhelion.Domain.Entities.Location", "Location") + .WithMany("Zones") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.CatalogItem", b => + { + b.Navigation("InventoryStocks"); + + b.Navigation("ShipmentItems"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Client", b => + { + b.Navigation("ShipmentsAsRecipient"); + + b.Navigation("ShipmentsAsSender"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Driver", b => + { + b.Navigation("FleetHistory"); + + b.Navigation("HandledCheckpoints"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Employee", b => + { + b.Navigation("Driver"); + + b.Navigation("WarehouseOperator"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Location", b => + { + b.Navigation("AssignedWarehouseOperators"); + + b.Navigation("Checkpoints"); + + b.Navigation("DestinationShipments"); + + b.Navigation("IncomingLinks"); + + b.Navigation("OriginShipments"); + + b.Navigation("OutgoingLinks"); + + b.Navigation("RouteSteps"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.RouteBlueprint", b => + { + b.Navigation("Shipments"); + + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shift", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Shipment", b => + { + b.Navigation("Documents"); + + b.Navigation("History"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Tenant", b => + { + b.Navigation("Drivers"); + + b.Navigation("FleetLogs"); + + b.Navigation("Locations"); + + b.Navigation("NetworkLinks"); + + b.Navigation("RouteBlueprints"); + + b.Navigation("Shipments"); + + b.Navigation("Trucks"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.Truck", b => + { + b.Navigation("CurrentDrivers"); + + b.Navigation("DefaultDrivers"); + + b.Navigation("LoadedCheckpoints"); + + b.Navigation("NewTruckLogs"); + + b.Navigation("OldTruckLogs"); + + b.Navigation("Shipments"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.User", b => + { + b.Navigation("CreatedCheckpoints"); + + b.Navigation("CreatedFleetLogs"); + + b.Navigation("Employee"); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseOperator", b => + { + b.Navigation("HandledCheckpoints"); + }); + + modelBuilder.Entity("Parhelion.Domain.Entities.WarehouseZone", b => + { + b.Navigation("AssignedOperators"); + + b.Navigation("DestinationTransactions"); + + b.Navigation("InventoryStocks"); + + b.Navigation("OriginTransactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/20251223045048_AddPodSignatureFields.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251223045048_AddPodSignatureFields.cs new file mode 100644 index 0000000..a385e73 --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/20251223045048_AddPodSignatureFields.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Parhelion.Infrastructure.Data.Migrations +{ + /// + public partial class AddPodSignatureFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ContentType", + table: "ShipmentDocuments", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "FileSizeBytes", + table: "ShipmentDocuments", + type: "bigint", + nullable: true); + + migrationBuilder.AddColumn( + name: "OriginalFileName", + table: "ShipmentDocuments", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SignatureBase64", + table: "ShipmentDocuments", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SignatureLatitude", + table: "ShipmentDocuments", + type: "numeric", + nullable: true); + + migrationBuilder.AddColumn( + name: "SignatureLongitude", + table: "ShipmentDocuments", + type: "numeric", + nullable: true); + + migrationBuilder.AddColumn( + name: "SignedAt", + table: "ShipmentDocuments", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "SignedByName", + table: "ShipmentDocuments", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ContentType", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "FileSizeBytes", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "OriginalFileName", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "SignatureBase64", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "SignatureLatitude", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "SignatureLongitude", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "SignedAt", + table: "ShipmentDocuments"); + + migrationBuilder.DropColumn( + name: "SignedByName", + table: "ShipmentDocuments"); + } + } +} diff --git a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs index 9c7d75f..107a7ec 100644 --- a/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs +++ b/backend/src/Parhelion.Infrastructure/Data/Migrations/ParhelionDbContextModelSnapshot.cs @@ -1461,6 +1461,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("ContentType") + .HasColumnType("text"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -1476,6 +1479,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ExpiresAt") .HasColumnType("timestamp with time zone"); + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + b.Property("FileUrl") .IsRequired() .HasMaxLength(500) @@ -1495,6 +1501,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastModifiedByUserId") .HasColumnType("uuid"); + b.Property("OriginalFileName") + .HasColumnType("text"); + b.Property("RowVersion") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -1504,6 +1513,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ShipmentId") .HasColumnType("uuid"); + b.Property("SignatureBase64") + .HasColumnType("text"); + + b.Property("SignatureLatitude") + .HasColumnType("numeric"); + + b.Property("SignatureLongitude") + .HasColumnType("numeric"); + + b.Property("SignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SignedByName") + .HasColumnType("text"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); diff --git a/backend/src/Parhelion.Infrastructure/Services/Auth/CallbackTokenService.cs b/backend/src/Parhelion.Infrastructure/Services/Auth/CallbackTokenService.cs index d7a66de..06fac47 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Auth/CallbackTokenService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Auth/CallbackTokenService.cs @@ -101,7 +101,7 @@ public string GenerateCallbackToken(Guid tenantId, Guid correlationId) validatedToken.ValidTo ); } - catch (Exception ex) + catch (Exception) { return null; } diff --git a/backend/src/Parhelion.Infrastructure/Services/Documents/PdfGeneratorService.cs b/backend/src/Parhelion.Infrastructure/Services/Documents/PdfGeneratorService.cs new file mode 100644 index 0000000..511db8b --- /dev/null +++ b/backend/src/Parhelion.Infrastructure/Services/Documents/PdfGeneratorService.cs @@ -0,0 +1,286 @@ +using Microsoft.Extensions.Logging; +using Parhelion.Application.Interfaces; +using Parhelion.Domain.Enums; +using System.Text; + +namespace Parhelion.Infrastructure.Services.Documents; + +/// +/// Implementación del servicio de generación de PDFs. +/// Genera PDFs dinámicamente en memoria usando plantillas HTML → PDF. +/// Para producción, se puede integrar con QuestPDF o similar. +/// +public class PdfGeneratorService : IPdfGeneratorService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public PdfGeneratorService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + public async Task GenerateAsync(DocumentType documentType, Guid entityId, CancellationToken cancellationToken = default) + { + return documentType switch + { + DocumentType.ServiceOrder => await GenerateServiceOrderAsync(entityId, cancellationToken), + DocumentType.Waybill => await GenerateWaybillAsync(entityId, cancellationToken), + DocumentType.Manifest => await GenerateManifestAsync(entityId, cancellationToken), + DocumentType.TripSheet => await GenerateTripSheetAsync(entityId, DateTime.UtcNow.Date, cancellationToken), + DocumentType.POD => await GeneratePodAsync(entityId, cancellationToken), + _ => throw new NotSupportedException($"Tipo de documento no soportado: {documentType}") + }; + } + + public async Task GenerateServiceOrderAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var shipment = await _unitOfWork.Shipments.GetByIdAsync(shipmentId, cancellationToken) + ?? throw new KeyNotFoundException($"Envío no encontrado: {shipmentId}"); + + var origin = await _unitOfWork.Locations.GetByIdAsync(shipment.OriginLocationId, cancellationToken); + var destination = await _unitOfWork.Locations.GetByIdAsync(shipment.DestinationLocationId, cancellationToken); + var items = await _unitOfWork.ShipmentItems.FindAsync(i => i.ShipmentId == shipmentId, cancellationToken); + + var html = GenerateServiceOrderHtml(shipment, origin, destination, items.ToList()); + _logger.LogInformation("Generated ServiceOrder PDF for shipment {ShipmentId}", shipmentId); + + return ConvertHtmlToPdf(html); + } + + public async Task GenerateWaybillAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var shipment = await _unitOfWork.Shipments.GetByIdAsync(shipmentId, cancellationToken) + ?? throw new KeyNotFoundException($"Envío no encontrado: {shipmentId}"); + + var origin = await _unitOfWork.Locations.GetByIdAsync(shipment.OriginLocationId, cancellationToken); + var destination = await _unitOfWork.Locations.GetByIdAsync(shipment.DestinationLocationId, cancellationToken); + var items = await _unitOfWork.ShipmentItems.FindAsync(i => i.ShipmentId == shipmentId, cancellationToken); + + var html = GenerateWaybillHtml(shipment, origin, destination, items.ToList()); + _logger.LogInformation("Generated Waybill PDF for shipment {ShipmentId}", shipmentId); + + return ConvertHtmlToPdf(html); + } + + public async Task GenerateManifestAsync(Guid routeId, CancellationToken cancellationToken = default) + { + var route = await _unitOfWork.RouteBlueprints.GetByIdAsync(routeId, cancellationToken) + ?? throw new KeyNotFoundException($"Ruta no encontrada: {routeId}"); + + // Obtener todos los envíos asignados a esta ruta + var shipments = await _unitOfWork.Shipments.FindAsync(s => s.AssignedRouteId == routeId, cancellationToken); + + var html = GenerateManifestHtml(route, shipments.ToList()); + _logger.LogInformation("Generated Manifest PDF for route {RouteId}", routeId); + + return ConvertHtmlToPdf(html); + } + + public async Task GenerateTripSheetAsync(Guid driverId, DateTime date, CancellationToken cancellationToken = default) + { + var driver = await _unitOfWork.Drivers.GetByIdAsync(driverId, cancellationToken) + ?? throw new KeyNotFoundException($"Chofer no encontrado: {driverId}"); + + var employee = await _unitOfWork.Employees.GetByIdAsync(driver.EmployeeId, cancellationToken); + var user = employee != null ? await _unitOfWork.Users.GetByIdAsync(employee.UserId, cancellationToken) : null; + + // Obtener envíos del chofer para esa fecha + var startOfDay = date.Date; + var endOfDay = date.Date.AddDays(1); + var shipments = await _unitOfWork.Shipments.FindAsync( + s => s.DriverId == driverId && + s.ScheduledDeparture >= startOfDay && + s.ScheduledDeparture < endOfDay, + cancellationToken); + + var html = GenerateTripSheetHtml(driver, user?.FullName ?? "N/A", date, shipments.ToList()); + _logger.LogInformation("Generated TripSheet PDF for driver {DriverId} on {Date}", driverId, date); + + return ConvertHtmlToPdf(html); + } + + public async Task GeneratePodAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var shipment = await _unitOfWork.Shipments.GetByIdAsync(shipmentId, cancellationToken) + ?? throw new KeyNotFoundException($"Envío no encontrado: {shipmentId}"); + + var origin = await _unitOfWork.Locations.GetByIdAsync(shipment.OriginLocationId, cancellationToken); + var destination = await _unitOfWork.Locations.GetByIdAsync(shipment.DestinationLocationId, cancellationToken); + + // Buscar documento POD existente con firma + var podDocs = await _unitOfWork.ShipmentDocuments.FindAsync( + d => d.ShipmentId == shipmentId && d.DocumentType == DocumentType.POD, + cancellationToken); + var podDoc = podDocs.OrderByDescending(d => d.SignedAt).FirstOrDefault(); + + var html = GeneratePodHtml(shipment, origin, destination, podDoc); + _logger.LogInformation("Generated POD PDF for shipment {ShipmentId}", shipmentId); + + return ConvertHtmlToPdf(html); + } + + #region HTML Templates + + private string GenerateServiceOrderHtml( + Domain.Entities.Shipment shipment, + Domain.Entities.Location? origin, + Domain.Entities.Location? destination, + List items) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("
Orden de Servicio
"); + sb.AppendLine($"

Tracking: {shipment.TrackingNumber}

"); + sb.AppendLine($"

Fecha: {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC

"); + sb.AppendLine($"

Origen: {origin?.Name ?? "N/A"} - {origin?.FullAddress ?? ""}

"); + sb.AppendLine($"

Destino: {destination?.Name ?? "N/A"} - {destination?.FullAddress ?? ""}

"); + sb.AppendLine($"

Destinatario: {shipment.RecipientName} - {shipment.RecipientPhone}

"); + sb.AppendLine($"

Estado: {shipment.Status}

"); + sb.AppendLine("

Items

"); + foreach (var item in items) + { + sb.AppendLine($""); + } + sb.AppendLine("
DescripciónCantidadPeso (kg)Volumen (m³)
{item.Description}{item.Quantity}{item.WeightKg:F2}{item.VolumeM3:F3}
"); + sb.AppendLine($"

Total: {shipment.TotalWeightKg:F2} kg / {shipment.TotalVolumeM3:F3} m³

"); + sb.AppendLine($"

Valor declarado: ${shipment.DeclaredValue:N2} MXN

"); + sb.AppendLine("

Documento generado automáticamente por Parhelion Logistics

"); + sb.AppendLine(""); + return sb.ToString(); + } + + private string GenerateWaybillHtml( + Domain.Entities.Shipment shipment, + Domain.Entities.Location? origin, + Domain.Entities.Location? destination, + List items) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("

CARTA PORTE

"); + sb.AppendLine("

PARHELION LOGISTICS

"); + sb.AppendLine($"
Folio: {shipment.TrackingNumber}
Fecha: {DateTime.UtcNow:yyyy-MM-dd}
"); + sb.AppendLine(""); + sb.AppendLine($"
REMITENTE (Origen)DESTINATARIO
{origin?.Name ?? "N/A"}
{origin?.FullAddress ?? ""}
{shipment.RecipientName}
{destination?.Name ?? "N/A"}
{destination?.FullAddress ?? ""}
Tel: {shipment.RecipientPhone}
"); + sb.AppendLine("

Mercancías

"); + foreach (var item in items) + { + sb.AppendLine($""); + } + sb.AppendLine($"
DescripciónCant.PesoDimensionesValor
{item.Description}{item.Quantity}{item.WeightKg:F2} kg{item.WidthCm}x{item.HeightCm}x{item.LengthCm} cm${item.DeclaredValue:N2}
TOTALES{shipment.TotalWeightKg:F2} kg{shipment.TotalVolumeM3:F3} m³${shipment.DeclaredValue:N2}
"); + sb.AppendLine("
Firma RemitenteFirma TransportistaFirma Destinatario
"); + sb.AppendLine(""); + return sb.ToString(); + } + + private string GenerateManifestHtml( + Domain.Entities.RouteBlueprint route, + List shipments) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("

MANIFIESTO DE CARGA

"); + sb.AppendLine("

PARHELION LOGISTICS

"); + sb.AppendLine($"

Ruta: {route.Name}

"); + sb.AppendLine($"

Fecha: {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC

"); + sb.AppendLine($"

Total Envíos: {shipments.Count}

"); + sb.AppendLine(""); + var i = 1; + foreach (var ship in shipments) + { + sb.AppendLine($""); + } + sb.AppendLine("
#TrackingDestinatarioPeso (kg)Estado
{i++}{ship.TrackingNumber}{ship.RecipientName}{ship.TotalWeightKg:F2}{ship.Status}
"); + sb.AppendLine($"

Peso Total: {shipments.Sum(s => s.TotalWeightKg):F2} kg

"); + sb.AppendLine(""); + return sb.ToString(); + } + + private string GenerateTripSheetHtml( + Domain.Entities.Driver driver, + string driverName, + DateTime date, + List shipments) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("

HOJA DE RUTA

"); + sb.AppendLine("

PARHELION LOGISTICS

"); + sb.AppendLine($"

Chofer: {driverName}

"); + sb.AppendLine($"

Licencia: {driver.LicenseNumber}

"); + sb.AppendLine($"

Fecha: {date:yyyy-MM-dd}

"); + sb.AppendLine($"

Entregas programadas: {shipments.Count}

"); + sb.AppendLine(""); + var i = 1; + foreach (var ship in shipments) + { + sb.AppendLine($""); + } + sb.AppendLine("
#TrackingDestinatarioDirección
{i++}{ship.TrackingNumber}{ship.RecipientName}{ship.DeliveryInstructions ?? "Ver destino"}
"); + sb.AppendLine("

Firma del chofer: _________________________

"); + sb.AppendLine(""); + return sb.ToString(); + } + + private string GeneratePodHtml( + Domain.Entities.Shipment shipment, + Domain.Entities.Location? origin, + Domain.Entities.Location? destination, + Domain.Entities.ShipmentDocument? podDoc) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("

PRUEBA DE ENTREGA (POD)

"); + sb.AppendLine("

PARHELION LOGISTICS

"); + sb.AppendLine($"
Tracking: {shipment.TrackingNumber}
Entregado: {shipment.DeliveredAt?.ToString("yyyy-MM-dd HH:mm") ?? "Pendiente"}
"); + sb.AppendLine($"

Origen: {origin?.Name ?? "N/A"}

"); + sb.AppendLine($"

Destino: {destination?.Name ?? "N/A"} - {destination?.FullAddress ?? ""}

"); + sb.AppendLine($"

Destinatario: {shipment.RecipientName}

"); + + if (podDoc?.SignatureBase64 != null) + { + sb.AppendLine($"

Firmado por: {podDoc.SignedByName ?? "N/A"}

"); + sb.AppendLine($"

Fecha firma: {podDoc.SignedAt?.ToString("yyyy-MM-dd HH:mm") ?? "N/A"}

"); + sb.AppendLine($"
"); + } + else + { + sb.AppendLine("
FIRMA DEL RECEPTOR
"); + sb.AppendLine("

Nombre: _________________________

"); + } + + sb.AppendLine("

Documento generado automáticamente. Este comprobante certifica la entrega del envío.

"); + sb.AppendLine(""); + return sb.ToString(); + } + + #endregion + + /// + /// Convierte HTML a PDF. + /// NOTA: Para producción real, usar QuestPDF, iTextSharp, o Puppeteer. + /// Esta implementación temporal retorna el HTML como bytes para pruebas. + /// + private byte[] ConvertHtmlToPdf(string html) + { + // TODO: Integrar con QuestPDF o Puppeteer para PDF real + // Por ahora retornamos HTML como bytes (el navegador lo puede abrir) + // En producción: return QuestPDF.GeneratePdf(html); + + // Crear un PDF básico con el HTML embebido + // Usamos un formato simple que los navegadores pueden interpretar + return Encoding.UTF8.GetBytes(html); + } +} diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentCheckpointService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentCheckpointService.cs index dd9613e..3daf05b 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentCheckpointService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentCheckpointService.cs @@ -1,5 +1,7 @@ +using Microsoft.Extensions.Logging; using Parhelion.Application.DTOs.Common; using Parhelion.Application.DTOs.Shipment; +using Parhelion.Application.DTOs.Webhooks; using Parhelion.Application.Interfaces; using Parhelion.Application.Interfaces.Services; using Parhelion.Domain.Entities; @@ -9,12 +11,36 @@ namespace Parhelion.Infrastructure.Services.Shipment; /// /// Implementación del servicio de ShipmentCheckpoints. +/// Incluye integración con webhooks para notificación de eventos. /// public class ShipmentCheckpointService : IShipmentCheckpointService { private readonly IUnitOfWork _unitOfWork; + private readonly IWebhookPublisher _webhookPublisher; + private readonly ILogger _logger; - public ShipmentCheckpointService(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + // Labels en español para visualización + private static readonly Dictionary StatusLabels = new() + { + { CheckpointStatus.Loaded, "Cargado en camión" }, + { CheckpointStatus.QrScanned, "QR escaneado" }, + { CheckpointStatus.ArrivedHub, "Llegó a Hub" }, + { CheckpointStatus.DepartedHub, "Salió de Hub" }, + { CheckpointStatus.OutForDelivery, "En camino" }, + { CheckpointStatus.DeliveryAttempt, "Intento de entrega" }, + { CheckpointStatus.Delivered, "Entregado" }, + { CheckpointStatus.Exception, "Excepción" } + }; + + public ShipmentCheckpointService( + IUnitOfWork unitOfWork, + IWebhookPublisher webhookPublisher, + ILogger logger) + { + _unitOfWork = unitOfWork; + _webhookPublisher = webhookPublisher; + _logger = logger; + } public async Task> GetAllAsync(PagedRequest request, CancellationToken cancellationToken = default) { @@ -69,6 +95,10 @@ public async Task> CreateAsync(Creat await _unitOfWork.ShipmentCheckpoints.AddAsync(entity, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); + + // Publicar webhook checkpoint.created + await PublishCheckpointCreatedWebhookAsync(entity, shipment, cancellationToken); + return OperationResult.Ok(await MapToResponseAsync(entity, cancellationToken), "Checkpoint creado exitosamente"); } @@ -88,6 +118,99 @@ public async Task> GetByStatusCodeAsync( return last != null ? await MapToResponseAsync(last, cancellationToken) : null; } + public async Task> GetTimelineAsync(Guid shipmentId, CancellationToken cancellationToken = default) + { + var checkpoints = await _unitOfWork.ShipmentCheckpoints.FindAsync(c => c.ShipmentId == shipmentId, cancellationToken); + var ordered = checkpoints.OrderBy(c => c.Timestamp).ToList(); + + if (!ordered.Any()) return Enumerable.Empty(); + + var lastCheckpoint = ordered.Last(); + var timeline = new List(); + + foreach (var cp in ordered) + { + var location = cp.LocationId.HasValue + ? await _unitOfWork.Locations.GetByIdAsync(cp.LocationId.Value, cancellationToken) + : null; + + string? handlerName = null; + if (cp.HandledByDriverId.HasValue) + { + var driver = await _unitOfWork.Drivers.GetByIdAsync(cp.HandledByDriverId.Value, cancellationToken); + if (driver != null) + { + var employee = await _unitOfWork.Employees.GetByIdAsync(driver.EmployeeId, cancellationToken); + if (employee != null) + { + var user = await _unitOfWork.Users.GetByIdAsync(employee.UserId, cancellationToken); + handlerName = user?.FullName; + } + } + } + else if (cp.HandledByWarehouseOperatorId.HasValue) + { + var warehouseOp = await _unitOfWork.WarehouseOperators.GetByIdAsync(cp.HandledByWarehouseOperatorId.Value, cancellationToken); + if (warehouseOp != null) + { + var employee = await _unitOfWork.Employees.GetByIdAsync(warehouseOp.EmployeeId, cancellationToken); + if (employee != null) + { + var user = await _unitOfWork.Users.GetByIdAsync(employee.UserId, cancellationToken); + handlerName = user?.FullName; + } + } + } + + timeline.Add(new CheckpointTimelineItem( + cp.Id, + cp.StatusCode.ToString(), + StatusLabels.GetValueOrDefault(cp.StatusCode, cp.StatusCode.ToString()), + location?.Name, + location?.Code, + cp.Timestamp, + handlerName, + cp.Remarks, + cp.Id == lastCheckpoint.Id + )); + } + + return timeline; + } + + private async Task PublishCheckpointCreatedWebhookAsync(ShipmentCheckpoint checkpoint, Domain.Entities.Shipment shipment, CancellationToken ct) + { + try + { + var location = checkpoint.LocationId.HasValue + ? await _unitOfWork.Locations.GetByIdAsync(checkpoint.LocationId.Value, ct) + : null; + + var webhookEvent = new CheckpointCreatedEvent( + CheckpointId: checkpoint.Id, + ShipmentId: shipment.Id, + TrackingNumber: shipment.TrackingNumber, + TenantId: shipment.TenantId, + StatusCode: checkpoint.StatusCode.ToString(), + LocationId: checkpoint.LocationId, + LocationCode: location?.Code, + Timestamp: checkpoint.Timestamp, + HandledByDriverId: checkpoint.HandledByDriverId, + HandledByWarehouseOperatorId: checkpoint.HandledByWarehouseOperatorId, + Remarks: checkpoint.Remarks, + WasQrScanned: checkpoint.StatusCode == CheckpointStatus.QrScanned + ); + + await _webhookPublisher.PublishAsync("checkpoint.created", webhookEvent, ct); + _logger.LogInformation("Published checkpoint.created webhook for checkpoint {CheckpointId}", checkpoint.Id); + } + catch (Exception ex) + { + // Fire-and-forget: no interrumpir el flujo si falla el webhook + _logger.LogWarning(ex, "Failed to publish checkpoint.created webhook for checkpoint {CheckpointId}", checkpoint.Id); + } + } + private async Task MapToResponseAsync(ShipmentCheckpoint e, CancellationToken ct) { var location = e.LocationId.HasValue ? await _unitOfWork.Locations.GetByIdAsync(e.LocationId.Value, ct) : null; @@ -115,3 +238,4 @@ private async Task MapToResponseAsync(ShipmentCheckp truck?.Plate, e.ActionType, e.PreviousCustodian, e.NewCustodian, e.Latitude, e.Longitude, e.CreatedAt); } } + diff --git a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentDocumentService.cs b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentDocumentService.cs index 87745c7..cd05f4e 100644 --- a/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentDocumentService.cs +++ b/backend/src/Parhelion.Infrastructure/Services/Shipment/ShipmentDocumentService.cs @@ -75,5 +75,6 @@ public async Task DeleteAsync(Guid id, CancellationToken cancel } private static ShipmentDocumentResponse MapToResponse(ShipmentDocument e) => new( - e.Id, e.ShipmentId, e.DocumentType.ToString(), e.FileUrl, e.GeneratedBy, e.GeneratedAt, e.ExpiresAt, e.CreatedAt); + e.Id, e.ShipmentId, e.DocumentType.ToString(), e.FileUrl, e.GeneratedBy, e.GeneratedAt, e.ExpiresAt, e.CreatedAt, + e.SignatureBase64, e.SignedByName, e.SignedAt); } diff --git a/docker-compose.yml b/docker-compose.yml index 6c6ec5c..28ce1cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -73,6 +73,23 @@ services: timeout: 10s retries: 3 + # ===== FRONTEND INICIO (Angular Landing) ===== + inicio: + build: + context: ./frontend-inicio + dockerfile: Dockerfile + container_name: parhelion-inicio + restart: unless-stopped + ports: + - "${INICIO_PORT:-4000}:80" + networks: + - parhelion-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + # ===== FRONTEND OPERACIONES (React PWA) ===== operaciones: build: diff --git a/frontend-inicio/.editorconfig b/frontend-inicio/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/frontend-inicio/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/frontend-inicio/.gitignore b/frontend-inicio/.gitignore new file mode 100644 index 0000000..cc7b141 --- /dev/null +++ b/frontend-inicio/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/frontend-inicio/.vscode/extensions.json b/frontend-inicio/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/frontend-inicio/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/frontend-inicio/.vscode/launch.json b/frontend-inicio/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/frontend-inicio/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/frontend-inicio/.vscode/tasks.json b/frontend-inicio/.vscode/tasks.json new file mode 100644 index 0000000..a298b5b --- /dev/null +++ b/frontend-inicio/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/frontend-inicio/Dockerfile b/frontend-inicio/Dockerfile new file mode 100644 index 0000000..ecd2a6f --- /dev/null +++ b/frontend-inicio/Dockerfile @@ -0,0 +1,35 @@ +# =================================== +# PARHELION INICIO (Angular) - Dockerfile +# Multi-stage build para producción +# =================================== + +# --- STAGE 1: Build --- +FROM node:20-alpine AS build +WORKDIR /app + +# Copiar archivos de dependencias primero (cache) +COPY package*.json ./ + +# Instalar dependencias +RUN npm install --silent + +# Copiar código fuente +COPY . . + +# Build de producción +RUN npm run build + +# --- STAGE 2: Nginx --- +FROM nginx:alpine AS runtime + +# Copiar configuración de Nginx +COPY nginx.conf /etc/nginx/nginx.conf + +# Copiar artefactos del build +COPY --from=build /app/dist/frontend-inicio/browser /usr/share/nginx/html + +# Puerto +EXPOSE 80 + +# Comando de inicio +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend-inicio/README.md b/frontend-inicio/README.md new file mode 100644 index 0000000..8955973 --- /dev/null +++ b/frontend-inicio/README.md @@ -0,0 +1,27 @@ +# FrontendInicio + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.21. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/frontend-inicio/angular.json b/frontend-inicio/angular.json new file mode 100644 index 0000000..2e38da9 --- /dev/null +++ b/frontend-inicio/angular.json @@ -0,0 +1,121 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "frontend-inicio": { + "projectType": "application", + "schematics": { + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:component": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/frontend-inicio", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kB", + "maximumError": "10kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "frontend-inicio:build:production" + }, + "development": { + "buildTarget": "frontend-inicio:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/frontend-inicio/nginx.conf b/frontend-inicio/nginx.conf new file mode 100644 index 0000000..d700400 --- /dev/null +++ b/frontend-inicio/nginx.conf @@ -0,0 +1,30 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + # SPA routing - redirect all requests to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } +} diff --git a/frontend-inicio/package-lock.json b/frontend-inicio/package-lock.json new file mode 100644 index 0000000..91d805a --- /dev/null +++ b/frontend-inicio/package-lock.json @@ -0,0 +1,14239 @@ +{ + "name": "frontend-inicio", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend-inicio", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^18.2.0", + "@angular/common": "^18.2.0", + "@angular/compiler": "^18.2.0", + "@angular/core": "^18.2.0", + "@angular/forms": "^18.2.0", + "@angular/platform-browser": "^18.2.0", + "@angular/platform-browser-dynamic": "^18.2.0", + "@angular/router": "^18.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.10" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.2.21", + "@angular/cli": "^18.2.21", + "@angular/compiler-cli": "^18.2.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.5.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1802.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.21.tgz", + "integrity": "sha512-+Ll+xtpKwZ3iLWN/YypvnCZV/F0MVbP+/7ZpMR+Xv/uB0OmribhBVj9WGaCd9I/bGgoYBw8wBV/NFNCKkf0k3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.21.tgz", + "integrity": "sha512-0pJfURFpEUV2USgZ2TL3nNAaJmF9bICx9OVddBoC+F9FeOpVKxkcVIb+c8Km5zHFo1iyVtPZ6Rb25vFk9Zm/ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.21", + "@angular-devkit/build-webpack": "0.1802.21", + "@angular-devkit/core": "18.2.21", + "@angular/build": "18.2.21", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", + "@discoveryjs/json-ext": "0.6.1", + "@ngtools/webpack": "18.2.21", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.1.3", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "critters": "0.0.24", + "css-loader": "7.1.2", + "esbuild-wasm": "0.23.0", + "fast-glob": "3.3.2", + "http-proxy-middleware": "3.0.5", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "magic-string": "0.30.11", + "mini-css-extract-plugin": "2.9.0", + "mrmime": "2.0.0", + "open": "10.1.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "postcss": "8.4.41", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.77.6", + "sass-loader": "16.0.0", + "semver": "7.6.3", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.31.6", + "tree-kill": "1.2.2", + "tslib": "2.6.3", + "watchpack": "2.4.1", + "webpack": "5.94.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.2.2", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.23.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^18.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1802.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.21.tgz", + "integrity": "sha512-2jSVRhA3N4Elg8OLcBktgi+CMSjlAm/bBQJE6TQYbdQWnniuT7JAWUHA/iPf7MYlQE5qj4rnAni1CI/c1Bk4HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1802.21", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.21.tgz", + "integrity": "sha512-Lno6GNbJME85wpc/uqn+wamBxvfZJZFYSH8+oAkkyjU/hk8r5+X8DuyqsKAa0m8t46zSTUsonHsQhVe5vgrZeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.21.tgz", + "integrity": "sha512-yuC2vN4VL48JhnsaOa9J/o0Jl+cxOklRNQp5J2/ypMuRROaVCrZAPiX+ChSHh++kHYMpj8+ggNrrUwRNfMKACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.11", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/animations": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.14.tgz", + "integrity": "sha512-Kp/MWShoYYO+R3lrrZbZgszbbLGVXHB+39mdJZwnIuZMDkeL3JsIBlSOzyJRTnpS1vITc+9jgHvP/6uKbMrW1Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14" + } + }, + "node_modules/@angular/build": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.21.tgz", + "integrity": "sha512-uvq3qP4cByJrUkV1ri0v3x6LxOFt4fDKiQdNwbQAqdxtfRs3ssEIoCGns4t89sTWXv6VZWBNDcDIKK9/Fa9mmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.21", + "@babel/core": "7.25.2", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.24.7", + "@inquirer/confirm": "3.1.22", + "@vitejs/plugin-basic-ssl": "1.1.0", + "browserslist": "^4.23.0", + "critters": "0.0.24", + "esbuild": "0.23.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "listr2": "8.2.4", + "lmdb": "3.0.13", + "magic-string": "0.30.11", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "rollup": "4.22.4", + "sass": "1.77.6", + "semver": "7.6.3", + "vite": "~5.4.17", + "watchpack": "2.4.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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/@angular/build/node_modules/@babel/core/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/@angular/build/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular/build/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/@angular/cli": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.21.tgz", + "integrity": "sha512-efweY4p8awRTbHs+HKdg6s44hl7Y0gdVlXYi3HeY8Z5JDC0abbka0K6sA/MrV9AXvn/5ovxYbxiL3AsOApjTpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1802.21", + "@angular-devkit/core": "18.2.21", + "@angular-devkit/schematics": "18.2.21", + "@inquirer/prompts": "5.3.8", + "@listr2/prompt-adapter-inquirer": "2.0.15", + "@schematics/angular": "18.2.21", + "@yarnpkg/lockfile": "1.1.0", + "ini": "4.1.3", + "jsonc-parser": "3.3.1", + "listr2": "8.2.4", + "npm-package-arg": "11.0.3", + "npm-pick-manifest": "9.1.0", + "pacote": "18.0.6", + "resolve": "1.22.8", + "semver": "7.6.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.14.tgz", + "integrity": "sha512-ZPRswzaVRiqcfZoowuAM22Hr2/z10ajWOUoFDoQ9tWqz/fH/773kJv2F9VvePIekgNPCzaizqv9gF6tGNqaAwg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.14.tgz", + "integrity": "sha512-Mpq3v/mztQzGAQAAFV+wAI1hlXxZ0m8eDBgaN2kD3Ue+r4S6bLm1Vlryw0iyUnt05PcFIdxPT6xkcphq5pl6lw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.14.tgz", + "integrity": "sha512-BmmjyrFSBSYkm0tBSqpu4cwnJX/b/XvhM36mj2k8jah3tNS5zLDDx5w6tyHmaPJa/1D95MlXx2h6u7K9D+Mhew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.25.2", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "18.2.14", + "typescript": ">=5.4 <5.6" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/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/@angular/core": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.14.tgz", + "integrity": "sha512-BIPrCs93ZZTY9ym7yfoTgAQ5rs706yoYeAdrgc8kh/bDbM9DawxKlgeKBx2FLt09Y0YQ1bFhKVp0cV4gDEaMxQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.10" + } + }, + "node_modules/@angular/forms": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.14.tgz", + "integrity": "sha512-fZVwXctmBJa5VdopJae/T9MYKPXNd04+6j4k/6X819y+9fiyWLJt2QicSc5Rc+YD9mmhXag3xaljlrnotf9VGA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.14.tgz", + "integrity": "sha512-W+JTxI25su3RiZVZT3Yrw6KNUCmOIy7OZIZ+612skPgYK2f2qil7VclnW1oCwG896h50cMJU/lnAfxZxefQgyQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "18.2.14", + "@angular/common": "18.2.14", + "@angular/core": "18.2.14" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.14.tgz", + "integrity": "sha512-QOv+o89u8HLN0LG8faTIVHKBxfkOBHVDB0UuXy19+HJofWZGGvho+vGjV0/IAkhZnMC4Sxdoy/mOHP2ytALX3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/compiler": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14" + } + }, + "node_modules/@angular/router": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.14.tgz", + "integrity": "sha512-v/gweh8MBjjDfh1QssuyjISa+6SVVIvIZox7MaMs81RkaoVHwS9grDtPud1pTKHzms2KxSVpvwwyvkRJQplueg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "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.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "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/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/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/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "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-compilation-targets/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/@babel/helper-create-class-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/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/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/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/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "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-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.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-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "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/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "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-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "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-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "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-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "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-block-scoping": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "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-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "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-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "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-exponentiation-operator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "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-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "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-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "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-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "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-logical-assignment-operators": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "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-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "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-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "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-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "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-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "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-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "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-optional-chaining": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "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-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "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-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "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-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "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-runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/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/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "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-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "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-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "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-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "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-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "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-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "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/preset-env/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/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.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/traverse/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/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/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", + "integrity": "sha512-boghen8F0Q8D+0/Q1/1r6DUEieUJ8w2a1gIknExMSHBsJFOr2+0KUfHiVYBvucPwl3+RU5PFBK833FjFCh3BhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", + "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", + "integrity": "sha512-MZrGem/Ujjd4cPTLYDfCZK2iKKeiO/8OX13S6jqxldLs0Prf2aGqVlJ77nMBqMv7fzqgXEgjrNHLXcKR8l9lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 6" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.0.13.tgz", + "integrity": "sha512-uiKPB0Fv6WEEOZjruu9a6wnW/8jrjzlZbxXscMB8kuCJ1k6kHpcBnuvaAWcqhbI7rqX5GKziwWEdD+wi2gNLfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.0.13.tgz", + "integrity": "sha512-bEVIIfK5mSQoG1R19qA+fJOvCB+0wVGGnXHT3smchBVahYBdlPn2OsZZKzlHWfb1E+PhLBmYfqB5zQXFP7hJig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.0.13.tgz", + "integrity": "sha512-Yml1KlMzOnXj/tnW7yX8U78iAzTk39aILYvCPbqeewAq1kSzl+w59k/fiVkTBfvDi/oW/5YRxL+Fq+Y1Fr1r2Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.0.13.tgz", + "integrity": "sha512-afbVrsMgZ9dUTNUchFpj5VkmJRxvht/u335jUJ7o23YTbNbnpmXif3VKQGCtnjSh+CZaqm6N3CPG8KO3zwyZ1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.0.13.tgz", + "integrity": "sha512-vOtxu0xC0SLdQ2WRXg8Qgd8T32ak4SPqk5zjItRszrJk2BdeXqfGxBJbP7o4aOvSPSmSSv46Lr1EP4HXU8v7Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.0.13.tgz", + "integrity": "sha512-UCrMJQY/gJnOl3XgbWRZZUvGGBuKy6i0YNSptgMzHBjs+QYDYR1Mt/RLTOPy4fzzves65O1EDmlL//OzEqoLlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ngtools/webpack": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.21.tgz", + "integrity": "sha512-mfLT7lXbyJRlsazuPyuF5AGsMcgzRJRwsDlgxFbiy1DBlaF1chRFsXrKYj1gQ/WXQWNcEd11aedU0Rt+iCNDVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "typescript": ">=5.4 <5.6", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/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/@npmcli/package-json/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/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/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.21.tgz", + "integrity": "sha512-5Ai+NEflQZi67y4NsQ3o04iEp7zT0/BUFVCrJ3CueU3uYQGs8jrN1Lk6tvQ9c5HzGcTDrMXuTrCswyR9o6ecpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "@angular-devkit/schematics": "18.2.21", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", + "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/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/@tufjs/models/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/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz", + "integrity": "sha512-MYCcDkruFc92LeYZux5BC0dmqo2jk+M5UIZ4/oFnAPCXN9mCcQhLyj7F3/Za7rocVyt5YRr1MmqJqFlvQ9LVcg==", + "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/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "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.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/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/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.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/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "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/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/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/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "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/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "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/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/critters": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", + "integrity": "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==", + "deprecated": "Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "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/cross-spawn/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/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "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/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "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==", + "dev": true, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "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/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.23.0.tgz", + "integrity": "sha512-6jP8UmWy6R6TUUV8bMuC3ZyZ6lZKI56x0tkxyCIqWwRRJ/DgeQKneh/Oid5EoGoPFLrGNkz47ZEtWAYuiY/u9g==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "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-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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/esrecurse/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/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "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-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/globby/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "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/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/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/ignore-walk/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/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", + "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/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/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.0.13.tgz", + "integrity": "sha512-UGe+BbaSUQtAMZobTb4nHvFMrmvuAQKSeaqAX2meTEQjfsbpl5sxdHD8T72OnwD4GU9uwNhYXIVe4QGs8N9Zyw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "msgpackr": "^1.10.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.0.13", + "@lmdb/lmdb-darwin-x64": "3.0.13", + "@lmdb/lmdb-linux-arm": "3.0.13", + "@lmdb/lmdb-linux-arm64": "3.0.13", + "@lmdb/lmdb-linux-x64": "3.0.13", + "@lmdb/lmdb-win32-x64": "3.0.13" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "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/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.51.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", + "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/msgpackr": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", + "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/nice-napi/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/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/node-gyp/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/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/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "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/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ordered-binary": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz", + "integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", + "integrity": "sha512-z30AwWGtQE+Apr+2WBZensP2lIvwoaMcOPkQlIEmSGMJNUvaYACylPYrQM6wSdUNJlnDVMSpLv7xTMJqlVshOA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "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.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "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/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", + "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/sass/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sass/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/sax": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "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/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.2.tgz", + "integrity": "sha512-wMAICvNHJNtnd3Jq97xROyRyFjMQ2G8QsVF6V+K6+6lztP3GaTcIaos+6E7+8jD/NoY++/vCvU9AI+bvRBNXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "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/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "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/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/uri-js/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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/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/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/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/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/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/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", + "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==", + "license": "MIT" + } + } +} diff --git a/frontend-inicio/package.json b/frontend-inicio/package.json new file mode 100644 index 0000000..631adda --- /dev/null +++ b/frontend-inicio/package.json @@ -0,0 +1,38 @@ +{ + "name": "frontend-inicio", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^18.2.0", + "@angular/common": "^18.2.0", + "@angular/compiler": "^18.2.0", + "@angular/core": "^18.2.0", + "@angular/forms": "^18.2.0", + "@angular/platform-browser": "^18.2.0", + "@angular/platform-browser-dynamic": "^18.2.0", + "@angular/router": "^18.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.10" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.2.21", + "@angular/cli": "^18.2.21", + "@angular/compiler-cli": "^18.2.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.5.2" + } +} diff --git a/frontend-inicio/public/favicon.ico b/frontend-inicio/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/frontend-inicio/src/app/app.component.css b/frontend-inicio/src/app/app.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend-inicio/src/app/app.component.html b/frontend-inicio/src/app/app.component.html new file mode 100644 index 0000000..b8daeb7 --- /dev/null +++ b/frontend-inicio/src/app/app.component.html @@ -0,0 +1,335 @@ + + + + + + + + + + + +
+
+
+ +

Hello, {{ title }}

+

Congratulations! Your app is running. 🎉

+
+ +
+
+ @for (item of [ + { title: 'Explore the Docs', link: 'https://angular.dev' }, + { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, + { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, + { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, + { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, + ]; track item.title) { + + {{ item.title }} + + + + + } +
+ +
+
+
+ + + + + + + + + + diff --git a/frontend-inicio/src/app/app.component.ts b/frontend-inicio/src/app/app.component.ts new file mode 100644 index 0000000..5471f5d --- /dev/null +++ b/frontend-inicio/src/app/app.component.ts @@ -0,0 +1,866 @@ +import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [CommonModule], + template: ` +
+ +
+ + +
+
+ ★ PARHELION v0.5.7 RELEASED + + Dynamic PDF Generation + + Checkpoint Timeline + + POD Digital Signatures + + n8n AI Integration + + ★ PARHELION v0.5.7 RELEASED + + Dynamic PDF Generation + + Checkpoint Timeline + + POD Digital Signatures + + n8n AI Integration + +
+
+ + +
+
+
+

Parhelion

+

Logistics

+
+ +
+ NEW v0.5.7 + Enterprise + Multi-tenant +
+ +

Plataforma Unificada de Logística B2B

+

WMS + TMS | Flotillas Tipificadas | Red Hub & Spoke | Documentación Legal SAT

+ + +
+ +
+ Development Preview - Sistema en desarrollo activo. + Ver GitHub +
+
+ + + +
+
+ + +
+

Características Principales

+ +
+ + + + +
+ +
+
+
+
+

Multi-Tenancy

+ +
+
+ Query Filters globales por TenantId. Aislamiento completo de datos. +
+
+
+
+

JWT Authentication

+ +
+
+ Roles: SuperAdmin, Admin, Driver, Warehouse. BCrypt + Argon2id. +
+
+
+
+

Clean Architecture

+ +
+
+ Domain-Driven Design. 25 entidades, 17 enums, Repository Pattern. +
+
+
+
+ +
+
+
+
+

Camiones Tipificados

+ 5 tipos +
+
+ DryBox, Refrigerado, HAZMAT, Plataforma, Blindado +
+
+
+
+

Choferes GPS

+ +
+
+ Búsqueda geoespacial con Haversine. Endpoint /drivers/nearby +
+
+
+
+

FleetLog Automático

+ +
+
+ Bitácora de cambios de vehículo: ShiftChange, Breakdown, Reassignment +
+
+
+
+ +
+
+
+
+

Generación Dinámica

+ NEW +
+
+ PDFs on-demand sin almacenamiento. Blob URLs estilo WhatsApp Web. +
+
+
+
+

5 Documentos

+ SAT +
+
+ Orden de Servicio, Carta Porte, Manifiesto, Hoja de Ruta, POD +
+
+
+
+

Firma Digital

+ NEW +
+
+ Captura de firma, geolocalización y timestamp en POD +
+
+
+
+ +
+
+
+
+

n8n Webhooks

+ AI +
+
+ 5 eventos: Exception, BookingRequest, Handshake, StatusChanged, Checkpoint +
+
+
+
+

Crisis Management

+ +
+
+ Agente IA busca chofer cercano ante excepciones automáticamente +
+
+
+
+

ServiceApiKey

+ +
+
+ Autenticación para agentes por tenant con SHA256 +
+
+
+
+
+ + +
+

Progreso del MVP

+ +
+
+
+ Backend API + 95% +
+
+
+
+
+
+
+ Database Schema + 100% +
+
+
+
+
+
+
+ Unit Tests + 122 +
+
+
+
+
+
+
+ Frontend Apps + 60% +
+
+
+
+
+
+
+ + +
+

Changelog Completo

+ + +
+ + +
+

Stack Tecnológico

+ +
+
+ +
+
+

Framework: ASP.NET Core 8 Web API

+

ORM: Entity Framework Core (Code First)

+

Database: PostgreSQL 17

+

Architecture: Clean Architecture + DDD

+
+
+
+ +
+ +
+
+

Admin: Angular 18 + Material Design

+

Operaciones: React + Vite + Tailwind (PWA)

+

Driver: React + Vite + Tailwind (PWA)

+

Design: Neo-Brutalism

+
+
+
+ +
+ +
+
+

Container: Docker + Docker Compose

+

Tunnel: Cloudflare Tunnel (Zero Trust)

+

Server: Digital Ocean Droplet (Linux)

+

Automation: n8n Workflow Engine

+
+
+
+
+
+ + + +
+ `, + styles: [` + .app { + min-height: 100vh; + background-color: var(--parhelion-sand); + position: relative; + overflow-x: hidden; + } + + /* HERO */ + .hero { + min-height: 90vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + padding-top: 4rem; + } + + .hero-content { + text-align: center; + max-width: 800px; + } + + .logo { + font-family: var(--font-logo); + font-size: 5rem; + margin: 0; + line-height: 1; + } + + .logo-subtitle { + font-family: var(--font-logo); + font-size: 3rem; + color: var(--parhelion-oxide); + margin: 0 0 1rem 0; + } + + .badges { + display: flex; + justify-content: center; + gap: 0.75rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; + } + + .tagline { + font-family: var(--font-heading); + font-size: 1.75rem; + color: var(--parhelion-gray); + margin: 0 0 0.5rem 0; + } + + .description { + color: #666; + margin-bottom: 2rem; + font-size: 1.1rem; + } + + .alert { + max-width: 500px; + margin: 0 auto 2rem auto; + text-align: left; + } + + .alert a { + color: var(--parhelion-oxide-dark); + font-weight: 600; + } + + .app-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + } + + /* FEATURES */ + .features-section { + background-color: var(--parhelion-white); + border-top: 2px solid var(--parhelion-black); + border-bottom: 2px solid var(--parhelion-black); + } + + .features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + margin-top: 1rem; + } + + .features-grid .card:hover { + border-color: var(--parhelion-oxide); + } + + .features-grid .card-header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .features-grid .card-header h3 { + font-size: 1.1rem; + margin: 0; + } + + .features-grid .card-content { + font-size: 0.95rem; + color: #555; + } + + /* PROGRESS */ + .progress-section { + max-width: 700px; + margin: 0 auto; + } + + .progress-items { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + .progress-item { + animation: slideIn 0.5s ease; + } + + .progress-label { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + font-weight: 600; + } + + /* CAROUSEL */ + .changelog-section { + max-width: 700px; + margin: 0 auto; + } + + .changelog-item { + text-align: left; + } + + .changelog-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 0.25rem; + } + + .changelog-header h3 { + margin: 0; + font-size: 1.25rem; + } + + .changelog-date { + color: #888; + font-size: 0.85rem; + margin: 0 0 1rem 0; + } + + .changelog-item ul { + list-style: none; + padding: 0; + } + + .changelog-item li { + padding: 0.5rem 0; + border-bottom: 1px dashed #ccc; + font-size: 0.95rem; + } + + .changelog-item li:last-child { + border-bottom: none; + } + + /* ACCORDION */ + .stack-section { + max-width: 700px; + margin: 0 auto; + } + + .accordion { + border: 2px solid var(--parhelion-black); + } + + .accordion-content-inner p { + margin: 0.5rem 0; + font-size: 0.95rem; + } + + /* FOOTER */ + .footer { + background-color: var(--parhelion-black); + color: var(--parhelion-white); + padding: 3rem 2rem; + text-align: center; + border-top: 4px solid var(--parhelion-oxide); + } + + .footer-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + } + + .footer .btn-primary { + background-color: var(--parhelion-white); + color: var(--parhelion-black); + } + + .footer .btn-primary:hover { + background-color: var(--parhelion-oxide); + color: var(--parhelion-white); + } + + .version { + color: var(--parhelion-oxide); + font-weight: 600; + margin: 0; + } + + .portfolio { + color: #aaa; + font-size: 0.9rem; + margin: 0; + } + + .credits { + color: #666; + font-size: 0.8rem; + margin: 0; + } + + .credits a { + color: var(--parhelion-oxide); + } + + /* RESPONSIVE - Mobile First */ + + /* Mobile (default) */ + .logo { font-size: 2.5rem; } + .logo-subtitle { font-size: 1.5rem; } + .tagline { font-size: 1rem; margin: 0 0 0.25rem 0; } + .description { font-size: 0.9rem; margin-bottom: 1.5rem; } + .hero { min-height: auto; padding: 2rem 1rem; padding-top: 3rem; } + .hero-content { max-width: 100%; } + .app-buttons { gap: 0.75rem; } + .features-grid { grid-template-columns: 1fr; gap: 1rem; } + .changelog-header h3 { font-size: 1rem; } + .changelog-item li { font-size: 0.85rem; padding: 0.35rem 0; } + .changelog-date { font-size: 0.75rem; } + .progress-section, .changelog-section, .stack-section { + max-width: 100%; + padding-left: 1rem; + padding-right: 1rem; + } + .progress-items { gap: 1rem; } + .progress-label { font-size: 0.9rem; } + .footer { padding: 2rem 1rem; } + .footer .btn-primary { padding: 10px 16px; font-size: 0.75rem; } + .portfolio { font-size: 0.8rem; } + .credits { font-size: 0.7rem; } + .version { font-size: 0.85rem; } + + /* Small phones (480px+) */ + @media (min-width: 480px) { + .logo { font-size: 3rem; } + .logo-subtitle { font-size: 1.75rem; } + .tagline { font-size: 1.15rem; } + .description { font-size: 0.95rem; } + .hero { padding: 2.5rem 1.25rem; } + .changelog-header h3 { font-size: 1.1rem; } + } + + /* Tablets (768px+) */ + @media (min-width: 768px) { + .logo { font-size: 4rem; } + .logo-subtitle { font-size: 2.25rem; } + .tagline { font-size: 1.35rem; margin: 0 0 0.5rem 0; } + .description { font-size: 1rem; margin-bottom: 2rem; } + .hero { min-height: 85vh; padding: 3rem 1.5rem; padding-top: 4rem; } + .hero-content { max-width: 700px; } + .app-buttons { gap: 1rem; } + .features-grid { grid-template-columns: repeat(2, 1fr); gap: 1.25rem; } + .changelog-header h3 { font-size: 1.15rem; } + .changelog-item li { font-size: 0.9rem; padding: 0.4rem 0; } + .changelog-date { font-size: 0.8rem; } + .progress-section, .changelog-section, .stack-section { max-width: 600px; } + .progress-items { gap: 1.25rem; } + .footer { padding: 2.5rem 1.5rem; } + .footer .btn-primary { padding: 12px 20px; font-size: 0.8rem; } + } + + /* Desktop (1024px+) */ + @media (min-width: 1024px) { + .logo { font-size: 5rem; } + .logo-subtitle { font-size: 3rem; } + .tagline { font-size: 1.75rem; } + .description { font-size: 1.1rem; } + .hero { min-height: 90vh; padding: 3rem 2rem; } + .hero-content { max-width: 800px; } + .features-grid { grid-template-columns: repeat(3, 1fr); gap: 1.5rem; } + .changelog-header h3 { font-size: 1.25rem; } + .changelog-item li { font-size: 0.95rem; padding: 0.5rem 0; } + .changelog-date { font-size: 0.85rem; } + .progress-section, .changelog-section, .stack-section { max-width: 700px; } + .progress-items { gap: 1.5rem; } + .footer { padding: 3rem 2rem; } + .footer .btn-primary { padding: 14px 24px; font-size: 0.85rem; } + .portfolio { font-size: 0.9rem; } + .credits { font-size: 0.8rem; } + } + `] +}) +export class AppComponent implements AfterViewInit { + @ViewChild('gridBg') gridBg!: ElementRef; + + activeTab = 'core'; + currentSlide = 0; + openAccordion = 'backend'; + + ngAfterViewInit() { + // Random grid animation direction + const directions = [ + { x: 150, y: 0 }, + { x: -150, y: 0 }, + { x: 0, y: 150 }, + { x: 0, y: -150 }, + { x: 120, y: 120 }, + { x: -120, y: 120 }, + { x: 120, y: -120 }, + { x: -120, y: -120 } + ]; + + const randomDir = directions[Math.floor(Math.random() * directions.length)]; + const randomDuration = 30 + Math.random() * 30; + + const el = this.gridBg.nativeElement; + el.style.setProperty('--grid-x', `${randomDir.x}px`); + el.style.setProperty('--grid-y', `${randomDir.y}px`); + el.style.setProperty('--grid-duration', `${randomDuration}s`); + + // Auto-rotate carousel + setInterval(() => { + this.currentSlide = (this.currentSlide + 1) % 8; + }, 6000); + } + + toggleAccordion(id: string) { + this.openAccordion = this.openAccordion === id ? '' : id; + } +} diff --git a/frontend-inicio/src/app/app.config.ts b/frontend-inicio/src/app/app.config.ts new file mode 100644 index 0000000..d03bbbc --- /dev/null +++ b/frontend-inicio/src/app/app.config.ts @@ -0,0 +1,5 @@ +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; + +export const appConfig: ApplicationConfig = { + providers: [provideZoneChangeDetection({ eventCoalescing: true })] +}; diff --git a/frontend-inicio/src/index.html b/frontend-inicio/src/index.html new file mode 100644 index 0000000..8c7ba99 --- /dev/null +++ b/frontend-inicio/src/index.html @@ -0,0 +1,17 @@ + + + + + Parhelion Logistics Home + + + + + + + + + diff --git a/frontend-inicio/src/main.ts b/frontend-inicio/src/main.ts new file mode 100644 index 0000000..35b00f3 --- /dev/null +++ b/frontend-inicio/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/frontend-inicio/src/styles.css b/frontend-inicio/src/styles.css new file mode 100644 index 0000000..d96632d --- /dev/null +++ b/frontend-inicio/src/styles.css @@ -0,0 +1,858 @@ +/* ===== PARHELION DESIGN SYSTEM - NEO-BRUTALISM ===== */ + +@import url('https://fonts.googleapis.com/css2?family=New+Rocker&family=Merriweather:wght@700;900&family=Inter:wght@400;500;600;700&display=swap'); + +:root { + /* Color Palette - Industrial Solar */ + --parhelion-oxide: #C85A17; + --parhelion-oxide-dark: #A84810; + --parhelion-oxide-light: #E8721F; + --parhelion-white: #FAFAFA; + --parhelion-black: #000000; + --parhelion-sand: #E8E6E1; + --parhelion-gray: #333333; + --parhelion-slate: #2C3E50; + --parhelion-success: #10B981; + --parhelion-warning: #F59E0B; + --parhelion-info: #3B82F6; + + /* Typography */ + --font-logo: 'New Rocker', cursive; + --font-heading: 'Merriweather', Georgia, serif; + --font-body: 'Inter', system-ui, sans-serif; + + /* Neo-Brutalism */ + --border-width: 2px; + --shadow-offset: 4px; + --shadow-offset-lg: 6px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-body); + background-color: var(--parhelion-sand); + color: var(--parhelion-black); + -webkit-font-smoothing: antialiased; + overflow-x: hidden; +} + +/* ===== TYPOGRAPHY ===== */ + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + font-weight: 900; + color: var(--parhelion-black); +} + +/* ===== NEO-BRUTALIST BUTTON ===== */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 14px 28px; + font-family: var(--font-body); + font-weight: 700; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.05em; + border: var(--border-width) solid var(--parhelion-black); + cursor: pointer; + text-decoration: none; + transition: transform 0.15s ease, box-shadow 0.15s ease; + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--parhelion-black); +} + +.btn:hover { + transform: translate(2px, 2px); + box-shadow: 2px 2px 0 var(--parhelion-black); +} + +.btn:active { + transform: translate(4px, 4px); + box-shadow: 0 0 0 var(--parhelion-black); +} + +.btn-primary { + background-color: var(--parhelion-white); + color: var(--parhelion-black); +} + +.btn-primary:hover { + background-color: var(--parhelion-oxide); + color: var(--parhelion-white); +} + +.btn-oxide { + background-color: var(--parhelion-oxide); + color: var(--parhelion-white); +} + +.btn-oxide:hover { + background-color: var(--parhelion-oxide-dark); + color: var(--parhelion-white); +} + +.btn-lg { + padding: 18px 36px; + font-size: 1rem; + box-shadow: var(--shadow-offset-lg) var(--shadow-offset-lg) 0 var(--parhelion-black); +} + +/* ===== NEO-BRUTALIST BADGE ===== */ + +.badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 4px 10px; + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + border: 2px solid var(--parhelion-black); + background-color: var(--parhelion-white); +} + +.badge-new { + background-color: var(--parhelion-success); + color: white; + animation: pulse-badge 2s infinite; +} + +.badge-oxide { + background-color: var(--parhelion-oxide); + color: white; +} + +.badge-warning { + background-color: var(--parhelion-warning); + color: var(--parhelion-black); +} + +@keyframes pulse-badge { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +/* ===== NEO-BRUTALIST CARD ===== */ + +.card { + background-color: var(--parhelion-white); + border: var(--border-width) solid var(--parhelion-black); + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--parhelion-black); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card:hover { + transform: translate(-2px, -2px); + box-shadow: 6px 6px 0 var(--parhelion-black); +} + +.card-header { + padding: 1.25rem; + border-bottom: 2px solid var(--parhelion-black); +} + +.card-content { + padding: 1.25rem; +} + +.card-footer { + padding: 1rem 1.25rem; + border-top: 2px solid var(--parhelion-black); + background-color: var(--parhelion-sand); +} + +/* ===== MARQUEE ===== */ + +.marquee-container { + overflow: hidden; + border-top: 2px solid var(--parhelion-black); + border-bottom: 2px solid var(--parhelion-black); + background-color: var(--parhelion-oxide); + padding: 0.75rem 0; +} + +.marquee { + display: flex; + gap: 3rem; + animation: marquee 30s linear infinite; + white-space: nowrap; +} + +.marquee span { + color: var(--parhelion-white); + font-weight: 700; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +@keyframes marquee { + 0% { transform: translateX(0); } + 100% { transform: translateX(-50%); } +} + +/* ===== CAROUSEL ===== */ + +.carousel { + position: relative; + overflow: hidden; +} + +.carousel-track { + display: flex; + transition: transform 0.5s ease; +} + +.carousel-slide { + min-width: 100%; + padding: 1.5rem; +} + +.carousel-nav { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-top: 1rem; +} + +.carousel-dot { + width: 12px; + height: 12px; + border: 2px solid var(--parhelion-black); + background-color: var(--parhelion-white); + cursor: pointer; + transition: all 0.2s; +} + +.carousel-dot.active { + background-color: var(--parhelion-oxide); + transform: scale(1.2); +} + +/* ===== PROGRESS BAR ===== */ + +.progress-container { + background-color: var(--parhelion-white); + border: 2px solid var(--parhelion-black); + height: 24px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background-color: var(--parhelion-oxide); + transition: width 1s ease; + position: relative; +} + +.progress-bar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255,255,255,0.3) 50%, + transparent 100% + ); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +/* ===== TABS ===== */ + +.tabs-list { + display: flex; + border: 2px solid var(--parhelion-black); + background-color: var(--parhelion-sand); +} + +.tab-trigger { + flex: 1; + padding: 12px 20px; + font-family: var(--font-body); + font-weight: 600; + font-size: 0.85rem; + text-transform: uppercase; + background: transparent; + border: none; + border-right: 2px solid var(--parhelion-black); + cursor: pointer; + transition: all 0.2s; +} + +.tab-trigger:last-child { + border-right: none; +} + +.tab-trigger:hover { + background-color: var(--parhelion-white); +} + +.tab-trigger.active { + background-color: var(--parhelion-oxide); + color: var(--parhelion-white); +} + +.tab-content { + display: none; + padding: 1.5rem; + border: 2px solid var(--parhelion-black); + border-top: none; + background-color: var(--parhelion-white); +} + +.tab-content.active { + display: block; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ===== ALERT ===== */ + +.alert { + padding: 1rem 1.25rem; + border: 2px solid var(--parhelion-black); + display: flex; + align-items: flex-start; + gap: 0.75rem; + animation: slideIn 0.4s ease; +} + +.alert-success { background-color: #D1FAE5; } +.alert-warning { background-color: #FEF3C7; } +.alert-info { background-color: #DBEAFE; } +.alert-oxide { background-color: #FED7AA; } + +@keyframes slideIn { + from { opacity: 0; transform: translateX(-20px); } + to { opacity: 1; transform: translateX(0); } +} + +/* ===== ACCORDION ===== */ + +.accordion-item { + border: 2px solid var(--parhelion-black); + margin-bottom: -2px; +} + +.accordion-trigger { + width: 100%; + padding: 1rem 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + font-family: var(--font-body); + font-weight: 600; + font-size: 1rem; + background-color: var(--parhelion-white); + border: none; + cursor: pointer; + text-align: left; + transition: background-color 0.2s; +} + +.accordion-trigger:hover { + background-color: var(--parhelion-sand); +} + +.accordion-trigger .icon { + transition: transform 0.3s; +} + +.accordion-item.open .accordion-trigger .icon { + transform: rotate(180deg); +} + +.accordion-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + background-color: var(--parhelion-sand); +} + +.accordion-item.open .accordion-content { + max-height: 500px; +} + +.accordion-content-inner { + padding: 1.25rem; + border-top: 2px solid var(--parhelion-black); +} + +/* ===== AVATAR ===== */ + +.avatar { + width: 48px; + height: 48px; + border: 2px solid var(--parhelion-black); + border-radius: 50%; + overflow: hidden; + box-shadow: 2px 2px 0 var(--parhelion-black); +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* ===== ANIMATED GRID BACKGROUND ===== */ + +.grid-background { + position: fixed; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + pointer-events: none; + z-index: 0; + opacity: 0.15; + background-image: + linear-gradient(rgba(200, 90, 23, 0.5) 2px, transparent 2px), + linear-gradient(90deg, rgba(200, 90, 23, 0.5) 2px, transparent 2px); + background-size: 50px 50px; + animation: grid-move var(--grid-duration, 40s) linear infinite; +} + +@keyframes grid-move { + 0% { transform: translate(0, 0) rotate(0deg); } + 100% { transform: translate(var(--grid-x, 100px), var(--grid-y, 100px)) rotate(2deg); } +} + +.content-layer { + position: relative; + z-index: 1; +} + +/* ===== FLOATING ANIMATION ===== */ + +.float { + animation: float 4s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} + +/* ===== SECTION STYLING ===== */ + +section { + padding: 4rem 2rem; +} + +.section-title { + font-size: 2rem; + margin-bottom: 2rem; + text-align: center; +} + +.section-title span { + color: var(--parhelion-oxide); +} + +/* ===== RESPONSIVE DESIGN SYSTEM ===== */ + +/* Container */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* ===== MOBILE FIRST (Default: < 480px) ===== */ + +.btn { + padding: 10px 16px; + font-size: 0.75rem; + gap: 0.35rem; +} + +.btn svg { + width: 18px; + height: 18px; +} + +.btn-lg { + padding: 12px 20px; + font-size: 0.8rem; +} + +.badge { + padding: 3px 8px; + font-size: 0.6rem; +} + +section { + padding: 2rem 1rem; +} + +.section-title { + font-size: 1.25rem; + margin-bottom: 1.25rem; +} + +.marquee span { + font-size: 0.75rem; +} + +.tabs-list { + flex-wrap: wrap; +} + +.tab-trigger { + flex: 1 1 50%; + padding: 10px 8px; + font-size: 0.7rem; + border-right: none; + border-bottom: 2px solid var(--parhelion-black); +} + +.tab-trigger:nth-child(2n) { + border-left: 2px solid var(--parhelion-black); +} + +.tab-trigger:nth-last-child(-n+2) { + border-bottom: none; +} + +.tab-content { + padding: 1rem; +} + +.card-header { + padding: 0.75rem 1rem; + flex-direction: column; + gap: 0.5rem; + align-items: flex-start !important; +} + +.card-header h3 { + font-size: 0.95rem !important; +} + +.card-content { + padding: 0.75rem 1rem; + font-size: 0.85rem; +} + +.carousel-slide { + padding: 1rem; +} + +.carousel-dot { + width: 10px; + height: 10px; +} + +.progress-container { + height: 20px; +} + +.accordion-trigger { + padding: 0.75rem 1rem; + font-size: 0.9rem; +} + +.accordion-content-inner { + padding: 1rem; +} + +.accordion-content-inner p { + font-size: 0.85rem; +} + +.alert { + padding: 0.75rem 1rem; + flex-direction: column; + gap: 0.5rem; +} + +.alert-icon { + font-size: 1.25rem; +} + +/* ===== SMALL PHONES (480px+) ===== */ + +@media (min-width: 480px) { + .btn { + padding: 12px 20px; + font-size: 0.8rem; + } + + .btn-lg { + padding: 14px 24px; + font-size: 0.85rem; + } + + .badge { + padding: 4px 10px; + font-size: 0.65rem; + } + + section { + padding: 2.5rem 1.25rem; + } + + .section-title { + font-size: 1.5rem; + } + + .card-header { + flex-direction: row; + align-items: center !important; + gap: 0; + } + + .alert { + flex-direction: row; + gap: 0.75rem; + } +} + +/* ===== TABLETS (768px+) ===== */ + +@media (min-width: 768px) { + .btn { + padding: 14px 28px; + font-size: 0.9rem; + gap: 0.5rem; + } + + .btn svg { + width: 20px; + height: 20px; + } + + .btn-lg { + padding: 16px 32px; + font-size: 0.95rem; + } + + .badge { + padding: 4px 10px; + font-size: 0.7rem; + } + + section { + padding: 3rem 1.5rem; + } + + .section-title { + font-size: 1.75rem; + margin-bottom: 1.5rem; + } + + .marquee span { + font-size: 0.85rem; + } + + .tabs-list { + flex-wrap: nowrap; + } + + .tab-trigger { + flex: 1; + padding: 12px 16px; + font-size: 0.8rem; + border-bottom: none; + border-right: 2px solid var(--parhelion-black); + } + + .tab-trigger:nth-child(2n) { + border-left: none; + } + + .tab-trigger:last-child { + border-right: none; + } + + .tab-content { + padding: 1.25rem; + } + + .card-header { + padding: 1rem 1.25rem; + } + + .card-header h3 { + font-size: 1rem !important; + } + + .card-content { + padding: 1rem 1.25rem; + font-size: 0.9rem; + } + + .carousel-slide { + padding: 1.25rem; + } + + .carousel-dot { + width: 12px; + height: 12px; + } + + .progress-container { + height: 22px; + } + + .accordion-trigger { + padding: 1rem 1.25rem; + font-size: 1rem; + } + + .accordion-content-inner { + padding: 1.25rem; + } + + .accordion-content-inner p { + font-size: 0.95rem; + } +} + +/* ===== DESKTOP (1024px+) ===== */ + +@media (min-width: 1024px) { + .btn { + padding: 14px 28px; + font-size: 0.9rem; + } + + .btn svg { + width: 22px; + height: 22px; + } + + .btn-lg { + padding: 18px 36px; + font-size: 1rem; + } + + section { + padding: 4rem 2rem; + } + + .section-title { + font-size: 2rem; + margin-bottom: 2rem; + } + + .marquee span { + font-size: 0.9rem; + } + + .tab-trigger { + padding: 12px 20px; + font-size: 0.85rem; + } + + .tab-content { + padding: 1.5rem; + } + + .card-header { + padding: 1.25rem; + } + + .card-header h3 { + font-size: 1.1rem !important; + } + + .card-content { + padding: 1.25rem; + font-size: 0.95rem; + } + + .carousel-slide { + padding: 1.5rem; + } + + .progress-container { + height: 24px; + } +} + +/* ===== LARGE DESKTOP (1280px+) ===== */ + +@media (min-width: 1280px) { + section { + padding: 5rem 2rem; + } + + .section-title { + font-size: 2.25rem; + } +} + +/* ===== TOUCH DEVICE ENHANCEMENTS ===== */ + +@media (hover: none) and (pointer: coarse) { + .btn:hover { + transform: none; + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--parhelion-black); + } + + .btn:active { + transform: translate(2px, 2px); + box-shadow: 2px 2px 0 var(--parhelion-black); + } + + .card:hover { + transform: none; + box-shadow: var(--shadow-offset) var(--shadow-offset) 0 var(--parhelion-black); + } + + .carousel-dot { + width: 14px; + height: 14px; + } +} + +/* ===== REDUCED MOTION ===== */ + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + + .marquee { + animation: none; + } + + .grid-background { + animation: none; + } + + .float { + animation: none; + } +} diff --git a/frontend-inicio/tsconfig.app.json b/frontend-inicio/tsconfig.app.json new file mode 100644 index 0000000..3775b37 --- /dev/null +++ b/frontend-inicio/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/frontend-inicio/tsconfig.json b/frontend-inicio/tsconfig.json new file mode 100644 index 0000000..a8bb65b --- /dev/null +++ b/frontend-inicio/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend-inicio/tsconfig.spec.json b/frontend-inicio/tsconfig.spec.json new file mode 100644 index 0000000..5fb748d --- /dev/null +++ b/frontend-inicio/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/requirments.md b/requirments.md index 6b7c4fd..541ba5e 100644 --- a/requirments.md +++ b/requirments.md @@ -70,61 +70,61 @@ El sistema debe soportar los siguientes tipos de usuarios con permisos distintos - [ ] **Alta de Recursos:** Después del registro, el cliente puede dar de alta camiones, choferes y ubicaciones. - [ ] **Multi-tenancy:** Cada cliente tiene su propio espacio aislado de datos. -### Módulo 1: Seguridad y Acceso +### Módulo 1: Seguridad y Acceso (v0.4.0+) -- [ ] **Login:** El sistema debe permitir el ingreso mediante Email y Contraseña. -- [ ] **Autenticación:** Debe usar tokens seguros (JWT). La sesión debe expirar automáticamente tras 2 horas de inactividad. -- [ ] **Roles:** Admin, Driver, Warehouse (Almacenista), DemoUser. -- [ ] **Protección:** Un Chofer no debe poder acceder a las pantallas de Administración (Rutas protegidas). +- [x] **Login:** El sistema permite el ingreso mediante Email y Contraseña. +- [x] **Autenticación:** Usa tokens seguros (JWT). La sesión expira automáticamente. +- [x] **Roles:** Admin, Driver, Warehouse (Almacenista), SuperAdmin. +- [x] **Protección:** Rutas protegidas por rol (Authorize). - [ ] **Recuperación de Contraseña:** Flujo básico de "Olvidé mi contraseña" con enlace temporal. -### Módulo 2: Gestión de Flotilla (Camiones) +### Módulo 2: Gestión de Flotilla (Camiones) (v0.4.0+) -- [ ] **Listado:** Ver todos los camiones disponibles, su placa, modelo, tipo y chofer asignado. -- [ ] **Alta de Camión:** Registrar placa (ej. "NL-554-X"), modelo, **Tipo de Camión** y capacidades. -- [ ] **Tipos de Camión (TruckType):** +- [x] **Listado:** Ver todos los camiones disponibles, su placa, modelo, tipo y chofer asignado. +- [x] **Alta de Camión:** Registrar placa (ej. "NL-554-X"), modelo, **Tipo de Camión** y capacidades. +- [x] **Tipos de Camión (TruckType):** - `DryBox` - Caja Seca (Estándar) - `Refrigerated` - Termo/Refrigerado (Cadena de frío) - `HazmatTank` - Pipa (Materiales peligrosos) - `Flatbed` - Plataforma (Carga pesada) - `Armored` - Blindado (Alto valor) -- [ ] **Capacidades:** Peso máximo (kg) y volumen máximo (m³). -- [ ] **Validación:** No pueden existir dos camiones con la misma placa dentro del mismo Cliente. +- [x] **Capacidades:** Peso máximo (kg) y volumen máximo (m³). +- [x] **Validación:** No pueden existir dos camiones con la misma placa dentro del mismo Cliente. -### Módulo 2.5: Gestión de Choferes +### Módulo 2.5: Gestión de Choferes (v0.5.4+) -- [ ] **Listado:** Ver todos los choferes registrados y su estatus (Disponible, En Ruta, Inactivo). -- [ ] **Alta de Chofer:** Registrar nombre, teléfono, email y licencia. -- [ ] **Asignación Híbrida Chofer-Camión:** +- [x] **Listado:** Ver todos los choferes registrados y su estatus (Disponible, En Ruta, Inactivo). +- [x] **Alta de Chofer:** Registrar nombre, teléfono, email y licencia. +- [x] **Asignación Híbrida Chofer-Camión:** - **default_truck_id:** Camión fijo asignado ("su unidad"). - **current_truck_id:** Camión que conduce actualmente (puede diferir). -- [ ] **Bitácora de Flotilla (FleetLog):** Registro automático de cada cambio de vehículo con motivo (ShiftChange, Breakdown, Reassignment). +- [x] **Bitácora de Flotilla (FleetLog):** Registro automático de cada cambio de vehículo con motivo (ShiftChange, Breakdown, Reassignment). -### Módulo 3: Red Logística (Locations) +### Módulo 3: Red Logística (Locations) (v0.4.0+) -- [ ] **Nodos de Red:** Gestión de ubicaciones con código único (estilo aeropuerto: MTY, GDL, MM). -- [ ] **Tipos de Ubicación (LocationType):** +- [x] **Nodos de Red:** Gestión de ubicaciones con código único (estilo aeropuerto: MTY, GDL, MM). +- [x] **Tipos de Ubicación (LocationType):** - `RegionalHub` - Nodo central, recibe y despacha masivo - `CrossDock` - Transferencia rápida sin almacenamiento - `Warehouse` - Bodega de almacenamiento prolongado - `Store` - Punto de venta final (solo recibe) - `SupplierPlant` - Fábrica de origen (solo despacha) -- [ ] **Capacidades:** Flags `can_receive` y `can_dispatch` por ubicación. +- [x] **Capacidades:** Flags `can_receive` y `can_dispatch` por ubicación. -### Módulo 3.5: Enrutamiento (Hub & Spoke) +### Módulo 3.5: Enrutamiento (Hub & Spoke) (v0.5.0+) -- [ ] **Enlaces de Red (NetworkLink):** Conexiones permitidas entre ubicaciones. +- [x] **Enlaces de Red (NetworkLink):** Conexiones permitidas entre ubicaciones. - `FirstMile` - Recolección: Cliente/Proveedor → Hub - `LineHaul` - Carretera: Hub → Hub (larga distancia) - `LastMile` - Entrega: Hub → Cliente/Tienda -- [ ] **Regla de Conexión:** Clientes no pueden conectarse directamente entre sí. -- [ ] **Rutas Predefinidas (RouteBlueprint):** Secuencia de paradas con tiempos de tránsito. +- [x] **Regla de Conexión:** Clientes no pueden conectarse directamente entre sí. +- [x] **Rutas Predefinidas (RouteBlueprint):** Secuencia de paradas con tiempos de tránsito. - [ ] **Cálculo de ETA:** `scheduled_departure + SUM(transit_times)`. -### Módulo 4: Envíos (Shipments) +### Módulo 4: Envíos (Shipments) (v0.5.0+) -- [ ] **Crear Envío:** Registrar origen, destino, ruta asignada, destinatario y prioridad. -- [ ] **Flujo de Estados:** +- [x] **Crear Envío:** Registrar origen, destino, ruta asignada, destinatario y prioridad. +- [x] **Flujo de Estados:** - `PendingApproval` - Orden de servicio esperando revisión - `Approved` - Envío aprobado, listo para asignar - `Loaded` - Paquete cargado en camión @@ -134,28 +134,28 @@ El sistema debe soportar los siguientes tipos de usuarios con permisos distintos - `Delivered` - Entrega confirmada, POD capturado - `Exception` - Problema que requiere atención -### Módulo 4.5: Manifiesto de Carga (ShipmentItems) +### Módulo 4.5: Manifiesto de Carga (ShipmentItems) (v0.5.4+) -- [ ] **Partidas:** SKU, descripción, cantidad, dimensiones, peso. -- [ ] **Peso Volumétrico:** `(Largo × Ancho × Alto) / 5000` -- [ ] **Valor Declarado:** Para cálculo de seguro. -- [ ] **Flags Especiales:** +- [x] **Partidas:** SKU, descripción, cantidad, dimensiones, peso. +- [x] **Peso Volumétrico:** `(Largo × Ancho × Alto) / 5000` +- [x] **Valor Declarado:** Para cálculo de seguro. +- [x] **Flags Especiales:** - `is_fragile` - Requiere manejo cuidadoso - `is_hazardous` - Material peligroso (HAZMAT) - `requires_refrigeration` - Cadena de frío -- [ ] **Instrucciones de Estiba:** "No apilar más de 2 niveles". +- [x] **Instrucciones de Estiba:** "No apilar más de 2 niveles". -### Módulo 5: Validación de Compatibilidad (Hard Constraints) +### Módulo 5: Validación de Compatibilidad (Hard Constraints) (v0.5.5+) -- [ ] **Cadena de Frío:** Items con `requires_refrigeration=true` SOLO en camiones `Refrigerated`. -- [ ] **HAZMAT:** Items con `is_hazardous=true` SOLO en camiones `HazmatTank`. -- [ ] **Alto Valor:** Si `SUM(declared_value) > $1,000,000`, requiere camión `Armored`. -- [ ] **Capacidad:** La suma de peso/volumen NO puede exceder la capacidad del camión. +- [x] **Cadena de Frío:** Items con `requires_refrigeration=true` SOLO en camiones `Refrigerated`. +- [x] **HAZMAT:** Items con `is_hazardous=true` SOLO en camiones `HazmatTank`. +- [x] **Alto Valor:** Si `SUM(declared_value) > $1,000,000`, requiere camión `Armored`. +- [x] **Capacidad:** La suma de peso/volumen NO puede exceder la capacidad del camión. -### Módulo 6: Trazabilidad (Checkpoints) +### Módulo 6: Trazabilidad (Checkpoints) (v0.5.7) -- [ ] **Bitácora de Eventos:** Cada acción genera un `ShipmentCheckpoint`. -- [ ] **Códigos de Checkpoint:** +- [x] **Bitácora de Eventos:** Cada acción genera un `ShipmentCheckpoint`. +- [x] **Códigos de Checkpoint:** - `Loaded` - Paquete cargado en camión (manual) - `QrScanned` - Paquete escaneado por chofer (cadena custodia) - `ArrivedHub` - Llegó a un Hub/CEDIS @@ -164,7 +164,8 @@ El sistema debe soportar los siguientes tipos de usuarios con permisos distintos - `DeliveryAttempt` - Intento de entrega - `Delivered` - Entregado exitosamente - `Exception` - Problema reportado -- [ ] **Inmutabilidad:** Los checkpoints no se modifican, solo se agregan nuevos. +- [x] **Inmutabilidad:** Los checkpoints no se modifican, solo se agregan nuevos. +- [x] **Timeline Metro:** `GET /api/shipment-checkpoints/timeline/{id}` con labels en español. ### Módulo 7: QR Handshake (Cadena de Custodia) @@ -175,13 +176,15 @@ El sistema debe soportar los siguientes tipos de usuarios con permisos distintos - Angular: `angularx-qrcode` - React: `react-qr-reader` -### Módulo 8: Documentación B2B +### Módulo 8: Documentación B2B (v0.5.7) -- [ ] **Orden de Servicio:** Petición inicial del cliente a Admin. -- [ ] **Carta Porte (Waybill):** Documento legal SAT con QR para inspecciones. -- [ ] **Manifiesto de Carga:** Checklist de estiba para almacenista. -- [ ] **Hoja de Ruta (TripSheet):** Itinerario con ventanas de entrega. -- [ ] **POD (Proof of Delivery):** Firma digital del receptor, timestamp, incidencias. +> Los documentos se generan dinámicamente con datos de BD. No hay almacenamiento de archivos. + +- [x] **Orden de Servicio:** `GET /api/documents/service-order/{shipmentId}` +- [x] **Carta Porte (Waybill):** `GET /api/documents/waybill/{shipmentId}` +- [x] **Manifiesto de Carga:** `GET /api/documents/manifest/{routeId}` +- [x] **Hoja de Ruta (TripSheet):** `GET /api/documents/trip-sheet/{driverId}` +- [x] **POD (Proof of Delivery):** `GET /api/documents/pod/{shipmentId}` con firma digital. ### Módulo 9: Dashboard (Panel de Control) From 39a4dd421e6b0aa372ca9a333c3733e0cd227e33 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sun, 28 Dec 2025 19:38:19 +0000 Subject: [PATCH 28/34] =?UTF-8?q?feat(python):=20integraci=C3=B3n=20micros?= =?UTF-8?q?ervicio=20Python=20Analytics=20v0.6.0-alpha?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python Microservice Integration v0.6.0-alpha ESPAÑOL: - Nuevo microservicio Python con FastAPI 0.115+ y Python 3.12 - Arquitectura Clean Architecture: domain, application, infrastructure, api - Conexión async a PostgreSQL con SQLAlchemy 2.0 + asyncpg - Health endpoints: /health, /health/db, /health/ready - Dockerfile multi-stage optimizado con non-root user - 4 tests unitarios pasando (pytest) - Nuevo archivo python-analytics.md con 10 objetivos y roadmap - Actualizada documentación: README, CHANGELOG, api-architecture, database-schema - Sistema de versionado SemVer con pre-releases (alpha/beta/rc) - Variables de entorno seguras (sin credenciales en docker-compose) ENGLISH: - New Python microservice with FastAPI 0.115+ and Python 3.12 - Clean Architecture: domain, application, infrastructure, api layers - Async PostgreSQL connection with SQLAlchemy 2.0 + asyncpg - Health endpoints: /health, /health/db, /health/ready - Optimized multi-stage Dockerfile with non-root user - 4 unit tests passing (pytest) - New python-analytics.md with 10 objectives and roadmap - Updated docs: README, CHANGELOG, api-architecture, database-schema - SemVer versioning with pre-releases (alpha/beta/rc) - Secure environment variables (no credentials in docker-compose) Archivos nuevos / New files: - service-python/ (25 archivos) - python-analytics.md Contenedores activos / Active containers: 9 (all healthy) --- .github/workflows/ci.yml | 57 ++- .gitignore | 21 + CHANGELOG.md | 64 +++ README.md | 194 +++++++-- api-architecture.md | 60 ++- database-schema.md | 9 +- docker-compose.yml | 51 ++- docs/api-reference.md | 45 ++- python-analytics.md | 373 ++++++++++++++++++ requirments.md | 59 +++ service-python/.env.example | 21 + service-python/Dockerfile | 66 ++++ service-python/README.md | 88 +++++ service-python/pyproject.toml | 124 ++++++ service-python/requirements.txt | 28 ++ service-python/src/parhelion_py/__init__.py | 15 + .../src/parhelion_py/api/__init__.py | 1 + .../parhelion_py/api/middleware/__init__.py | 1 + .../src/parhelion_py/api/routers/__init__.py | 5 + .../src/parhelion_py/api/routers/health.py | 74 ++++ .../src/parhelion_py/application/__init__.py | 1 + .../parhelion_py/application/dtos/__init__.py | 1 + .../application/interfaces/__init__.py | 1 + .../application/services/__init__.py | 1 + .../src/parhelion_py/domain/__init__.py | 1 + .../parhelion_py/domain/entities/__init__.py | 1 + .../domain/exceptions/__init__.py | 1 + .../domain/interfaces/__init__.py | 1 + .../domain/value_objects/__init__.py | 1 + .../parhelion_py/infrastructure/__init__.py | 1 + .../infrastructure/config/__init__.py | 5 + .../infrastructure/config/settings.py | 64 +++ .../infrastructure/database/__init__.py | 9 + .../infrastructure/database/connection.py | 82 ++++ .../infrastructure/external/__init__.py | 1 + service-python/src/parhelion_py/main.py | 73 ++++ service-python/tests/__init__.py | 1 + service-python/tests/conftest.py | 23 ++ service-python/tests/integration/__init__.py | 1 + service-python/tests/unit/__init__.py | 1 + service-python/tests/unit/test_health.py | 51 +++ service-webhooks.md | 59 +++ 42 files changed, 1681 insertions(+), 55 deletions(-) create mode 100644 python-analytics.md create mode 100644 service-python/.env.example create mode 100644 service-python/Dockerfile create mode 100644 service-python/README.md create mode 100644 service-python/pyproject.toml create mode 100644 service-python/requirements.txt create mode 100644 service-python/src/parhelion_py/__init__.py create mode 100644 service-python/src/parhelion_py/api/__init__.py create mode 100644 service-python/src/parhelion_py/api/middleware/__init__.py create mode 100644 service-python/src/parhelion_py/api/routers/__init__.py create mode 100644 service-python/src/parhelion_py/api/routers/health.py create mode 100644 service-python/src/parhelion_py/application/__init__.py create mode 100644 service-python/src/parhelion_py/application/dtos/__init__.py create mode 100644 service-python/src/parhelion_py/application/interfaces/__init__.py create mode 100644 service-python/src/parhelion_py/application/services/__init__.py create mode 100644 service-python/src/parhelion_py/domain/__init__.py create mode 100644 service-python/src/parhelion_py/domain/entities/__init__.py create mode 100644 service-python/src/parhelion_py/domain/exceptions/__init__.py create mode 100644 service-python/src/parhelion_py/domain/interfaces/__init__.py create mode 100644 service-python/src/parhelion_py/domain/value_objects/__init__.py create mode 100644 service-python/src/parhelion_py/infrastructure/__init__.py create mode 100644 service-python/src/parhelion_py/infrastructure/config/__init__.py create mode 100644 service-python/src/parhelion_py/infrastructure/config/settings.py create mode 100644 service-python/src/parhelion_py/infrastructure/database/__init__.py create mode 100644 service-python/src/parhelion_py/infrastructure/database/connection.py create mode 100644 service-python/src/parhelion_py/infrastructure/external/__init__.py create mode 100644 service-python/src/parhelion_py/main.py create mode 100644 service-python/tests/__init__.py create mode 100644 service-python/tests/conftest.py create mode 100644 service-python/tests/integration/__init__.py create mode 100644 service-python/tests/unit/__init__.py create mode 100644 service-python/tests/unit/test_health.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f760c5..5ae7be0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ # =================================== # PARHELION CI - Build & Test Pipeline # Se ejecuta en cada push/PR a develop y main -# v0.5.1: Foundation + Repository Pattern + xUnit Tests +# v0.6.0-alpha: Python Microservice Integration # =================================== name: CI Pipeline @@ -176,6 +176,61 @@ jobs: - name: Build all images run: docker compose build + # ===== PYTHON ANALYTICS SERVICE (v0.6.0+) ===== + # Descomentar cuando service-python/ esté implementado + # python-analytics: + # name: Python Build & Tests + # runs-on: ubuntu-latest + # defaults: + # run: + # working-directory: ./service-python + # + # services: + # postgres: + # image: postgres:17 + # env: + # POSTGRES_USER: parhelion_test + # POSTGRES_PASSWORD: test_password_ci + # POSTGRES_DB: parhelion_test_db + # ports: + # - 5432:5432 + # options: >- + # --health-cmd pg_isready + # --health-interval 10s + # --health-timeout 5s + # --health-retries 5 + # + # env: + # DATABASE_URL: "postgresql+asyncpg://parhelion_test:test_password_ci@localhost:5432/parhelion_test_db" + # + # steps: + # - uses: actions/checkout@v4 + # + # - name: Setup Python 3.12 + # uses: actions/setup-python@v5 + # with: + # python-version: "3.12" + # cache: "pip" + # + # - name: Install dependencies + # run: pip install -r requirements.txt + # + # - name: Lint with Ruff + # run: ruff check src/ + # + # - name: Type check with MyPy + # run: mypy src/ + # + # - name: Run pytest + # run: pytest tests/ -v --tb=short + # + # - name: Upload Coverage + # uses: actions/upload-artifact@v4 + # if: always() + # with: + # name: python-coverage + # path: htmlcov/ + # ===== RESUMEN FINAL ===== summary: name: All Checks Passed diff --git a/.gitignore b/.gitignore index 3f5a850..00b398a 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,27 @@ TestResults/ *.temp *.bak +# ===== PYTHON ===== +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +ENV/ +env/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ +*.egg-info/ +*.egg +pip-log.txt +pip-delete-this-directory.txt +.ipynb_checkpoints/ + # ===== Local Development Tools (NEVER in production) ===== # Control panel is a private development tool - never committed to repository control-panel/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c5b10a..7c674fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,70 @@ Historial de cambios del proyecto Parhelion Logistics. --- +## [0.6.0-alpha] - 2025-12-28 (En Progreso) + +### Nuevo Sistema de Versionado + +A partir de esta versión, el proyecto adopta **Semantic Versioning (SemVer)** estricto con pre-releases: + +``` +MAJOR.MINOR.PATCH-PRERELEASE+BUILD +Ejemplo: 0.6.0-alpha.1+build.2025.12.28 +``` + +| Etapa | Significado | +| ------- | ------------------------------------------- | +| `alpha` | Desarrollo activo, funcionalidad incompleta | +| `beta` | Feature-complete, en testing | +| `rc` | Release Candidate, listo para producción | + +### Agregado + +- **Python Analytics Service** (Microservicio local): + + - Framework: FastAPI 0.115+ con Python 3.12 + - Arquitectura: Clean Architecture (domain, application, infrastructure, api) + - ORM: SQLAlchemy 2.0 + asyncpg (async PostgreSQL) + - Bounded Context: Analytics & Predictions (separado del Core .NET) + - Puerto interno: 8000 + - Container name: `parhelion-python` + +- **Preparativos de Integración**: + + - Documentación actualizada: README.md, api-architecture.md, database-schema.md + - `.gitignore` con patrones Python (**pycache**, .venv, .pytest_cache, etc.) + - Nuevo roadmap hacia v1.0.0 MVP (Q1 2026) + - Sistema de versionado SemVer con staged releases (alpha → beta → rc) + +### Modificado + +- `docker-compose.yml` - Preparado para servicio `python-analytics` +- `README.md` - Stack tecnológico expandido con Python/FastAPI +- `.github/workflows/ci.yml` - Estructura preparada para job Python + +### Arquitectura + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Docker Network │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │ +│ │ .NET API│◄─┤PostgreSQL├─►│ Python │ │ n8n │ │ +│ │ :5000 │ │ :5432 │ │ :8000 │ │ :5678 │ │ +│ └────┬────┘ └─────────┘ └────┬────┘ └────────┬────────┘ │ +│ │ │ │ │ +│ └─────────────────────────┴─────────────────┘ │ +│ Internal REST/JSON │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Notas de Migración + +- Nueva variable de entorno requerida: `INTERNAL_SERVICE_KEY` para auth inter-servicios +- Volume nuevo: `python_cache` para modelos ML (futuro) +- El microservicio Python es local (como n8n), no expuesto públicamente + +--- + ## [0.5.7] - 2025-12-23 ### Agregado diff --git a/README.md b/README.md index 5d068fc..efd255f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ ![Parhelion-Logistics Banner](./bannerlogo.png) ![.NET 8](https://img.shields.io/badge/.NET%208-512BD4?style=for-the-badge&logo=dotnet&logoColor=white) +![Python](https://img.shields.io/badge/Python%203.12-3776AB?style=for-the-badge&logo=python&logoColor=FFD43B) +![FastAPI](https://img.shields.io/badge/FastAPI-009688?style=for-the-badge&logo=fastapi&logoColor=white) ![Angular](https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white) ![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) @@ -10,9 +12,9 @@ ![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white) ![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge) -Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant con **agentes de IA automatizados**. +Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona inventarios, flotas tipificadas, redes Hub & Spoke y documentación legal (Carta Porte) en un entorno Multi-tenant con **agentes de IA automatizados** y **análisis predictivo con Python**. -> **Estado:** Development Preview v0.5.7 - Dynamic PDF Generation + Checkpoint Timeline + POD Signatures +> **Estado:** Development Preview v0.6.0-alpha - Python Microservice Integration --- @@ -20,7 +22,7 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in **Parhelion-Logistics** es una plataforma SaaS multi-tenant de nivel Enterprise que unifica las capacidades de un WMS (Warehouse Management System) y un TMS (Transportation Management System). Diseñada para empresas de transporte B2B que requieren gestión integral: inventarios estáticos en almacén, flotas tipificadas (refrigerado, HAZMAT, blindado), redes de distribución Hub & Spoke, trazabilidad por checkpoints y documentación legal mexicana (Carta Porte, POD). -**Objetivo Técnico:** Implementación de **Clean Architecture** y **Domain-Driven Design (DDD)** en un entorno de producción utilizando .NET 8, Angular, React, Docker, PostgreSQL y **n8n** para automatización inteligente. +**Objetivo Técnico:** Implementación de **Clean Architecture** y **Domain-Driven Design (DDD)** en un entorno de producción utilizando .NET 8, **Python (FastAPI)**, Angular, React, Docker, PostgreSQL y **n8n** para automatización inteligente. --- @@ -102,13 +104,76 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in --- +## 🐍 Python Analytics Service (v0.6.0+) + +Microservicio dedicado para **análisis avanzado, predicciones ML y reportes**. Implementado con Clean Architecture en Python. + +### Arquitectura + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Docker Network │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ +│ │ .NET API │◄─────►│ Python │◄─────►│ PostgreSQL│ │ +│ │ :5000 │ REST │ :8000 │ async │ :5432 │ │ +│ └──────┬──────┘ └──────┬──────┘ └───────────┘ │ +│ │ │ │ +│ └─────────┬───────────┘ │ +│ ▼ │ +│ ┌───────────┐ │ +│ │ n8n │ (Workflows + IA) │ +│ │ :5678 │ │ +│ └───────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Capacidades + +| Módulo | Descripción | Estado | +| ---------------------- | -------------------------------------------------- | --------- | +| **Health Monitoring** | Endpoints `/health`, `/health/db`, `/health/ready` | ✅ v0.6.0 | +| **Shipment Analytics** | Métricas históricas por período, tenant, ruta | 📋 v0.7.x | +| **Fleet Analytics** | KPIs de ocupación, tiempo muerto, distancia | 📋 v0.7.x | +| **ETA Prediction** | Modelo ML basado en historial de checkpoints | 📋 v0.8.x | +| **Excel Reports** | Generación dinámica con pandas + openpyxl | 📋 v0.9.x | + +### Endpoints Python + +```bash +GET /health # Estado del servicio +GET /health/db # Conectividad PostgreSQL +GET /health/ready # Readiness probe + +# Próximamente (v0.7.x+) +GET /api/py/analytics/shipments +GET /api/py/analytics/fleet +POST /api/py/predictions/eta +POST /api/py/reports/export +``` + +### Tecnologías + +| Componente | Tecnología | +| ---------- | ------------------------ | +| Framework | FastAPI 0.115+ | +| Runtime | Python 3.12 | +| ORM | SQLAlchemy 2.0 + asyncpg | +| Validación | Pydantic v2 | +| Testing | pytest + pytest-asyncio | +| Linting | Ruff + MyPy | + +--- + ## Stack Tecnológico | Capa | Tecnología | Usuario | | :----------------------- | :------------------------------------ | :---------- | | **Backend** | C# / .NET 8 Web API | - | +| **Analytics Service** | Python 3.12 / FastAPI | - | | **Base de Datos** | PostgreSQL 17 | - | -| **ORM** | Entity Framework Core (Code First) | - | +| **ORM (.NET)** | Entity Framework Core (Code First) | - | +| **ORM (Python)** | SQLAlchemy 2.0 + asyncpg | - | | **Automatización** | n8n (Workflow Automation) | Agentes IA | | **Frontend (Admin)** | Angular 18+ (Material Design) | Admin | | **Frontend (Operacion)** | React + Vite + Tailwind CSS (PWA) | Almacenista | @@ -257,24 +322,33 @@ Para más detalles técnicos, ver [Sección 12 de database-schema.md](./database ## Estructura del Proyecto ``` -src/ +backend/src/ ├── Parhelion.Domain/ # Núcleo: Entidades y Excepciones (Sin dependencias) ├── Parhelion.Application/ # Reglas: DTOs, Interfaces, Validaciones ├── Parhelion.Infrastructure/ # Persistencia: DbContext, Repositorios, Migraciones └── Parhelion.API/ # Entrada: Controllers, JWT Config, DI + +service-python/ # 🐍 Microservicio Python (Analytics & Predictions) +├── src/parhelion_py/ # Clean Architecture: domain, application, infrastructure, api +│ ├── domain/ # Entidades, Value Objects, Interfaces +│ ├── application/ # DTOs, Services, Use Cases +│ ├── infrastructure/ # Database, External Clients +│ └── api/ # FastAPI Routers, Middleware +└── tests/ # pytest unit/integration tests ``` --- ## Documentacion -| Documento | Descripcion | -| :----------------------------------------------- | :-------------------------------------------- | -| [Requerimientos (MVP)](./requirments.md) | Especificacion funcional completa del sistema | -| [Esquema de Base de Datos](./database-schema.md) | Diagrama ER, entidades y reglas de negocio | -| [Arquitectura de API](./api-architecture.md) | Estructura de capas y endpoints (v0.5.7) | -| [Guía de Webhooks](./service-webhooks.md) | Integración n8n, eventos y notificaciones | -| [CHANGELOG](./CHANGELOG.md) | Historial detallado de todas las versiones | +| Documento | Descripcion | +| :----------------------------------------------- | :------------------------------------------------ | +| [Requerimientos (MVP)](./requirments.md) | Especificacion funcional completa del sistema | +| [Esquema de Base de Datos](./database-schema.md) | Diagrama ER, entidades y reglas de negocio | +| [Arquitectura de API](./api-architecture.md) | Estructura de capas y endpoints (.NET + Python) | +| [Python Analytics](./python-analytics.md) | 🐍 Roadmap, 10 objetivos, estructura del servicio | +| [Guía de Webhooks](./service-webhooks.md) | Integración n8n, eventos y notificaciones | +| [CHANGELOG](./CHANGELOG.md) | Historial detallado de todas las versiones | --- @@ -307,28 +381,80 @@ src/ ### Completado -| Version | Fecha | Descripcion | -| ---------- | ----------- | --------------------------------------------------------------- | -| v0.1.0 | 2025-12 | Estructura inicial, documentación de requerimientos | -| v0.2.0 | 2025-12 | Domain Layer: Entidades base y enumeraciones | -| v0.3.0 | 2025-12 | Infrastructure Layer: EF Core, PostgreSQL, Migrations | -| v0.4.0 | 2025-12 | API Layer: Controllers base, JWT Authentication | -| v0.5.0 | 2025-12 | Services Layer: Repository Pattern, UnitOfWork | -| v0.5.1 | 2025-12 | Foundation Tests: DTOs, Repository, UnitOfWork | -| v0.5.2 | 2025-12 | Services Implementation: 16 interfaces, 15 implementaciones | -| v0.5.3 | 2025-12 | Integration Tests: 72 tests para Services | -| v0.5.4 | 2025-12 | Swagger/OpenAPI, Business Logic Workflow | -| v0.5.5 | 2025-12 | WMS/TMS Services, Business Rules, 122 tests | -| v0.5.6 | 2025-12 | n8n Integration, Webhooks, Notifications, ServiceApiKey | -| **v0.5.7** | **2025-12** | **Dynamic PDF Generation, Checkpoint Timeline, POD Signatures** | - -### Próximas Versiones (Pre-0.6.0) - -| Version | Objetivo | Características | -| ---------- | ---------------- | -------------------------------------------------- | -| v0.5.8 | QR Handshake | Transferencia de custodia digital via QR | -| v0.5.9 | Route Assignment | Asignación de rutas a shipments, avance por pasos | -| **v0.6.0** | **Dashboard** | **KPIs operativos, métricas por status, Frontend** | +| Version | Fecha | Descripcion | +| ------- | ------- | ----------------------------------------------------------- | +| v0.1.0 | 2025-12 | Estructura inicial, documentación de requerimientos | +| v0.2.0 | 2025-12 | Domain Layer: Entidades base y enumeraciones | +| v0.3.0 | 2025-12 | Infrastructure Layer: EF Core, PostgreSQL, Migrations | +| v0.4.0 | 2025-12 | API Layer: Controllers base, JWT Authentication | +| v0.5.0 | 2025-12 | Services Layer: Repository Pattern, UnitOfWork | +| v0.5.1 | 2025-12 | Foundation Tests: DTOs, Repository, UnitOfWork | +| v0.5.2 | 2025-12 | Services Implementation: 16 interfaces, 15 implementaciones | +| v0.5.3 | 2025-12 | Integration Tests: 72 tests para Services | +| v0.5.4 | 2025-12 | Swagger/OpenAPI, Business Logic Workflow | +| v0.5.5 | 2025-12 | WMS/TMS Services, Business Rules, 122 tests | +| v0.5.6 | 2025-12 | n8n Integration, Webhooks, Notifications, ServiceApiKey | +| v0.5.7 | 2025-12 | Dynamic PDF Generation, Checkpoint Timeline, POD Signatures | + +### En Progreso (v0.6.x - Python Integration) + +| Version | Nombre Clave | Descripción | +| ---------------- | ------------- | -------------------------------------------------- | +| **v0.6.0-alpha** | `foundation` | Estructura base Python, Docker, health checks | +| v0.6.0-beta | `integration` | Comunicación API ↔ Python, autenticación interna | +| v0.6.0-rc.1 | `validation` | Tests de integración, documentación | +| **v0.6.0** | `Python Core` | Release estable con microservicio Python integrado | + +### Próximas Versiones (v0.7.x - v1.0.0) + +#### v0.7.x - Operaciones de Campo (QR + Rutas) + +| Version | Feature | Descripción | +| ------- | ---------------- | -------------------------------------------------- | +| v0.7.0 | QR Generation | Generación de códigos QR por envío (Angular Admin) | +| v0.7.1 | QR Scanning | Lectura QR en React PWA (Driver + Operaciones) | +| v0.7.2 | Custody Transfer | Transferencia de custodia digital con checkpoint | +| v0.7.3 | Route Assignment | Asignación de rutas predefinidas a shipments | +| v0.7.4 | Route Progress | Avance automático por pasos de ruta | + +#### v0.8.x - Frontend Admin Panel (Angular) + +| Version | Feature | Descripción | +| ------- | ----------------- | --------------------------------------------- | +| v0.8.0 | Admin Shell | Layout, navegación, auth guards, interceptors | +| v0.8.1 | Core CRUD | Gestión de Tenants, Users, Roles, Employees | +| v0.8.2 | Fleet CRUD | Gestión de Trucks, Drivers, FleetLogs | +| v0.8.3 | Shipment CRUD | Crear envíos, asignar chofer/camión, items | +| v0.8.4 | Shipment Tracking | Timeline de checkpoints, status updates | +| v0.8.5 | Network CRUD | Gestión de Locations, Routes, NetworkLinks | + +#### v0.9.x - Frontend PWAs + Dashboard + +| Version | Feature | Descripción | +| ------- | -------------------- | ---------------------------------------------- | +| v0.9.0 | Operaciones PWA | App tablet: login, lista de envíos, carga | +| v0.9.1 | Operaciones QR | Escaneo QR, validación peso/volumen | +| v0.9.2 | Driver PWA | App móvil: login, hoja de ruta, navegación | +| v0.9.3 | Driver Confirmations | Confirmar llegadas, entregas, firma POD | +| v0.9.4 | Dashboard Base | KPIs principales: envíos por status, ocupación | +| v0.9.5 | Dashboard Analytics | Métricas con Python: tendencias, predicciones | +| v0.9.6 | AI Predictions | Predicción ETA, alertas de retraso | + +#### v1.0.0 - MVP Release (Q1 2026) + +| Criterio | Requerimiento | +| ---------------- | ---------------------------------------- | +| Backend API | 100% endpoints funcionales con tests | +| Python Analytics | Análisis y predicciones operativas | +| Admin Panel | CRUD completo para todas las entidades | +| Operaciones PWA | Funcional para almacenistas | +| Driver App PWA | Funcional para choferes con firma POD | +| Dashboard | KPIs operativos en tiempo real | +| Documentación | README, API docs, Swagger actualizados | +| Deployment | Docker + Cloudflare Tunnel en producción | + +> **Nota:** Cada versión x.y.z puede completarse en días, no semanas. +> Las funcionalidades se entregan incrementalmente siguiendo Agile. --- diff --git a/api-architecture.md b/api-architecture.md index d8091a9..393b27e 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -4,9 +4,9 @@ Documentacion tecnica de la estructura API-First del backend Parhelion. ## Estado Actual -**Version:** 0.5.7 -**Enfoque:** Dynamic PDF Generation + Checkpoint Timeline + POD Signatures -**Arquitectura:** Clean Architecture + Domain-Driven Design +**Version:** 0.6.0-alpha +**Enfoque:** Python Microservice Integration + Analytics Foundation +**Arquitectura:** Clean Architecture + Domain-Driven Design + Microservices --- @@ -169,15 +169,61 @@ El token se obtiene via `/api/auth/login` con credenciales validas. --- -## Pendientes (v0.5.8+) +## Python Analytics Service (v0.6.0+) + +Microservicio local para análisis avanzado, predicciones y reportes. + +### Tecnologías + +| Componente | Tecnología | +| ---------- | ------------------------ | +| Framework | FastAPI 0.115+ | +| Runtime | Python 3.12+ | +| ORM | SQLAlchemy 2.0 + asyncpg | +| Validación | Pydantic v2 | +| Testing | pytest + pytest-asyncio | + +### Endpoints Python + +| Endpoint | Método | Descripción | +| ----------------------------- | ------ | -------------------------------- | +| `/health` | GET | Estado del servicio | +| `/health/db` | GET | Conectividad PostgreSQL | +| `/api/py/analytics/shipments` | GET | Análisis de envíos por período | +| `/api/py/analytics/fleet` | GET | Métricas de utilización de flota | +| `/api/py/predictions/eta` | POST | Predicción de ETA con ML | +| `/api/py/reports/export` | POST | Generación de reportes Excel | + +### Autenticación Python + +Requiere header `X-Internal-Service-Key` para llamadas desde .NET API, +o `Authorization: Bearer ` para llamadas desde n8n. + +### Comunicación Inter-Servicios + +``` +┌─────────────┐ REST/JSON ┌─────────────┐ +│ .NET API │◄──────────────────►│ Python │ +│ :5000 │ Internal JWT/Key │ :8000 │ +└──────┬──────┘ └──────┬──────┘ + │ │ + └──────────► PostgreSQL ◄──────────┘ + :5432 +``` + +--- + +## Pendientes (v0.7.0+) Los siguientes items quedan pendientes para futuras versiones: - [ ] QR Handshake (Transferencia de custodia digital via QR) - [ ] Route Assignment (Asignación de rutas a shipments) -- [ ] Dashboard (KPIs operativos, métricas por status) +- [ ] Dashboard (KPIs operativos con procesamiento Python) +- [ ] Predicción ETA con ML (Python) +- [ ] Exportación Excel dinámica (Python + pandas) - [ ] Recuperación de contraseña -- [ ] Cálculo de ETA dinámico +- [ ] Demo Mode para reclutadores --- @@ -187,4 +233,4 @@ La gestion de endpoints durante desarrollo utiliza herramientas privadas que no --- -**Ultima actualizacion:** 2025-12-23 +**Ultima actualizacion:** 2025-12-28 diff --git a/database-schema.md b/database-schema.md index 991afa2..eb9b7df 100644 --- a/database-schema.md +++ b/database-schema.md @@ -1,9 +1,9 @@ # PARHELION-LOGISTICS | Modelo de Base de Datos -**Version:** 2.6 (v0.5.6 - n8n Integration + Webhooks + Notifications) +**Version:** 2.7 (v0.6.0-alpha - Python Analytics Integration) **Fecha:** Diciembre 2025 -**Motor:** PostgreSQL + Entity Framework Core (Code First) -**Estado:** Diseno Cerrado - Listo para Implementacion +**Motor:** PostgreSQL + Entity Framework Core (Code First) + SQLAlchemy (Python) +**Estado:** Diseno Abierto - Integracion Python en Progreso > **Nota Tecnica:** Esta plataforma unifica WMS (Warehouse Management System) y TMS (Transportation Management System). El modulo de almacen gestiona inventario estatico y carga, mientras que el nucleo TMS maneja logistica de media milla: gestion de flotas tipificadas, redes Hub & Spoke y trazabilidad de envios en movimiento. @@ -85,7 +85,7 @@ erDiagram --- -## 1.1 Entidades del Sistema (25 tablas) +## 1.1 Entidades del Sistema (27 tablas) | Modulo | Entidades | | ------------- | ------------------------------------------------------------ | @@ -98,6 +98,7 @@ erDiagram | **Routing** | RouteBlueprint, RouteStep, NetworkLink | | **CRM** | Client | | **n8n** | Notification | +| **Analytics** | AnalyticsSession, PredictionResult (Python Service) | --- diff --git a/docker-compose.yml b/docker-compose.yml index 28ce1cb..6fc5abd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,9 @@ # =================================== # PARHELION LOGISTICS - Docker Compose -# Version: 0.5.4 +# Version: 0.6.0-alpha # Red unificada: parhelion-network # Swagger UI: http://localhost:5100/swagger +# Python Analytics: http://localhost:8000 (v0.6.0+) # =================================== services: @@ -13,8 +14,8 @@ services: restart: unless-stopped environment: POSTGRES_DB: parhelion_dev - POSTGRES_USER: ${DB_USER:-MetaCodeX} - POSTGRES_PASSWORD: ${DB_PASSWORD:-H4NZC0D3X1521} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres_pgdata:/var/lib/postgresql/data ports: @@ -23,7 +24,7 @@ services: - parhelion-network healthcheck: test: - ["CMD-SHELL", "pg_isready -U ${DB_USER:-MetaCodeX} -d parhelion_dev"] + ["CMD-SHELL", "pg_isready -U ${DB_USER} -d parhelion_dev"] interval: 10s timeout: 5s retries: 5 @@ -40,7 +41,7 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Production - ASPNETCORE_URLS=http://+:5000 - - ConnectionStrings__DefaultConnection=Host=postgres;Database=parhelion_dev;Username=${DB_USER:-MetaCodeX};Password=${DB_PASSWORD:-H4NZC0D3X1521} + - ConnectionStrings__DefaultConnection=Host=postgres;Database=parhelion_dev;Username=${DB_USER};Password=${DB_PASSWORD} - JWT_SECRET=${JWT_SECRET} - N8N_WEBHOOK_SECRET=${N8N_WEBHOOK_SECRET} - N8N_BASE_URL=${N8N_BASE_URL:-http://parhelion-n8n:5678} @@ -124,6 +125,42 @@ services: timeout: 10s retries: 3 + # ===== PYTHON ANALYTICS SERVICE (v0.6.0) ===== + python-analytics: + build: + context: ./service-python + dockerfile: Dockerfile + container_name: parhelion-python + restart: unless-stopped + ports: + - "${PYTHON_PORT:-8000}:8000" + environment: + # ===== DATABASE (Read Replica Pattern) ===== + - DATABASE_URL=postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@postgres:5432/parhelion_dev + # ===== INTERNAL SERVICES ===== + - PARHELION_API_URL=http://parhelion-api:5000 + - PARHELION_API_INTERNAL_KEY=${INTERNAL_SERVICE_KEY} + # ===== SECURITY ===== + - JWT_SECRET=${JWT_SECRET} + - SERVICE_NAME=python-analytics + # ===== RUNTIME ===== + - ENVIRONMENT=production + - LOG_LEVEL=info + - WORKERS=4 + depends_on: + postgres: + condition: service_healthy + api: + condition: service_healthy + networks: + - parhelion-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + # ===== N8N AI ORCHESTRATOR ===== # Docs: https://docs.n8n.io/hosting/configuration/environment-variables/ n8n: @@ -143,8 +180,8 @@ services: - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PORT=5432 - DB_POSTGRESDB_DATABASE=parhelion_dev - - DB_POSTGRESDB_USER=${DB_USER:-MetaCodeX} - - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD:-H4NZC0D3X1521} + - DB_POSTGRESDB_USER=${DB_USER} + - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD} # ===== WEBHOOKS ===== - WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://localhost:5678/} diff --git a/docs/api-reference.md b/docs/api-reference.md index 19506e2..c4735d9 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -137,5 +137,46 @@ http://localhost:5100/swagger --- -**Versión**: 0.5.0 -**Última actualización**: 2025-12-14 +**Versión**: 0.6.0-alpha +**Última actualización**: 2025-12-28 + +--- + +## Python Analytics Service (v0.6.0+) + +Microservicio local para análisis avanzado y predicciones. + +**Base URL:** `http://localhost:8000` (interno: `http://parhelion-python:8000`) + +### Health Endpoints + +```bash +GET /health # Service status +GET /health/db # Database connectivity +``` + +### Analytics Endpoints + +```bash +GET /api/py/analytics/shipments # Análisis de envíos por período +GET /api/py/analytics/fleet # Métricas de utilización de flota +``` + +### Predictions Endpoints + +```bash +POST /api/py/predictions/eta # Predicción de ETA con ML +``` + +### Reports Endpoints + +```bash +POST /api/py/reports/export # Generación de reportes Excel +``` + +### Autenticación Python + +```http +X-Internal-Service-Key: # Desde .NET API +Authorization: Bearer # Desde n8n +``` diff --git a/python-analytics.md b/python-analytics.md new file mode 100644 index 0000000..ec85f3c --- /dev/null +++ b/python-analytics.md @@ -0,0 +1,373 @@ +# Python Analytics Service - Documentación Técnica + +**Version:** 0.6.0-alpha +**Bounded Context:** Analytics & Predictions +**Puerto:** 8000 +**Container:** parhelion-python + +--- + +## 1. Visión General + +El Python Analytics Service es un microservicio dedicado al **análisis avanzado de datos logísticos** y **predicciones basadas en ML**. Complementa el API .NET principal con capacidades especializadas de procesamiento de datos. + +```mermaid +flowchart LR + subgraph Frontend + A[Angular Admin] + B[React PWAs] + end + + subgraph Backend + C[".NET API\n:5000"] + D["Python Analytics\n:8000"] + end + + subgraph Data + E[(PostgreSQL)] + F[n8n Workflows] + end + + A & B --> C + C <--> D + C & D --> E + F --> C & D +``` + +--- + +## 2. Los 10 Objetivos del Python Service + +| # | Objetivo | Versión | Prioridad | +| --- | ----------------------------------------------------------------- | ----------- | --------- | +| 1 | **Health Monitoring** - Endpoints de estado y conectividad | v0.6.0 ✅ | P0 | +| 2 | **Database Integration** - Conexión async a PostgreSQL compartido | v0.6.0 ✅ | P0 | +| 3 | **Service Auth** - Autenticación inter-servicios con .NET | v0.6.0-beta | P0 | +| 4 | **Shipment Analytics** - Métricas históricas de envíos | v0.7.x | P1 | +| 5 | **Fleet Analytics** - KPIs de utilización de flota | v0.7.x | P1 | +| 6 | **ETA Prediction** - ML para estimación de tiempos | v0.8.x | P2 | +| 7 | **Anomaly Detection** - Alertas de retrasos potenciales | v0.8.x | P2 | +| 8 | **Excel Reports** - Generación dinámica con pandas | v0.9.x | P2 | +| 9 | **Dashboard Data** - Endpoints para KPIs en tiempo real | v0.9.x | P1 | +| 10 | **n8n Integration** - Callbacks y eventos bidireccionales | v0.9.x | P1 | + +--- + +## 3. Estructura Actual (v0.6.0-alpha) + +``` +service-python/ +├── pyproject.toml # Configuración del proyecto +├── requirements.txt # Dependencias para Docker +├── Dockerfile # Multi-stage build +├── README.md # Documentación del servicio +├── .env.example # Template de variables +│ +├── src/parhelion_py/ +│ ├── __init__.py # Package metadata +│ ├── main.py # FastAPI entry point +│ │ +│ ├── domain/ # 🔷 DOMAIN LAYER +│ │ ├── entities/ # (vacío - v0.7.x) +│ │ ├── value_objects/ # (vacío - v0.7.x) +│ │ ├── exceptions/ # (vacío - v0.7.x) +│ │ └── interfaces/ # (vacío - v0.7.x) +│ │ +│ ├── application/ # 🔶 APPLICATION LAYER +│ │ ├── dtos/ # (vacío - v0.7.x) +│ │ ├── services/ # (vacío - v0.7.x) +│ │ └── interfaces/ # (vacío - v0.7.x) +│ │ +│ ├── infrastructure/ # 🔵 INFRASTRUCTURE LAYER +│ │ ├── config/ +│ │ │ └── settings.py # ✅ Pydantic Settings +│ │ ├── database/ +│ │ │ └── connection.py # ✅ SQLAlchemy async +│ │ └── external/ # (vacío - v0.6.0-beta) +│ │ +│ └── api/ # 🟢 API LAYER +│ ├── routers/ +│ │ └── health.py # ✅ /health, /health/db, /health/ready +│ └── middleware/ # (vacío - v0.6.0-beta) +│ +└── tests/ + ├── conftest.py # ✅ pytest fixtures + └── unit/ + └── test_health.py # ✅ 4 tests pasando +``` + +### Componentes Implementados (v0.6.0-alpha) + +| Componente | Archivo | Estado | +| ------------- | --------------------------------------- | ------ | +| FastAPI App | `main.py` | ✅ | +| Settings | `infrastructure/config/settings.py` | ✅ | +| DB Connection | `infrastructure/database/connection.py` | ✅ | +| Health Router | `api/routers/health.py` | ✅ | +| Unit Tests | `tests/unit/test_health.py` | ✅ 4/4 | + +--- + +## 4. Estructura Planeada (v0.7.x - v0.9.x) + +``` +service-python/src/parhelion_py/ +│ +├── domain/ +│ ├── entities/ +│ │ ├── base.py # BaseEntity, TenantEntity +│ │ ├── analytics_session.py # AnalyticsSession +│ │ └── prediction_result.py # PredictionResult +│ ├── value_objects/ +│ │ ├── date_range.py # DateRange VO +│ │ └── metrics.py # ShipmentMetrics, FleetMetrics +│ ├── exceptions/ +│ │ └── analytics_errors.py # AnalyticsNotFoundError, etc. +│ └── interfaces/ +│ └── repositories.py # IAnalyticsRepository (Port) +│ +├── application/ +│ ├── dtos/ +│ │ ├── requests.py # AnalyticsRequest, PredictionRequest +│ │ └── responses.py # AnalyticsResponse, ETAPrediction +│ ├── services/ +│ │ ├── analytics_service.py # ShipmentAnalytics, FleetAnalytics +│ │ ├── prediction_service.py # ETAPredictionService +│ │ └── report_service.py # ExcelReportService +│ └── interfaces/ +│ └── external_services.py # IParhelionApiClient +│ +├── infrastructure/ +│ ├── database/ +│ │ ├── connection.py # (existente) +│ │ ├── models.py # SQLAlchemy models +│ │ └── repositories.py # AnalyticsRepository (Adapter) +│ └── external/ +│ └── parhelion_client.py # Anti-Corruption Layer +│ +└── api/ + ├── routers/ + │ ├── health.py # (existente) + │ ├── analytics.py # /api/py/analytics/* + │ ├── predictions.py # /api/py/predictions/* + │ └── reports.py # /api/py/reports/* + ├── middleware/ + │ ├── auth.py # ServiceApiKey validation + │ └── tenant.py # Multi-tenant context + └── dependencies.py # FastAPI Depends() +``` + +--- + +## 5. Roadmap de Desarrollo + +### v0.6.x - Foundation (EN PROGRESO) + +```mermaid +gantt + title Python Service v0.6.x + dateFormat YYYY-MM-DD + section Foundation + v0.6.0-alpha :done, a1, 2025-12-28, 1d + v0.6.0-beta :active, a2, after a1, 3d + v0.6.0-rc.1 :a3, after a2, 2d + v0.6.0 :milestone, after a3, 0d +``` + +| Release | Nombre | Entregables | +| ---------------- | ----------- | ------------------------------------------- | +| **v0.6.0-alpha** | Foundation | ✅ Estructura, health, DB connection, tests | +| v0.6.0-beta | Integration | Auth middleware, ParhelionApiClient (ACL) | +| v0.6.0-rc.1 | Validation | E2E tests, logging estructurado | +| **v0.6.0** | Release | GitHub Actions, docs actualizadas | + +--- + +### v0.7.x - Analytics Core + +| Release | Feature | Endpoints | +| ------- | ----------------------- | --------------------------------------- | +| v0.7.0 | Shipment Analytics Base | `GET /api/py/analytics/shipments` | +| v0.7.1 | Fleet Analytics Base | `GET /api/py/analytics/fleet` | +| v0.7.2 | Filtros Avanzados | Query params: date, status, driver | +| v0.7.3 | Aggregations | Métricas por período, tenant | +| v0.7.4 | Caching | Redis/in-memory para queries frecuentes | + +--- + +### v0.8.x - Machine Learning + +| Release | Feature | Modelo | +| ------- | ------------------- | ------------------------ | +| v0.8.0 | ETA Prediction Base | Regresión lineal | +| v0.8.1 | Feature Engineering | Historial de checkpoints | +| v0.8.2 | Model Improvement | Gradient Boosting | +| v0.8.3 | Anomaly Detection | Isolation Forest | +| v0.8.4 | Confidence Scoring | Intervalos de predicción | + +--- + +### v0.9.x - Reports & Dashboard + +| Release | Feature | Output | +| ------- | ------------------- | ------------------------ | +| v0.9.0 | Excel Base | pandas + openpyxl | +| v0.9.1 | Report Templates | Diario, Semanal, Mensual | +| v0.9.2 | Dashboard Endpoints | KPIs en JSON | +| v0.9.3 | Real-time Metrics | WebSocket support | +| v0.9.4 | n8n Callbacks | Event-driven analytics | + +--- + +## 6. Diagrama de Capas (Clean Architecture) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ API LAYER │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Health │ │ Analytics │ │ Predictions │ │ +│ │ Router │ │ Router │ │ Router │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +│ │ │ │ │ +│ ┌──────┴────────────────┴────────────────┴──────┐ │ +│ │ Middleware (Auth, Tenant) │ │ +│ └────────────────────────┬──────────────────────┘ │ +├───────────────────────────┼─────────────────────────────────┤ +│ ▼ │ +│ APPLICATION LAYER │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Analytics │ │ Prediction │ │ Report │ │ +│ │ Service │ │ Service │ │ Service │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +│ │ │ │ │ +│ ┌──────┴────────────────┴────────────────┴──────┐ │ +│ │ DTOs │ │ +│ └────────────────────────┬──────────────────────┘ │ +├───────────────────────────┼─────────────────────────────────┤ +│ ▼ │ +│ DOMAIN LAYER │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Entities │ │ Value │ │ Interfaces │ │ +│ │ │ │ Objects │ │ (Ports) │ │ +│ └─────────────┘ └─────────────┘ └──────┬──────┘ │ +├───────────────────────────────────────────┼─────────────────┤ +│ ▼ │ +│ INFRASTRUCTURE LAYER │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Database │ │ External │ │ Config │ │ +│ │ (Adapters) │ │ Clients │ │ (Settings) │ │ +│ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ PostgreSQL │ │ .NET API │ │ +│ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 7. Tecnologías y Dependencias + +| Categoría | Tecnología | Versión | +| --------------- | ----------------------- | ----------- | +| **Runtime** | Python | 3.12+ | +| **Framework** | FastAPI | 0.115+ | +| **ASGI Server** | Uvicorn + Gunicorn | 0.32+ / 23+ | +| **ORM** | SQLAlchemy | 2.0+ | +| **DB Driver** | asyncpg | 0.30+ | +| **Validation** | Pydantic | 2.10+ | +| **HTTP Client** | httpx | 0.28+ | +| **Auth** | python-jose | 3.3+ | +| **Testing** | pytest + pytest-asyncio | 8.3+ | +| **Linting** | Ruff | 0.8+ | +| **Type Check** | MyPy | 1.13+ | + +### Dependencias Futuras (v0.8.x+) + +| Categoría | Tecnología | Uso | +| --------- | ------------ | ---------------------- | +| **ML** | scikit-learn | Predicciones | +| **Data** | pandas | DataFrames | +| **Math** | numpy | Operaciones numéricas | +| **Excel** | openpyxl | Generación de reportes | + +--- + +## 8. Endpoints Actuales y Planeados + +### Implementados (v0.6.0-alpha) + +```bash +GET /health # Estado del servicio +GET /health/db # Conectividad PostgreSQL +GET /health/ready # Readiness probe (K8s) +GET /docs # Swagger UI (dev only) +``` + +### Planeados (v0.7.x+) + +```bash +# Analytics +GET /api/py/analytics/shipments # Métricas de envíos +GET /api/py/analytics/fleet # Métricas de flota +GET /api/py/analytics/drivers/{id} # Por chofer + +# Predictions +POST /api/py/predictions/eta # Predicción ETA +POST /api/py/predictions/anomalies # Detección anomalías + +# Reports +POST /api/py/reports/export # Generar Excel +GET /api/py/reports/{id}/download # Descargar reporte + +# Dashboard +GET /api/py/dashboard/kpis # KPIs principales +GET /api/py/dashboard/realtime # Métricas tiempo real +``` + +--- + +## 9. Variables de Entorno + +| Variable | Requerida | Default | Descripción | +| ---------------------- | --------- | --------------------------- | --------------------------- | +| `DATABASE_URL` | ✅ | - | PostgreSQL async connection | +| `JWT_SECRET` | ✅ | - | Secret para validar tokens | +| `INTERNAL_SERVICE_KEY` | ✅ | - | Auth inter-servicios | +| `PARHELION_API_URL` | No | `http://parhelion-api:5000` | URL del API .NET | +| `ENVIRONMENT` | No | `development` | dev/production/testing | +| `LOG_LEVEL` | No | `info` | debug/info/warning/error | +| `WORKERS` | No | `4` | Gunicorn workers | + +--- + +## 10. Comunicación Inter-Servicios + +```mermaid +sequenceDiagram + participant Admin as Angular Admin + participant NET as .NET API + participant PY as Python Service + participant DB as PostgreSQL + participant N8N as n8n + + Admin->>NET: GET /api/shipments/analytics + NET->>PY: GET /api/py/analytics/shipments + Note over NET,PY: Header: X-Internal-Service-Key + PY->>DB: SELECT ... (async) + DB-->>PY: Results + PY-->>NET: JSON { metrics } + NET-->>Admin: JSON { analytics } + + N8N->>PY: POST /api/py/predictions/eta + Note over N8N,PY: Header: Authorization: Bearer + PY->>DB: Query historical data + PY-->>N8N: { eta, confidence } + N8N->>NET: POST /api/notifications +``` + +--- + +**Última actualización:** 2025-12-28 +**Próxima milestone:** v0.6.0-beta (Auth + ACL) diff --git a/requirments.md b/requirments.md index 541ba5e..cce4ffb 100644 --- a/requirments.md +++ b/requirments.md @@ -203,6 +203,65 @@ El sistema debe soportar los siguientes tipos de usuarios con permisos distintos - [ ] **Aislamiento:** Los datos de demo NO afectan a usuarios reales. - [ ] **Uso Portfolio:** El sistema será visible para reclutadores como demostración de habilidades técnicas. +### Módulo 11: Python Analytics Service (v0.6.0+) + +> Microservicio Python local para análisis avanzado y procesamiento de datos. + +- [ ] **Infraestructura Base:** + + - Framework: FastAPI 0.115+ con Python 3.12 + - ORM: SQLAlchemy 2.0 + asyncpg (async PostgreSQL) + - Docker service: `python-analytics` en puerto 8000 + - Health endpoints: `/health`, `/health/db` + +- [ ] **Análisis de Envíos:** + + - Métricas históricas por período, tenant, ruta + - Endpoint: `GET /api/py/analytics/shipments` + - Filtros: fecha, status, ubicación, chofer + +- [ ] **Análisis de Flota:** + - KPIs de ocupación, tiempo muerto, distancia + - Endpoint: `GET /api/py/analytics/fleet` + - Métricas por camión, chofer, tipo de unidad + +### Módulo 12: Predicciones ML (v0.7.0+) + +- [ ] **Predicción de ETA:** + + - Modelo basado en historial de checkpoints + - Endpoint: `POST /api/py/predictions/eta` + - Input: shipmentId, currentLocation + - Output: ETA estimado con nivel de confianza + +- [ ] **Detección de Anomalías:** + - Identificar envíos con riesgo de retraso + - Alertas proactivas vía webhook a n8n + +### Módulo 13: Reportes Dinámicos (v0.7.0+) + +- [ ] **Exportación Excel:** + + - Generación con pandas + openpyxl + - Endpoint: `POST /api/py/reports/export` + - Tipos: Reporte Diario, Reporte Mensual, KPIs de Flota + +- [ ] **Formatos Personalizados:** + - Templates predefinidos por tipo de reporte + - Logos y branding del tenant + +### Módulo 14: Comunicación Inter-Servicios (v0.6.0+) + +- [ ] **Autenticación Interna:** + + - Header `X-Internal-Service-Key` para .NET → Python + - Callback Token JWT para n8n → Python + - Scopes: `analytics:read`, `predictions:execute`, `reports:generate` + +- [ ] **Anti-Corruption Layer (ACL):** + - `ParhelionApiClient` en Python traduce modelos .NET + - Aislamiento de Bounded Contexts (DDD) + --- ## 4. Requerimientos Técnicos (Non-Functional) diff --git a/service-python/.env.example b/service-python/.env.example new file mode 100644 index 0000000..2288770 --- /dev/null +++ b/service-python/.env.example @@ -0,0 +1,21 @@ +# =================================== +# PARHELION PYTHON SERVICE - Environment Example +# Copy this file to .env and fill in values +# =================================== + +# === Application === +ENVIRONMENT=development +LOG_LEVEL=info +WORKERS=4 + +# === Database === +# PostgreSQL async connection string +DATABASE_URL=postgresql+asyncpg://MetaCodeX:password@localhost:5432/parhelion_dev + +# === Security === +JWT_SECRET=your-jwt-secret-here +INTERNAL_SERVICE_KEY=your-internal-key-here + +# === External Services === +PARHELION_API_URL=http://localhost:5100 +PARHELION_API_INTERNAL_KEY=same-as-internal-service-key diff --git a/service-python/Dockerfile b/service-python/Dockerfile new file mode 100644 index 0000000..33cdd7c --- /dev/null +++ b/service-python/Dockerfile @@ -0,0 +1,66 @@ +# =================================== +# PARHELION PYTHON SERVICE - Dockerfile +# Multi-stage build for production +# Version: 0.6.0-alpha +# =================================== + +# ===== BUILDER STAGE ===== +FROM python:3.12-slim AS builder + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# ===== PRODUCTION STAGE ===== +FROM python:3.12-slim AS production + +WORKDIR /app + +# Install runtime dependencies (curl for healthcheck) +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Copy virtual environment from builder +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Copy application code +COPY src/ ./src/ + +# Set Python path +ENV PYTHONPATH=/app/src +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# Create non-root user for security +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Run with Gunicorn + Uvicorn workers +CMD ["gunicorn", "parhelion_py.main:app", \ + "--worker-class", "uvicorn.workers.UvicornWorker", \ + "--bind", "0.0.0.0:8000", \ + "--workers", "4", \ + "--access-logfile", "-", \ + "--error-logfile", "-"] diff --git a/service-python/README.md b/service-python/README.md new file mode 100644 index 0000000..89be480 --- /dev/null +++ b/service-python/README.md @@ -0,0 +1,88 @@ +# Parhelion Python Analytics Service + +**Version:** 0.6.0-alpha +**Framework:** FastAPI + Python 3.12 +**Architecture:** Clean Architecture + +## Estructura + +``` +src/parhelion_py/ +├── main.py # FastAPI entry point +├── domain/ # 🔷 Core business logic (no dependencies) +│ ├── entities/ # Domain entities +│ ├── value_objects/ # Immutable value objects +│ ├── exceptions/ # Domain exceptions +│ └── interfaces/ # Repository interfaces (Ports) +├── application/ # 🔶 Use cases and DTOs +│ ├── dtos/ # Data Transfer Objects +│ ├── services/ # Application services +│ └── interfaces/ # External service interfaces +├── infrastructure/ # 🔵 External implementations +│ ├── config/ # Settings (Pydantic) +│ ├── database/ # SQLAlchemy async +│ └── external/ # HTTP clients (.NET API) +└── api/ # 🟢 HTTP layer + ├── routers/ # FastAPI routers + └── middleware/ # Auth, tenant, logging +``` + +## Desarrollo Local + +```bash +# Crear virtual environment +python -m venv .venv +source .venv/bin/activate # Linux/Mac +# .venv\Scripts\activate # Windows + +# Instalar dependencias +pip install -e ".[dev]" + +# Ejecutar servidor de desarrollo +uvicorn parhelion_py.main:app --reload --port 8000 + +# Ejecutar tests +pytest tests/ -v + +# Linting +ruff check src/ +mypy src/ +``` + +## Docker + +```bash +# Build +docker build -t parhelion-python . + +# Run +docker run -p 8000:8000 \ + -e DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/db" \ + -e JWT_SECRET="your-secret" \ + parhelion-python +``` + +## Endpoints + +| Endpoint | Método | Descripción | +| --------------- | ------ | ----------------------- | +| `/health` | GET | Estado del servicio | +| `/health/db` | GET | Conectividad PostgreSQL | +| `/health/ready` | GET | Readiness probe | +| `/docs` | GET | Swagger UI (dev only) | + +## Variables de Entorno + +| Variable | Requerida | Default | Descripción | +| ---------------------- | --------- | --------------------------- | ------------------------------ | +| `DATABASE_URL` | No | - | PostgreSQL async connection | +| `JWT_SECRET` | Sí | - | Secret para validar tokens | +| `INTERNAL_SERVICE_KEY` | Sí | - | Key para auth inter-servicios | +| `PARHELION_API_URL` | No | `http://parhelion-api:5000` | URL del API .NET | +| `ENVIRONMENT` | No | `development` | development/production/testing | + +--- + +**Bounded Context:** Analytics & Predictions +**Puerto:** 8000 +**Container:** parhelion-python diff --git a/service-python/pyproject.toml b/service-python/pyproject.toml new file mode 100644 index 0000000..10f14ad --- /dev/null +++ b/service-python/pyproject.toml @@ -0,0 +1,124 @@ +[project] +name = "parhelion-py" +version = "0.6.0-alpha" +description = "Python Analytics Service for Parhelion Logistics" +readme = "README.md" +requires-python = ">=3.12" +license = { text = "MIT" } +authors = [ + { name = "MetaCodeX", email = "dev@macrostasis.lat" } +] +keywords = ["logistics", "analytics", "fastapi", "wms", "tms"] + +dependencies = [ + # Web Framework + "fastapi>=0.115.0", + "uvicorn[standard]>=0.32.0", + + # Database + "sqlalchemy[asyncio]>=2.0.0", + "asyncpg>=0.30.0", + + # Validation & Settings + "pydantic>=2.10.0", + "pydantic-settings>=2.6.0", + + # HTTP Client (for .NET API calls) + "httpx>=0.28.0", + + # Security + "python-jose[cryptography]>=3.3.0", + + # Utilities + "python-multipart>=0.0.18", +] + +[project.optional-dependencies] +dev = [ + # Testing + "pytest>=8.3.0", + "pytest-asyncio>=0.24.0", + "pytest-cov>=6.0.0", + "httpx>=0.28.0", # For TestClient + + # Linting & Formatting + "ruff>=0.8.0", + "mypy>=1.13.0", + + # Type stubs + "types-python-jose>=3.3.0", +] + +ml = [ + # Future: ML/Analytics dependencies + "pandas>=2.2.0", + "numpy>=2.0.0", + "scikit-learn>=1.5.0", + "openpyxl>=3.1.0", # Excel export +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/parhelion_py"] + +# ===== RUFF (Linting) ===== +[tool.ruff] +target-version = "py312" +line-length = 100 +src = ["src"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG", # flake8-unused-arguments + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults +] + +[tool.ruff.lint.isort] +known-first-party = ["parhelion_py"] + +# ===== MYPY (Type Checking) ===== +[tool.mypy] +python_version = "3.12" +strict = true +warn_return_any = true +warn_unused_ignores = true +disallow_untyped_defs = true +plugins = ["pydantic.mypy"] + +[[tool.mypy.overrides]] +module = ["asyncpg.*", "jose.*"] +ignore_missing_imports = true + +# ===== PYTEST ===== +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +addopts = "-v --tb=short" + +# ===== COVERAGE ===== +[tool.coverage.run] +source = ["src/parhelion_py"] +branch = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise NotImplementedError", + "if TYPE_CHECKING:", +] diff --git a/service-python/requirements.txt b/service-python/requirements.txt new file mode 100644 index 0000000..d5019aa --- /dev/null +++ b/service-python/requirements.txt @@ -0,0 +1,28 @@ +# =================================== +# PARHELION PYTHON SERVICE - Requirements +# Generated from pyproject.toml for Docker builds +# =================================== + +# Web Framework +fastapi>=0.115.0 +uvicorn[standard]>=0.32.0 + +# Database +sqlalchemy[asyncio]>=2.0.0 +asyncpg>=0.30.0 + +# Validation & Settings +pydantic>=2.10.0 +pydantic-settings>=2.6.0 + +# HTTP Client +httpx>=0.28.0 + +# Security +python-jose[cryptography]>=3.3.0 + +# Utilities +python-multipart>=0.0.18 + +# Production Server +gunicorn>=23.0.0 diff --git a/service-python/src/parhelion_py/__init__.py b/service-python/src/parhelion_py/__init__.py new file mode 100644 index 0000000..c9ebf12 --- /dev/null +++ b/service-python/src/parhelion_py/__init__.py @@ -0,0 +1,15 @@ +""" +Parhelion Python Analytics Service +=================================== + +Microservicio para análisis avanzado y predicciones del sistema Parhelion Logistics. + +Estructura Clean Architecture: +- domain/: Entidades, Value Objects, Interfaces (sin dependencias externas) +- application/: DTOs, Services, Use Cases +- infrastructure/: Database, External Clients, Config +- api/: FastAPI Routers, Middleware +""" + +__version__ = "0.6.0-alpha" +__author__ = "MetaCodeX" diff --git a/service-python/src/parhelion_py/api/__init__.py b/service-python/src/parhelion_py/api/__init__.py new file mode 100644 index 0000000..0370f24 --- /dev/null +++ b/service-python/src/parhelion_py/api/__init__.py @@ -0,0 +1 @@ +"""API Package.""" diff --git a/service-python/src/parhelion_py/api/middleware/__init__.py b/service-python/src/parhelion_py/api/middleware/__init__.py new file mode 100644 index 0000000..2ca3716 --- /dev/null +++ b/service-python/src/parhelion_py/api/middleware/__init__.py @@ -0,0 +1 @@ +"""API Middleware Package.""" diff --git a/service-python/src/parhelion_py/api/routers/__init__.py b/service-python/src/parhelion_py/api/routers/__init__.py new file mode 100644 index 0000000..3514ead --- /dev/null +++ b/service-python/src/parhelion_py/api/routers/__init__.py @@ -0,0 +1,5 @@ +"""API Routers Package.""" + +from parhelion_py.api.routers import health + +__all__ = ["health"] diff --git a/service-python/src/parhelion_py/api/routers/health.py b/service-python/src/parhelion_py/api/routers/health.py new file mode 100644 index 0000000..eaf8733 --- /dev/null +++ b/service-python/src/parhelion_py/api/routers/health.py @@ -0,0 +1,74 @@ +""" +API Routers - Health Endpoints +============================== + +Health check endpoints for service monitoring. +""" + +from datetime import datetime, timezone +from typing import Any + +from fastapi import APIRouter + +from parhelion_py.infrastructure.config.settings import get_settings +from parhelion_py.infrastructure.database.connection import check_db_connection + +router = APIRouter() +settings = get_settings() + + +@router.get("/health") +async def health_check() -> dict[str, Any]: + """ + Basic health check endpoint. + + Returns service status and metadata. + """ + return { + "status": "healthy", + "service": settings.service_name, + "version": settings.version, + "environment": settings.environment, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + + +@router.get("/health/db") +async def health_db() -> dict[str, Any]: + """ + Database connectivity health check. + + Verifies connection to PostgreSQL. + """ + db_healthy = await check_db_connection() + + return { + "status": "healthy" if db_healthy else "unhealthy", + "database": { + "connected": db_healthy, + "type": "postgresql", + }, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + + +@router.get("/health/ready") +async def readiness_check() -> dict[str, Any]: + """ + Readiness probe for Kubernetes/Docker. + + Checks if service is ready to accept traffic. + """ + db_ready = await check_db_connection() + + # In alpha, we're ready even without DB (for testing) + is_ready = True # Will be: db_ready when DB is required + + return { + "ready": is_ready, + "checks": { + "database": db_ready, + "config": bool(settings.jwt_secret), + }, + "timestamp": datetime.now(timezone.utc).isoformat(), + } diff --git a/service-python/src/parhelion_py/application/__init__.py b/service-python/src/parhelion_py/application/__init__.py new file mode 100644 index 0000000..650088e --- /dev/null +++ b/service-python/src/parhelion_py/application/__init__.py @@ -0,0 +1 @@ +"""Application Package - Use cases, DTOs, and service interfaces.""" diff --git a/service-python/src/parhelion_py/application/dtos/__init__.py b/service-python/src/parhelion_py/application/dtos/__init__.py new file mode 100644 index 0000000..f9859ee --- /dev/null +++ b/service-python/src/parhelion_py/application/dtos/__init__.py @@ -0,0 +1 @@ +"""Application DTOs Package.""" diff --git a/service-python/src/parhelion_py/application/interfaces/__init__.py b/service-python/src/parhelion_py/application/interfaces/__init__.py new file mode 100644 index 0000000..a417313 --- /dev/null +++ b/service-python/src/parhelion_py/application/interfaces/__init__.py @@ -0,0 +1 @@ +"""Application Interfaces Package.""" diff --git a/service-python/src/parhelion_py/application/services/__init__.py b/service-python/src/parhelion_py/application/services/__init__.py new file mode 100644 index 0000000..9eb38e0 --- /dev/null +++ b/service-python/src/parhelion_py/application/services/__init__.py @@ -0,0 +1 @@ +"""Application Services Package.""" diff --git a/service-python/src/parhelion_py/domain/__init__.py b/service-python/src/parhelion_py/domain/__init__.py new file mode 100644 index 0000000..e224ce7 --- /dev/null +++ b/service-python/src/parhelion_py/domain/__init__.py @@ -0,0 +1 @@ +"""Domain Package - Core business logic without external dependencies.""" diff --git a/service-python/src/parhelion_py/domain/entities/__init__.py b/service-python/src/parhelion_py/domain/entities/__init__.py new file mode 100644 index 0000000..b9f2699 --- /dev/null +++ b/service-python/src/parhelion_py/domain/entities/__init__.py @@ -0,0 +1 @@ +"""Domain Entities Package.""" diff --git a/service-python/src/parhelion_py/domain/exceptions/__init__.py b/service-python/src/parhelion_py/domain/exceptions/__init__.py new file mode 100644 index 0000000..eba7321 --- /dev/null +++ b/service-python/src/parhelion_py/domain/exceptions/__init__.py @@ -0,0 +1 @@ +"""Domain Exceptions Package.""" diff --git a/service-python/src/parhelion_py/domain/interfaces/__init__.py b/service-python/src/parhelion_py/domain/interfaces/__init__.py new file mode 100644 index 0000000..cc4b715 --- /dev/null +++ b/service-python/src/parhelion_py/domain/interfaces/__init__.py @@ -0,0 +1 @@ +"""Domain Interfaces Package - Repository abstractions (Ports).""" diff --git a/service-python/src/parhelion_py/domain/value_objects/__init__.py b/service-python/src/parhelion_py/domain/value_objects/__init__.py new file mode 100644 index 0000000..d00ba59 --- /dev/null +++ b/service-python/src/parhelion_py/domain/value_objects/__init__.py @@ -0,0 +1 @@ +"""Domain Value Objects Package.""" diff --git a/service-python/src/parhelion_py/infrastructure/__init__.py b/service-python/src/parhelion_py/infrastructure/__init__.py new file mode 100644 index 0000000..eabb552 --- /dev/null +++ b/service-python/src/parhelion_py/infrastructure/__init__.py @@ -0,0 +1 @@ +"""Infrastructure Package.""" diff --git a/service-python/src/parhelion_py/infrastructure/config/__init__.py b/service-python/src/parhelion_py/infrastructure/config/__init__.py new file mode 100644 index 0000000..4163671 --- /dev/null +++ b/service-python/src/parhelion_py/infrastructure/config/__init__.py @@ -0,0 +1,5 @@ +"""Infrastructure Config Package.""" + +from parhelion_py.infrastructure.config.settings import Settings, get_settings + +__all__ = ["Settings", "get_settings"] diff --git a/service-python/src/parhelion_py/infrastructure/config/settings.py b/service-python/src/parhelion_py/infrastructure/config/settings.py new file mode 100644 index 0000000..36a3a31 --- /dev/null +++ b/service-python/src/parhelion_py/infrastructure/config/settings.py @@ -0,0 +1,64 @@ +""" +Infrastructure Configuration - Settings +======================================== + +Pydantic Settings for environment-based configuration. +""" + +from functools import lru_cache +from typing import Literal + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Application settings loaded from environment variables.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore", + ) + + # === Application === + version: str = "0.6.0-alpha" + environment: Literal["development", "production", "testing"] = "development" + service_name: str = "python-analytics" + log_level: str = "info" + workers: int = 4 + + # === Database === + database_url: str | None = None + + # === Security === + jwt_secret: str = "" + internal_service_key: str = "" + + # === External Services === + parhelion_api_url: str = "http://parhelion-api:5000" + parhelion_api_internal_key: str = "" + + # === CORS === + cors_origins: list[str] = [ + "http://localhost:4100", # Admin + "http://localhost:5101", # Operaciones + "http://localhost:5102", # Campo + "http://localhost:5100", # API (.NET) + ] + + @property + def is_production(self) -> bool: + """Check if running in production mode.""" + return self.environment == "production" + + @property + def is_testing(self) -> bool: + """Check if running in testing mode.""" + return self.environment == "testing" + + +@lru_cache +def get_settings() -> Settings: + """Get cached settings instance.""" + return Settings() diff --git a/service-python/src/parhelion_py/infrastructure/database/__init__.py b/service-python/src/parhelion_py/infrastructure/database/__init__.py new file mode 100644 index 0000000..430398e --- /dev/null +++ b/service-python/src/parhelion_py/infrastructure/database/__init__.py @@ -0,0 +1,9 @@ +"""Infrastructure Database Package.""" + +from parhelion_py.infrastructure.database.connection import ( + DbSession, + check_db_connection, + get_db_session, +) + +__all__ = ["DbSession", "check_db_connection", "get_db_session"] diff --git a/service-python/src/parhelion_py/infrastructure/database/connection.py b/service-python/src/parhelion_py/infrastructure/database/connection.py new file mode 100644 index 0000000..21fc2e8 --- /dev/null +++ b/service-python/src/parhelion_py/infrastructure/database/connection.py @@ -0,0 +1,82 @@ +""" +Infrastructure Database - Connection +===================================== + +Async SQLAlchemy engine and session management. +""" + +from collections.abc import AsyncGenerator +from typing import Annotated + +from fastapi import Depends +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +from parhelion_py.infrastructure.config.settings import get_settings + +settings = get_settings() + +# Create async engine (lazy initialization) +_engine = None +_async_session_factory = None + + +def get_engine(): + """Get or create async engine.""" + global _engine + if _engine is None and settings.database_url: + _engine = create_async_engine( + settings.database_url, + echo=not settings.is_production, + pool_size=5, + max_overflow=10, + pool_pre_ping=True, + ) + return _engine + + +def get_session_factory(): + """Get or create session factory.""" + global _async_session_factory + engine = get_engine() + if _async_session_factory is None and engine is not None: + _async_session_factory = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + autoflush=False, + ) + return _async_session_factory + + +async def get_db_session() -> AsyncGenerator[AsyncSession, None]: + """Dependency to get database session.""" + factory = get_session_factory() + if factory is None: + raise RuntimeError("Database not configured. Set DATABASE_URL environment variable.") + + async with factory() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + + +# Type alias for dependency injection +DbSession = Annotated[AsyncSession, Depends(get_db_session)] + + +async def check_db_connection() -> bool: + """Check if database connection is working.""" + engine = get_engine() + if engine is None: + return False + + try: + async with engine.connect() as conn: + await conn.execute(text("SELECT 1")) + return True + except Exception: + return False diff --git a/service-python/src/parhelion_py/infrastructure/external/__init__.py b/service-python/src/parhelion_py/infrastructure/external/__init__.py new file mode 100644 index 0000000..7962bb0 --- /dev/null +++ b/service-python/src/parhelion_py/infrastructure/external/__init__.py @@ -0,0 +1 @@ +"""Infrastructure External Package - HTTP clients for external services.""" diff --git a/service-python/src/parhelion_py/main.py b/service-python/src/parhelion_py/main.py new file mode 100644 index 0000000..e904f08 --- /dev/null +++ b/service-python/src/parhelion_py/main.py @@ -0,0 +1,73 @@ +""" +Parhelion Python Analytics Service - Main Application +====================================================== + +FastAPI application entry point with Clean Architecture. +""" + +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from parhelion_py.api.routers import health +from parhelion_py.infrastructure.config.settings import get_settings + +settings = get_settings() + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + """Application lifespan handler for startup/shutdown events.""" + # Startup + print(f"🚀 Starting Parhelion Python Analytics v{settings.version}") + print(f"📊 Environment: {settings.environment}") + print(f"🔗 Database: {settings.database_url.split('@')[-1] if settings.database_url else 'Not configured'}") + + yield + + # Shutdown + print("👋 Shutting down Parhelion Python Analytics") + + +def create_app() -> FastAPI: + """Factory function to create FastAPI application.""" + + app = FastAPI( + title="Parhelion Python Analytics", + description="Microservicio de análisis y predicciones para Parhelion Logistics", + version=settings.version, + docs_url="/docs" if settings.environment != "production" else None, + redoc_url="/redoc" if settings.environment != "production" else None, + lifespan=lifespan, + ) + + # CORS Middleware + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Include routers + app.include_router(health.router, tags=["Health"]) + + return app + + +# Application instance +app = create_app() + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "parhelion_py.main:app", + host="0.0.0.0", + port=8000, + reload=settings.environment == "development", + ) diff --git a/service-python/tests/__init__.py b/service-python/tests/__init__.py new file mode 100644 index 0000000..6d3c56e --- /dev/null +++ b/service-python/tests/__init__.py @@ -0,0 +1 @@ +"""Tests Package.""" diff --git a/service-python/tests/conftest.py b/service-python/tests/conftest.py new file mode 100644 index 0000000..cc046da --- /dev/null +++ b/service-python/tests/conftest.py @@ -0,0 +1,23 @@ +""" +Pytest Configuration +==================== + +Fixtures and configuration for tests. +""" + +import pytest +from fastapi.testclient import TestClient + +from parhelion_py.main import app + + +@pytest.fixture +def client() -> TestClient: + """Create test client for FastAPI app.""" + return TestClient(app) + + +@pytest.fixture +def api_prefix() -> str: + """API prefix for Python endpoints.""" + return "/api/py" diff --git a/service-python/tests/integration/__init__.py b/service-python/tests/integration/__init__.py new file mode 100644 index 0000000..f3e8f52 --- /dev/null +++ b/service-python/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration Tests Package.""" diff --git a/service-python/tests/unit/__init__.py b/service-python/tests/unit/__init__.py new file mode 100644 index 0000000..01abc5d --- /dev/null +++ b/service-python/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Unit Tests Package.""" diff --git a/service-python/tests/unit/test_health.py b/service-python/tests/unit/test_health.py new file mode 100644 index 0000000..f828bab --- /dev/null +++ b/service-python/tests/unit/test_health.py @@ -0,0 +1,51 @@ +""" +Health Endpoint Tests +===================== + +Tests for /health, /health/db, /health/ready endpoints. +""" + +from fastapi.testclient import TestClient + + +class TestHealthEndpoints: + """Test suite for health check endpoints.""" + + def test_health_returns_200(self, client: TestClient) -> None: + """Test basic health endpoint returns 200.""" + response = client.get("/health") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + assert "version" in data + assert "timestamp" in data + + def test_health_contains_service_info(self, client: TestClient) -> None: + """Test health endpoint contains service metadata.""" + response = client.get("/health") + data = response.json() + + assert data["service"] == "python-analytics" + assert data["version"] == "0.6.0-alpha" + assert data["environment"] in ["development", "production", "testing"] + + def test_health_db_returns_200(self, client: TestClient) -> None: + """Test database health endpoint returns 200.""" + response = client.get("/health/db") + + assert response.status_code == 200 + data = response.json() + assert "status" in data + assert "database" in data + assert data["database"]["type"] == "postgresql" + + def test_health_ready_returns_200(self, client: TestClient) -> None: + """Test readiness probe returns 200.""" + response = client.get("/health/ready") + + assert response.status_code == 200 + data = response.json() + assert "ready" in data + assert "checks" in data + assert "timestamp" in data diff --git a/service-webhooks.md b/service-webhooks.md index 08e1fb7..48f6492 100644 --- a/service-webhooks.md +++ b/service-webhooks.md @@ -106,3 +106,62 @@ Si el flujo de n8n necesita consultar información adicional a Parhelion (ej. "G - **Value:** `Bearer {{ $json.body.callbackToken }}` Esto asegura que el flujo utilice siempre el token válido de la sesión actual. + +--- + +## 6. Integración con Python Analytics Service (v0.6.0+) + +El microservicio Python participa en el sistema de eventos para análisis y predicciones. + +### 6.1 Eventos hacia Python + +| Evento | Trigger | Payload | +| -------------------- | ------------------------ | --------------------------------------------- | +| `analytics.request` | Admin solicita análisis | `{ tenantId, dateRange, type, filters }` | +| `prediction.request` | n8n necesita ETA | `{ shipmentId, currentLocation, urgency }` | +| `report.generate` | Usuario solicita reporte | `{ tenantId, reportType, dateRange, format }` | + +### 6.2 Callbacks desde Python + +| Callback | Destino | Method | Payload | +| --------------------- | -------- | ------ | ------------------------------------------ | +| `analytics.completed` | .NET API | POST | `{ sessionId, results, executionTime }` | +| `prediction.ready` | n8n | POST | `{ shipmentId, eta, confidence, factors }` | +| `report.ready` | .NET API | POST | `{ reportId, downloadUrl, expiresAt }` | + +### 6.3 Autenticación Python ↔ n8n + +Cuando n8n necesita llamar al Python Service: + +1. **URL Base:** `http://parhelion-python:8000/api/py` +2. **Header:** `Authorization: Bearer {{ $json.body.callbackToken }}` +3. **Scope requerido:** `analytics:read` o `predictions:execute` + +### 6.4 Ejemplo: Workflow n8n con Python + +``` +[Webhook Trigger] + ↓ +[shipment.exception received] + ↓ +[HTTP Request → Python] + POST /api/py/predictions/eta + { shipmentId, currentLocation } + ↓ +[Receive ETA prediction] + ↓ +[HTTP Request → .NET API] + POST /api/notifications + { driverId, message: "ETA actualizado" } +``` + +--- + +## 7. Variables de Entorno Requeridas (v0.6.0+) + +| Variable | Servicio | Descripción | +| ---------------------- | ------------ | ----------------------------------------------------------------- | +| `INTERNAL_SERVICE_KEY` | .NET, Python | Clave compartida para auth inter-servicios | +| `PYTHON_SERVICE_URL` | .NET | URL del servicio Python (default: `http://parhelion-python:8000`) | +| `PARHELION_API_URL` | Python | URL del API .NET (default: `http://parhelion-api:5000`) | +| `JWT_SECRET` | Todos | Secreto compartido para validar tokens | From 5a44d5452f4994c8d74550470207c4294cd9a9f0 Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sun, 28 Dec 2025 19:44:56 +0000 Subject: [PATCH 29/34] docs: refactorizacion y limpieza de documentacion v0.6.0-alpha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactoring and cleanup of documentation v0.6.0-alpha ESPAÑOL: - Removidos emojis de todos los archivos markdown (profesionalismo) - README.md: seccion Python Analytics compactada con referencia a python-analytics.md - Tabla de documentacion simplificada - CHANGELOG.md y database-schema.md limpios ENGLISH: - Removed emojis from all markdown files (professionalism) - README.md: Python Analytics section compacted with reference to python-analytics.md - Documentation table simplified - CHANGELOG.md and database-schema.md cleaned --- CHANGELOG.md | 4 +-- README.md | 85 +++++++++++---------------------------------- database-schema.md | 14 ++++---- python-analytics.md | 40 ++++++++++----------- 4 files changed, 49 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c674fe..46a7add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -747,8 +747,8 @@ Ejemplo: 0.6.0-alpha.1+build.2025.12.28 ## Próximos Pasos -1. ~~Implementar Domain Layer (entidades)~~ ✅ -2. ~~Configurar Infrastructure Layer (EF Core)~~ ✅ +1. ~~Implementar Domain Layer (entidades)~~ +2. ~~Configurar Infrastructure Layer (EF Core)~~ 3. Crear API endpoints básicos (CRUD) 4. Implementar autenticación JWT 5. Diseñar UI del Admin diff --git a/README.md b/README.md index efd255f..7a3c526 100644 --- a/README.md +++ b/README.md @@ -104,64 +104,19 @@ Plataforma Unificada de Logística B2B (WMS + TMS) nivel Enterprise. Gestiona in --- -## 🐍 Python Analytics Service (v0.6.0+) +## Python Analytics Service (v0.6.0) -Microservicio dedicado para **análisis avanzado, predicciones ML y reportes**. Implementado con Clean Architecture en Python. +Microservicio dedicado para analisis avanzado, predicciones ML y reportes. Implementado con Clean Architecture en Python. -### Arquitectura +Para documentacion completa del servicio, ver [python-analytics.md](./python-analytics.md). -``` -┌─────────────────────────────────────────────────────────────┐ -│ Docker Network │ -│ │ -│ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ -│ │ .NET API │◄─────►│ Python │◄─────►│ PostgreSQL│ │ -│ │ :5000 │ REST │ :8000 │ async │ :5432 │ │ -│ └──────┬──────┘ └──────┬──────┘ └───────────┘ │ -│ │ │ │ -│ └─────────┬───────────┘ │ -│ ▼ │ -│ ┌───────────┐ │ -│ │ n8n │ (Workflows + IA) │ -│ │ :5678 │ │ -│ └───────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Capacidades - -| Módulo | Descripción | Estado | -| ---------------------- | -------------------------------------------------- | --------- | -| **Health Monitoring** | Endpoints `/health`, `/health/db`, `/health/ready` | ✅ v0.6.0 | -| **Shipment Analytics** | Métricas históricas por período, tenant, ruta | 📋 v0.7.x | -| **Fleet Analytics** | KPIs de ocupación, tiempo muerto, distancia | 📋 v0.7.x | -| **ETA Prediction** | Modelo ML basado en historial de checkpoints | 📋 v0.8.x | -| **Excel Reports** | Generación dinámica con pandas + openpyxl | 📋 v0.9.x | - -### Endpoints Python - -```bash -GET /health # Estado del servicio -GET /health/db # Conectividad PostgreSQL -GET /health/ready # Readiness probe - -# Próximamente (v0.7.x+) -GET /api/py/analytics/shipments -GET /api/py/analytics/fleet -POST /api/py/predictions/eta -POST /api/py/reports/export -``` - -### Tecnologías - -| Componente | Tecnología | -| ---------- | ------------------------ | -| Framework | FastAPI 0.115+ | -| Runtime | Python 3.12 | -| ORM | SQLAlchemy 2.0 + asyncpg | -| Validación | Pydantic v2 | -| Testing | pytest + pytest-asyncio | -| Linting | Ruff + MyPy | +| Componente | Tecnologia | Estado | +| ---------- | ------------------------ | ------- | +| Framework | FastAPI 0.115+ | Activo | +| Runtime | Python 3.12 | Activo | +| ORM | SQLAlchemy 2.0 + asyncpg | Activo | +| Testing | pytest + pytest-asyncio | 4 tests | +| Container | parhelion-python:8000 | Healthy | --- @@ -298,7 +253,7 @@ flowchart LR | ORM | Entity Framework Core | 8.0.10 | | Provider | Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.10 | | Database | PostgreSQL | 17 (Docker) | -| Migrations | Code First | ✅ | +| Migrations | Code First | | ### Características de Seguridad @@ -328,7 +283,7 @@ backend/src/ ├── Parhelion.Infrastructure/ # Persistencia: DbContext, Repositorios, Migraciones └── Parhelion.API/ # Entrada: Controllers, JWT Config, DI -service-python/ # 🐍 Microservicio Python (Analytics & Predictions) +service-python/ # Microservicio Python (Analytics & Predictions) ├── src/parhelion_py/ # Clean Architecture: domain, application, infrastructure, api │ ├── domain/ # Entidades, Value Objects, Interfaces │ ├── application/ # DTOs, Services, Use Cases @@ -341,14 +296,14 @@ service-python/ # 🐍 Microservicio Python (Analytics & Predictio ## Documentacion -| Documento | Descripcion | -| :----------------------------------------------- | :------------------------------------------------ | -| [Requerimientos (MVP)](./requirments.md) | Especificacion funcional completa del sistema | -| [Esquema de Base de Datos](./database-schema.md) | Diagrama ER, entidades y reglas de negocio | -| [Arquitectura de API](./api-architecture.md) | Estructura de capas y endpoints (.NET + Python) | -| [Python Analytics](./python-analytics.md) | 🐍 Roadmap, 10 objetivos, estructura del servicio | -| [Guía de Webhooks](./service-webhooks.md) | Integración n8n, eventos y notificaciones | -| [CHANGELOG](./CHANGELOG.md) | Historial detallado de todas las versiones | +| Documento | Descripcion | +| :----------------------------------------------- | :---------------------------------------------- | +| [Requerimientos (MVP)](./requirments.md) | Especificacion funcional completa del sistema | +| [Esquema de Base de Datos](./database-schema.md) | Diagrama ER, entidades y reglas de negocio | +| [Arquitectura de API](./api-architecture.md) | Estructura de capas y endpoints (.NET + Python) | +| [Python Analytics](./python-analytics.md) | Roadmap, 10 objetivos, estructura del servicio | +| [Guia de Webhooks](./service-webhooks.md) | Integracion n8n, eventos y notificaciones | +| [CHANGELOG](./CHANGELOG.md) | Historial detallado de todas las versiones | --- diff --git a/database-schema.md b/database-schema.md index eb9b7df..882f03a 100644 --- a/database-schema.md +++ b/database-schema.md @@ -1402,7 +1402,7 @@ public class Driver ## 12. Metodología de Implementación (Detalles Técnicos) -> **Estado:** ✅ Implementado en v0.4.0 + v0.4.1 +> **Estado:** Implementado en v0.4.0 + v0.4.1 ### 12.1 Tecnologías Utilizadas @@ -1479,9 +1479,9 @@ modelBuilder.Entity().HasQueryFilter(e => **Beneficios:** -- ✅ SQL Injection Prevention: Queries siempre parameterizadas -- ✅ Tenant Isolation: Datos nunca se mezclan entre clientes -- ✅ Soft Delete: Datos nunca se pierden, solo se marcan +- SQL Injection Prevention: Queries siempre parameterizadas +- Tenant Isolation: Datos nunca se mezclan entre clientes +- Soft Delete: Datos nunca se pierden, solo se marcan ### 12.5 Audit Trail Automático @@ -1566,8 +1566,8 @@ environment: **Estado de Implementación:** -- ✅ Domain Layer completo (14 entidades, 11 enums) -- ✅ Infrastructure Layer completo (DbContext, Configurations, Migrations) -- ✅ Base de datos creada y tablas verificadas +- Domain Layer completo (14 entidades, 11 enums) +- Infrastructure Layer completo (DbContext, Configurations, Migrations) +- Base de datos creada y tablas verificadas - ⏳ API Endpoints CRUD (próximo) - ⏳ Autenticación JWT (próximo) diff --git a/python-analytics.md b/python-analytics.md index ec85f3c..db23e4b 100644 --- a/python-analytics.md +++ b/python-analytics.md @@ -40,8 +40,8 @@ flowchart LR | # | Objetivo | Versión | Prioridad | | --- | ----------------------------------------------------------------- | ----------- | --------- | -| 1 | **Health Monitoring** - Endpoints de estado y conectividad | v0.6.0 ✅ | P0 | -| 2 | **Database Integration** - Conexión async a PostgreSQL compartido | v0.6.0 ✅ | P0 | +| 1 | **Health Monitoring** - Endpoints de estado y conectividad | v0.6.0 | P0 | +| 2 | **Database Integration** - Conexión async a PostgreSQL compartido | v0.6.0 | P0 | | 3 | **Service Auth** - Autenticación inter-servicios con .NET | v0.6.0-beta | P0 | | 4 | **Shipment Analytics** - Métricas históricas de envíos | v0.7.x | P1 | | 5 | **Fleet Analytics** - KPIs de utilización de flota | v0.7.x | P1 | @@ -67,44 +67,44 @@ service-python/ │ ├── __init__.py # Package metadata │ ├── main.py # FastAPI entry point │ │ -│ ├── domain/ # 🔷 DOMAIN LAYER +│ ├── domain/ # DOMAIN LAYER │ │ ├── entities/ # (vacío - v0.7.x) │ │ ├── value_objects/ # (vacío - v0.7.x) │ │ ├── exceptions/ # (vacío - v0.7.x) │ │ └── interfaces/ # (vacío - v0.7.x) │ │ -│ ├── application/ # 🔶 APPLICATION LAYER +│ ├── application/ # APPLICATION LAYER │ │ ├── dtos/ # (vacío - v0.7.x) │ │ ├── services/ # (vacío - v0.7.x) │ │ └── interfaces/ # (vacío - v0.7.x) │ │ -│ ├── infrastructure/ # 🔵 INFRASTRUCTURE LAYER +│ ├── infrastructure/ # INFRASTRUCTURE LAYER │ │ ├── config/ -│ │ │ └── settings.py # ✅ Pydantic Settings +│ │ │ └── settings.py # Pydantic Settings │ │ ├── database/ -│ │ │ └── connection.py # ✅ SQLAlchemy async +│ │ │ └── connection.py # SQLAlchemy async │ │ └── external/ # (vacío - v0.6.0-beta) │ │ -│ └── api/ # 🟢 API LAYER +│ └── api/ # API LAYER │ ├── routers/ -│ │ └── health.py # ✅ /health, /health/db, /health/ready +│ │ └── health.py # /health, /health/db, /health/ready │ └── middleware/ # (vacío - v0.6.0-beta) │ └── tests/ - ├── conftest.py # ✅ pytest fixtures + ├── conftest.py # pytest fixtures └── unit/ - └── test_health.py # ✅ 4 tests pasando + └── test_health.py # 4 tests pasando ``` ### Componentes Implementados (v0.6.0-alpha) | Componente | Archivo | Estado | | ------------- | --------------------------------------- | ------ | -| FastAPI App | `main.py` | ✅ | -| Settings | `infrastructure/config/settings.py` | ✅ | -| DB Connection | `infrastructure/database/connection.py` | ✅ | -| Health Router | `api/routers/health.py` | ✅ | -| Unit Tests | `tests/unit/test_health.py` | ✅ 4/4 | +| FastAPI App | `main.py` | | +| Settings | `infrastructure/config/settings.py` | | +| DB Connection | `infrastructure/database/connection.py` | | +| Health Router | `api/routers/health.py` | | +| Unit Tests | `tests/unit/test_health.py` | 4/4 | --- @@ -176,7 +176,7 @@ gantt | Release | Nombre | Entregables | | ---------------- | ----------- | ------------------------------------------- | -| **v0.6.0-alpha** | Foundation | ✅ Estructura, health, DB connection, tests | +| **v0.6.0-alpha** | Foundation | Estructura, health, DB connection, tests | | v0.6.0-beta | Integration | Auth middleware, ParhelionApiClient (ACL) | | v0.6.0-rc.1 | Validation | E2E tests, logging estructurado | | **v0.6.0** | Release | GitHub Actions, docs actualizadas | @@ -332,9 +332,9 @@ GET /api/py/dashboard/realtime # Métricas tiempo real | Variable | Requerida | Default | Descripción | | ---------------------- | --------- | --------------------------- | --------------------------- | -| `DATABASE_URL` | ✅ | - | PostgreSQL async connection | -| `JWT_SECRET` | ✅ | - | Secret para validar tokens | -| `INTERNAL_SERVICE_KEY` | ✅ | - | Auth inter-servicios | +| `DATABASE_URL` | | - | PostgreSQL async connection | +| `JWT_SECRET` | | - | Secret para validar tokens | +| `INTERNAL_SERVICE_KEY` | | - | Auth inter-servicios | | `PARHELION_API_URL` | No | `http://parhelion-api:5000` | URL del API .NET | | `ENVIRONMENT` | No | `development` | dev/production/testing | | `LOG_LEVEL` | No | `info` | debug/info/warning/error | From 6afa9fb1547a52fd9995ee81f68fb4b7855f0d5b Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sun, 28 Dec 2025 20:33:20 +0000 Subject: [PATCH 30/34] docs: refactorizacion profesional de documentacion v0.6.0-alpha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Professional documentation refactoring v0.6.0-alpha ESPAÑOL: - python-analytics.md: Eliminado roadmap v0.7-v0.9 duplicado (ya existe en README) - python-analytics.md: Diagrama ASCII reemplazado con Mermaid profesional - python-analytics.md: Tabla de componentes completada con estados - api-architecture.md: Diagrama inter-servicios convertido a Mermaid - README.md: Rangos de version exactos (v0.7.0-v0.7.4 en lugar de v0.7.x) - service-webhooks.md: Workflow ASCII convertido a Mermaid ENGLISH: - python-analytics.md: Removed duplicate v0.7-v0.9 roadmap (already in README) - python-analytics.md: ASCII diagram replaced with professional Mermaid - python-analytics.md: Components table filled with status values - api-architecture.md: Inter-services diagram converted to Mermaid - README.md: Exact version ranges (v0.7.0-v0.7.4 instead of v0.7.x) - service-webhooks.md: ASCII workflow converted to Mermaid Estadisticas: - 4 archivos modificados - 98 inserciones, 123 eliminaciones - 0 diagramas ASCII restantes en documentacion principal --- README.md | 10 +-- api-architecture.md | 19 ++--- python-analytics.md | 170 +++++++++++++++++++------------------------- service-webhooks.md | 22 +++--- 4 files changed, 98 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 7a3c526..df30508 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ flowchart LR | ORM | Entity Framework Core | 8.0.10 | | Provider | Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.10 | | Database | PostgreSQL | 17 (Docker) | -| Migrations | Code First | | +| Migrations | Code First | | ### Características de Seguridad @@ -360,9 +360,9 @@ service-python/ # Microservicio Python (Analytics & Predictions) | v0.6.0-rc.1 | `validation` | Tests de integración, documentación | | **v0.6.0** | `Python Core` | Release estable con microservicio Python integrado | -### Próximas Versiones (v0.7.x - v1.0.0) +### Proximas Versiones (v0.7.0-v1.0.0) -#### v0.7.x - Operaciones de Campo (QR + Rutas) +#### v0.7.0-v0.7.4: Operaciones de Campo (QR + Rutas) | Version | Feature | Descripción | | ------- | ---------------- | -------------------------------------------------- | @@ -372,7 +372,7 @@ service-python/ # Microservicio Python (Analytics & Predictions) | v0.7.3 | Route Assignment | Asignación de rutas predefinidas a shipments | | v0.7.4 | Route Progress | Avance automático por pasos de ruta | -#### v0.8.x - Frontend Admin Panel (Angular) +#### v0.8.0-v0.8.5: Frontend Admin Panel (Angular) | Version | Feature | Descripción | | ------- | ----------------- | --------------------------------------------- | @@ -383,7 +383,7 @@ service-python/ # Microservicio Python (Analytics & Predictions) | v0.8.4 | Shipment Tracking | Timeline de checkpoints, status updates | | v0.8.5 | Network CRUD | Gestión de Locations, Routes, NetworkLinks | -#### v0.9.x - Frontend PWAs + Dashboard +#### v0.9.0-v0.9.6: Frontend PWAs + Dashboard | Version | Feature | Descripción | | ------- | -------------------- | ---------------------------------------------- | diff --git a/api-architecture.md b/api-architecture.md index 393b27e..5de6779 100644 --- a/api-architecture.md +++ b/api-architecture.md @@ -201,14 +201,17 @@ o `Authorization: Bearer ` para llamadas desde n8n. ### Comunicación Inter-Servicios -``` -┌─────────────┐ REST/JSON ┌─────────────┐ -│ .NET API │◄──────────────────►│ Python │ -│ :5000 │ Internal JWT/Key │ :8000 │ -└──────┬──────┘ └──────┬──────┘ - │ │ - └──────────► PostgreSQL ◄──────────┘ - :5432 +```mermaid +flowchart LR + subgraph Docker["Docker Network"] + NET[".NET API
:5000"] + PY["Python Analytics
:8000"] + DB[(PostgreSQL
:5432)] + end + + NET <-->|"REST/JSON
Internal JWT"| PY + NET --> DB + PY --> DB ``` --- diff --git a/python-analytics.md b/python-analytics.md index db23e4b..d509da1 100644 --- a/python-analytics.md +++ b/python-analytics.md @@ -40,8 +40,8 @@ flowchart LR | # | Objetivo | Versión | Prioridad | | --- | ----------------------------------------------------------------- | ----------- | --------- | -| 1 | **Health Monitoring** - Endpoints de estado y conectividad | v0.6.0 | P0 | -| 2 | **Database Integration** - Conexión async a PostgreSQL compartido | v0.6.0 | P0 | +| 1 | **Health Monitoring** - Endpoints de estado y conectividad | v0.6.0 | P0 | +| 2 | **Database Integration** - Conexión async a PostgreSQL compartido | v0.6.0 | P0 | | 3 | **Service Auth** - Autenticación inter-servicios con .NET | v0.6.0-beta | P0 | | 4 | **Shipment Analytics** - Métricas históricas de envíos | v0.7.x | P1 | | 5 | **Fleet Analytics** - KPIs de utilización de flota | v0.7.x | P1 | @@ -98,17 +98,17 @@ service-python/ ### Componentes Implementados (v0.6.0-alpha) -| Componente | Archivo | Estado | -| ------------- | --------------------------------------- | ------ | -| FastAPI App | `main.py` | | -| Settings | `infrastructure/config/settings.py` | | -| DB Connection | `infrastructure/database/connection.py` | | -| Health Router | `api/routers/health.py` | | -| Unit Tests | `tests/unit/test_health.py` | 4/4 | +| Componente | Archivo | Estado | +| ------------- | --------------------------------------- | ------------ | +| FastAPI App | `main.py` | Implementado | +| Settings | `infrastructure/config/settings.py` | Implementado | +| DB Connection | `infrastructure/database/connection.py` | Implementado | +| Health Router | `api/routers/health.py` | Implementado | +| Unit Tests | `tests/unit/test_health.py` | 4/4 pasando | --- -## 4. Estructura Planeada (v0.7.x - v0.9.x) +## 4. Estructura Planeada (v0.7.0-v0.9.6) ``` service-python/src/parhelion_py/ @@ -159,14 +159,15 @@ service-python/src/parhelion_py/ --- -## 5. Roadmap de Desarrollo +## 5. Roadmap del Servicio Python (v0.6.0) -### v0.6.x - Foundation (EN PROGRESO) +El siguiente roadmap aplica exclusivamente al desarrollo del microservicio Python Analytics. +Para el roadmap completo del proyecto (v0.7.0-v1.0.0), consultar [README.md](./README.md#roadmap). ```mermaid gantt - title Python Service v0.6.x - dateFormat YYYY-MM-DD + title Python Analytics Service - v0.6.0 + dateFormat YYYY-MM-DD section Foundation v0.6.0-alpha :done, a1, 2025-12-28, 1d v0.6.0-beta :active, a2, after a1, 3d @@ -174,96 +175,73 @@ gantt v0.6.0 :milestone, after a3, 0d ``` -| Release | Nombre | Entregables | -| ---------------- | ----------- | ------------------------------------------- | -| **v0.6.0-alpha** | Foundation | Estructura, health, DB connection, tests | -| v0.6.0-beta | Integration | Auth middleware, ParhelionApiClient (ACL) | -| v0.6.0-rc.1 | Validation | E2E tests, logging estructurado | -| **v0.6.0** | Release | GitHub Actions, docs actualizadas | +| Release | Nombre | Entregables | +| ------------ | ----------- | ----------------------------------------------------------------- | +| v0.6.0-alpha | Foundation | Estructura base, health endpoints, conexion DB, 4 tests unitarios | +| v0.6.0-beta | Integration | Middleware de autenticacion, ParhelionApiClient (ACL), logging | +| v0.6.0-rc.1 | Validation | Tests E2E, documentacion final, security review | +| v0.6.0 | Release | CI/CD Python en GitHub Actions, merge a develop | --- -### v0.7.x - Analytics Core +## 6. Arquitectura de Capas (Clean Architecture) -| Release | Feature | Endpoints | -| ------- | ----------------------- | --------------------------------------- | -| v0.7.0 | Shipment Analytics Base | `GET /api/py/analytics/shipments` | -| v0.7.1 | Fleet Analytics Base | `GET /api/py/analytics/fleet` | -| v0.7.2 | Filtros Avanzados | Query params: date, status, driver | -| v0.7.3 | Aggregations | Métricas por período, tenant | -| v0.7.4 | Caching | Redis/in-memory para queries frecuentes | - ---- - -### v0.8.x - Machine Learning +```mermaid +flowchart TB + subgraph API["API Layer - FastAPI"] + direction LR + R1[Health Router] + R2[Analytics Router] + R3[Predictions Router] + R4[Reports Router] + MW[Middleware] + end -| Release | Feature | Modelo | -| ------- | ------------------- | ------------------------ | -| v0.8.0 | ETA Prediction Base | Regresión lineal | -| v0.8.1 | Feature Engineering | Historial de checkpoints | -| v0.8.2 | Model Improvement | Gradient Boosting | -| v0.8.3 | Anomaly Detection | Isolation Forest | -| v0.8.4 | Confidence Scoring | Intervalos de predicción | + subgraph APP["Application Layer"] + direction LR + S1[AnalyticsService] + S2[PredictionService] + S3[ReportService] + DTO[DTOs] + end ---- + subgraph DOM["Domain Layer"] + direction LR + ENT[Entities] + VO[Value Objects] + INT[Interfaces] + end -### v0.9.x - Reports & Dashboard + subgraph INF["Infrastructure Layer"] + direction LR + DB[Database] + EXT[External Clients] + CFG[Config] + end -| Release | Feature | Output | -| ------- | ------------------- | ------------------------ | -| v0.9.0 | Excel Base | pandas + openpyxl | -| v0.9.1 | Report Templates | Diario, Semanal, Mensual | -| v0.9.2 | Dashboard Endpoints | KPIs en JSON | -| v0.9.3 | Real-time Metrics | WebSocket support | -| v0.9.4 | n8n Callbacks | Event-driven analytics | + subgraph EXTERNAL["External Systems"] + PG[(PostgreSQL)] + NET[".NET API"] + end ---- + R1 & R2 & R3 & R4 --> MW + MW --> S1 & S2 & S3 + S1 & S2 & S3 --> DTO + DTO --> ENT & VO + ENT & VO --> INT + INT --> DB & EXT + DB --> PG + EXT --> NET +``` -## 6. Diagrama de Capas (Clean Architecture) +### Principios de Diseno -``` -┌─────────────────────────────────────────────────────────────┐ -│ API LAYER │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Health │ │ Analytics │ │ Predictions │ │ -│ │ Router │ │ Router │ │ Router │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -│ │ │ │ │ -│ ┌──────┴────────────────┴────────────────┴──────┐ │ -│ │ Middleware (Auth, Tenant) │ │ -│ └────────────────────────┬──────────────────────┘ │ -├───────────────────────────┼─────────────────────────────────┤ -│ ▼ │ -│ APPLICATION LAYER │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Analytics │ │ Prediction │ │ Report │ │ -│ │ Service │ │ Service │ │ Service │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -│ │ │ │ │ -│ ┌──────┴────────────────┴────────────────┴──────┐ │ -│ │ DTOs │ │ -│ └────────────────────────┬──────────────────────┘ │ -├───────────────────────────┼─────────────────────────────────┤ -│ ▼ │ -│ DOMAIN LAYER │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Entities │ │ Value │ │ Interfaces │ │ -│ │ │ │ Objects │ │ (Ports) │ │ -│ └─────────────┘ └─────────────┘ └──────┬──────┘ │ -├───────────────────────────────────────────┼─────────────────┤ -│ ▼ │ -│ INFRASTRUCTURE LAYER │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Database │ │ External │ │ Config │ │ -│ │ (Adapters) │ │ Clients │ │ (Settings) │ │ -│ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌─────────────┐ ┌─────────────┐ │ -│ │ PostgreSQL │ │ .NET API │ │ -│ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` +| Principio | Implementacion | +| --------------------- | ------------------------------------------------------- | +| Dependency Inversion | Domain define interfaces, Infrastructure las implementa | +| Single Responsibility | Cada servicio maneja un caso de uso especifico | +| Interface Segregation | Interfaces pequenas y especificas por funcionalidad | +| Open/Closed | Nuevos routers sin modificar codigo existente | --- @@ -332,9 +310,9 @@ GET /api/py/dashboard/realtime # Métricas tiempo real | Variable | Requerida | Default | Descripción | | ---------------------- | --------- | --------------------------- | --------------------------- | -| `DATABASE_URL` | | - | PostgreSQL async connection | -| `JWT_SECRET` | | - | Secret para validar tokens | -| `INTERNAL_SERVICE_KEY` | | - | Auth inter-servicios | +| `DATABASE_URL` | | - | PostgreSQL async connection | +| `JWT_SECRET` | | - | Secret para validar tokens | +| `INTERNAL_SERVICE_KEY` | | - | Auth inter-servicios | | `PARHELION_API_URL` | No | `http://parhelion-api:5000` | URL del API .NET | | `ENVIRONMENT` | No | `development` | dev/production/testing | | `LOG_LEVEL` | No | `info` | debug/info/warning/error | diff --git a/service-webhooks.md b/service-webhooks.md index 48f6492..0261b2a 100644 --- a/service-webhooks.md +++ b/service-webhooks.md @@ -139,20 +139,14 @@ Cuando n8n necesita llamar al Python Service: ### 6.4 Ejemplo: Workflow n8n con Python -``` -[Webhook Trigger] - ↓ -[shipment.exception received] - ↓ -[HTTP Request → Python] - POST /api/py/predictions/eta - { shipmentId, currentLocation } - ↓ -[Receive ETA prediction] - ↓ -[HTTP Request → .NET API] - POST /api/notifications - { driverId, message: "ETA actualizado" } +```mermaid +flowchart TD + A[Webhook Trigger] --> B[shipment.exception received] + B --> C[HTTP Request a Python] + C --> D["POST /api/py/predictions/eta
{shipmentId, currentLocation}"] + D --> E[Receive ETA prediction] + E --> F[HTTP Request a .NET API] + F --> G["POST /api/notifications
{driverId, message}"] ``` --- From 9e6af4da9ade49fc189dbb7220fd9e0608fbbc2e Mon Sep 17 00:00:00 2001 From: MetaCodeX Date: Sun, 28 Dec 2025 20:54:51 +0000 Subject: [PATCH 31/34] fix(frontend): grid animation estable y actualizacion a v0.6.0-alpha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix grid animation and update to v0.6.0-alpha ESPAÑOL: - Grid background: corregido colapso con CSS puro (background-position animation) - GPU acceleration: translateZ(0), will-change, backface-visibility - Eliminada dependencia de JavaScript para animacion del grid - Version actualizada a v0.6.0-alpha en marquee, badges y footer - Agregada slide de v0.6.0 al carousel (9 slides ahora) - Stack tecnologico actualizado con Python/FastAPI - Contenido del marquee: Python Analytics Service ENGLISH: - Grid background: fixed collapse with pure CSS (background-position animation) - GPU acceleration: translateZ(0), will-change, backface-visibility - Removed JavaScript dependency for grid animation - Version updated to v0.6.0-alpha in marquee, badges and footer - Added v0.6.0 slide to carousel (9 slides now) - Technology stack updated with Python/FastAPI - Marquee content: Python Analytics Service Build passed: 54.01 kB transfer size --- frontend-inicio/src/app/app.component.ts | 79 ++++---- frontend-inicio/src/styles.css | 248 ++++++++++++++--------- 2 files changed, 187 insertions(+), 140 deletions(-) diff --git a/frontend-inicio/src/app/app.component.ts b/frontend-inicio/src/app/app.component.ts index 5471f5d..06517e1 100644 --- a/frontend-inicio/src/app/app.component.ts +++ b/frontend-inicio/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; +import { Component, AfterViewInit } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ @@ -13,25 +13,25 @@ import { CommonModule } from '@angular/common';
- ★ PARHELION v0.5.7 RELEASED + ★ PARHELION v0.6.0-alpha RELEASED - Dynamic PDF Generation + Python Analytics Service - Checkpoint Timeline + FastAPI + SQLAlchemy - POD Digital Signatures + Clean Architecture - n8n AI Integration + Multi-Service Docker - ★ PARHELION v0.5.7 RELEASED + ★ PARHELION v0.6.0-alpha RELEASED - Dynamic PDF Generation + Python Analytics Service - Checkpoint Timeline + FastAPI + SQLAlchemy - POD Digital Signatures + Clean Architecture - n8n AI Integration + Multi-Service Docker
@@ -45,8 +45,8 @@ import { CommonModule } from '@angular/common';
- NEW v0.5.7 - Enterprise + NEW v0.6.0 + Python + .NET Multi-tenant
@@ -299,6 +299,22 @@ import { CommonModule } from '@angular/common';
NEW +

v0.6.0-alpha

+
+

2025-12-28

+
    +
  • Python Analytics Service (FastAPI + SQLAlchemy)
  • +
  • Clean Architecture: domain, application, infrastructure
  • +
  • Async PostgreSQL connection (asyncpg)
  • +
  • Multi-service Docker deployment (9 containers)
  • +
+
+ + +