commit ab2c7578ffee4c2d8cd7769df0a0abf708a279a4 Author: RafaÅ‚ Miczek Date: Wed Oct 14 22:57:20 2026 +0200 Initial commit: iLED shop monolith with Drone CI/CD scaffold. Adds API, Angular shop/admin, docker compose, and .drone.yml for build + SSH deploy. Co-authored-by: Cursor diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..cd3892d --- /dev/null +++ b/.drone.yml @@ -0,0 +1,95 @@ +# Drone CI/CD for iLED shop +# Repo secrets (Drone UI → Settings → Secrets): +# ssh_host, ssh_user, ssh_key — deploy host +# ssh_port — optional, default 22 +# deploy_path — absolute path on host (e.g. /opt/iled-shop) +# +# First-time host setup: +# git clone ssh://git@git.zea.lt:2222/miczek/iled-shop.git $DEPLOY_PATH +# cd $DEPLOY_PATH/docker && cp .env.example .env # then edit secrets + +kind: pipeline +type: docker +name: ci + +clone: + depth: 50 + +concurrency: + limit: 1 + +steps: + - name: api + image: mcr.microsoft.com/dotnet/sdk:10.0 + commands: + - cd src/backend + - dotnet restore iLED.sln + - dotnet build iLED.sln -c Release --no-restore + + - name: shop + image: node:22-bookworm + commands: + - cd src/shop + - npm ci + - npm run build -- --configuration=production + + - name: admin + image: node:22-bookworm + commands: + - cd src/admin + - npm ci + - npm run build -- --configuration=production + +trigger: + event: + - push + - pull_request + +--- +kind: pipeline +type: docker +name: deploy + +depends_on: + - ci + +clone: + disable: true + +concurrency: + limit: 1 + +steps: + - name: ssh-deploy + image: appleboy/drone-ssh + settings: + host: + from_secret: ssh_host + username: + from_secret: ssh_user + key: + from_secret: ssh_key + port: + from_secret: ssh_port + script_stop: true + envs: + - DEPLOY_PATH + script: + - set -euo pipefail + - cd "${DEPLOY_PATH}" + - git fetch origin + - git checkout "${DRONE_COMMIT_SHA}" + - cd docker + - docker compose --profile full pull || true + - docker compose --profile full up -d --build --remove-orphans + - docker compose --profile full ps + environment: + DEPLOY_PATH: + from_secret: deploy_path + +trigger: + branch: + - master + - main + event: + - push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d84327 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Build +**/bin/ +**/obj/ +**/dist/ +**/node_modules/ + +# IDE +.vs/ +.idea/ +*.user + +# Env +.env +docker/.env +appsettings.*.local.json + +# OS +Thumbs.db +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bfa559 --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +# iLED — Sklep z lampami + +Polish lamp e-commerce platform with .NET modular monolith API, Angular shop, and Angular admin panel. + +## Structure + +``` +Shop/ + design/ Figma exports + src/ + backend/ .NET 10 API (Clean Architecture + CQRS) + shop/ Angular customer webshop (port 4200) + admin/ Angular admin panel (port 4201) + docker/ PostgreSQL, Redis, OpenObserve, optional API + .drone.yml Drone CI (build) + SSH deploy +``` + +## Drone CI / deploy + +Pipeline file: [`.drone.yml`](.drone.yml). + +**CI** (every push / PR): build API, shop, admin. + +**Deploy** (`master`/`main` push, after CI): SSH to the host, `git checkout` the commit, `docker compose --profile full up -d --build`. + +Drone secrets to configure: + +| Secret | Purpose | +|--------|---------| +| `ssh_host` | Deploy server hostname/IP | +| `ssh_user` | SSH user | +| `ssh_key` | Private key (PEM) | +| `ssh_port` | SSH port (use `22` if unused) | +| `deploy_path` | Absolute clone path, e.g. `/opt/iled-shop` | + +On the host once: + +```bash +git clone ssh://git@git.zea.lt:2222/miczek/iled-shop.git /opt/iled-shop +cd /opt/iled-shop/docker +cp .env.example .env # edit passwords / CORS origins +``` + +Shop/admin Angular apps are built in CI; production static hosting (nginx) can be wired next. + +## Quick start + +### 1. Infrastructure + +```bash +cd docker +docker compose up -d postgres redis openobserve +``` + +- PostgreSQL: `localhost:5433` (db/user/pass `iled` / `iled123`) +- Redis: `localhost:6379` +- OpenObserve UI: http://localhost:5080 — `root@iled.local` / `Complexpass#123` + +### 2. Backend API + +```bash +cd src/backend +dotnet run --project src/iLED.Api +``` + +API: http://localhost:5000 +Swagger: http://localhost:5000/swagger +Health: http://localhost:5000/health +Sitemap: http://localhost:5000/api/v1/seo/sitemap.xml + +Leave `OpenTelemetry:OtlpEndpoint` empty to disable exporters (default). +With OpenObserve running, set endpoint/headers via env or User Secrets +(see Observability below). + +Optional full stack (API in Docker): + +```bash +cd docker +docker compose --profile full up -d --build +``` + +### 3. Shop (customer) + +```bash +cd src/shop +npm start +``` + +http://localhost:4200 + +### 4. Admin + +```bash +cd src/admin +npm start +``` + +http://localhost:4201 + +**Admin login:** `admin@iled.pl` / `Admin123!` + +## Shop routes + +| Path | Purpose | +|------|---------| +| `/` | Home | +| `/products`, `/products/:slug` | Catalog / product detail | +| `/cart`, `/checkout` | Cart & checkout | +| `/login`, `/account` | Auth & orders | +| `/privacy`, `/terms` | Legal (draft copy) | +| `/**` | Custom 404 | + +Footer links to privacy/terms. Cookie banner gates OpenPanel analytics only +(OpenObserve is server-side and does not use browser cookies). + +## Payments (PayU) + +- Default: `PayU:UseSandbox: true` — local redirect simulation. +- Live: set `UseSandbox` to `false` and configure via User Secrets / env: + - `PayU:PosId`, `ClientId`, `ClientSecret`, `SecondKey` + - `OAuthUrl` / `OrdersUrl` (sandbox hosts are in `appsettings.json`; + production: `https://secure.payu.com/...`) + - `NotifyUrl`, `ContinueUrl` +- Webhook: `POST /api/v1/payments/webhook` (signature header + `OpenPayu-Signature`). Failures appear in OpenObserve traces/logs. +- Status sync (no admin button): thank-you page pulls PayU on return; + `PayUPaymentSyncBackgroundService` polls pending payments + (`PayU:StatusPollEnabled`, `PayU:StatusPollIntervalSeconds` — 30s in + Development). Needed locally when PayU cannot reach `NotifyUrl`. + +**Do not commit secrets.** Use `dotnet user-secrets` or environment variables. + +## Shipping (InPost / DHL / DPD) + +- Default: `Shipping:UseSandbox: true` — mock **labels**; InPost map can still + use the real Geowidget (sandbox SDK). +- **InPost map (localhost):** token from + [sandbox Parcel Manager](https://sandbox-manager.paczkomaty.pl) → API → + Geowidget → Generate **without a domain**, then set + `Shipping:InPost:GeowidgetSandboxToken`. SDK: + `https://sandbox-easy-geowidget-sdk.easypack24.net`. +- **InPost map (prod):** `GeowidgetPublicToken` bound to your domain. +- DHL/DPD: widget URLs in config; fallback = demo Leaflet map. +- Live labels (`UseSandbox: false`) need carrier API credentials. + +Pickup checkout shows a billing/invoice address hint (locker is not the +invoice address). + +## Observability + +### OpenObserve (API) + +Compose service `openobserve` on port **5080**. + +OTLP/HTTP base endpoint (SDK appends `/v1/traces|metrics|logs`): + +``` +OpenTelemetry__OtlpEndpoint=http://localhost:5080/api/default +OpenTelemetry__OtlpHeaders=Authorization=Basic +OpenTelemetry__ServiceName=iled-api +``` + +For the default compose user, Basic value is: + +`cm9vdEBpbGVkLmxvY2FsOkNvbXBsZXhwYXNzIzEyMw==` + +### OpenPanel (shop product analytics) + +OpenPanel’s official stack is multi-service (API, dashboard, ClickHouse, etc.). +Deploy with their self-hosting guide: + +https://openpanel.dev/docs/self-hosting/deploy-docker-compose + +Then set shop env: + +```ts +// src/shop/src/environments/environment.ts +openPanelApiUrl: 'https://your-openpanel-host/api', +openPanelClientId: '', +``` + +The tracker loads only after cookie consent is accepted. + +## SEO & legal + +- Per-route meta title/description/Open Graph via `SeoService` +- Favicon / apple-touch / `og-default` in `src/shop/public` +- Static `robots.txt` + API sitemap +- Privacy & Terms mention self-hosted OpenPanel + OpenObserve (draft for lawyer review) + +## Features + +### Shop +- Homepage, product list with filters, product detail +- Cart, checkout, PayU (sandbox by default) +- Login/register, favorites, account/orders +- InPost / DHL / DPD pickup widgets + sandbox map +- Product reviews, newsletter signup +- Cookie consent, 404, legal pages + +### Admin +- Dashboard KPIs +- Product CRUD (incl. image alt text), stock management +- Orders & shipping labels +- Discount rules & promo codes +- Newsletter subscribers & campaigns +- Review moderation + +### API modules +- Catalog, Identity, Cart, Orders, Payments, Shipping, Marketing, Reviews, SEO + +## Design + +Brand colors: `#FFBC39`, `#000000`, `#8C8C8C`, `#D9D9D9` diff --git a/design/ELEMENTY - KARTA.png b/design/ELEMENTY - KARTA.png new file mode 100644 index 0000000..71bf56c Binary files /dev/null and b/design/ELEMENTY - KARTA.png differ diff --git a/design/ELEMENTY - KOSZYK.png b/design/ELEMENTY - KOSZYK.png new file mode 100644 index 0000000..efb3be8 Binary files /dev/null and b/design/ELEMENTY - KOSZYK.png differ diff --git a/design/ELEMENTY - LISTA.png b/design/ELEMENTY - LISTA.png new file mode 100644 index 0000000..0d38700 Binary files /dev/null and b/design/ELEMENTY - LISTA.png differ diff --git a/design/ELEMENTY - LOGIN.png b/design/ELEMENTY - LOGIN.png new file mode 100644 index 0000000..8cb9f27 Binary files /dev/null and b/design/ELEMENTY - LOGIN.png differ diff --git a/design/ELEMENTY - STRONA GŁÓWNA.png b/design/ELEMENTY - STRONA GŁÓWNA.png new file mode 100644 index 0000000..d612a4f Binary files /dev/null and b/design/ELEMENTY - STRONA GŁÓWNA.png differ diff --git a/design/ELEMENTY - ULUBIONE.png b/design/ELEMENTY - ULUBIONE.png new file mode 100644 index 0000000..540dd96 Binary files /dev/null and b/design/ELEMENTY - ULUBIONE.png differ diff --git a/design/KARTA PRODUKTU.png b/design/KARTA PRODUKTU.png new file mode 100644 index 0000000..dd0478d Binary files /dev/null and b/design/KARTA PRODUKTU.png differ diff --git a/design/KOSZYK.png b/design/KOSZYK.png new file mode 100644 index 0000000..846be6c Binary files /dev/null and b/design/KOSZYK.png differ diff --git a/design/LISTA PRODUKTÓW.png b/design/LISTA PRODUKTÓW.png new file mode 100644 index 0000000..f656c0c Binary files /dev/null and b/design/LISTA PRODUKTÓW.png differ diff --git a/design/LOGIN.png b/design/LOGIN.png new file mode 100644 index 0000000..e56b8f8 Binary files /dev/null and b/design/LOGIN.png differ diff --git a/design/STRONA GŁÓWNA - ALTERNATYWNA.png b/design/STRONA GŁÓWNA - ALTERNATYWNA.png new file mode 100644 index 0000000..363175c Binary files /dev/null and b/design/STRONA GŁÓWNA - ALTERNATYWNA.png differ diff --git a/design/STRONA GŁÓWNA.png b/design/STRONA GŁÓWNA.png new file mode 100644 index 0000000..cb7ba46 Binary files /dev/null and b/design/STRONA GŁÓWNA.png differ diff --git a/design/STYLE GUIDE.png b/design/STYLE GUIDE.png new file mode 100644 index 0000000..095e98f Binary files /dev/null and b/design/STYLE GUIDE.png differ diff --git a/design/ULUBIONE.png b/design/ULUBIONE.png new file mode 100644 index 0000000..2ace7c5 Binary files /dev/null and b/design/ULUBIONE.png differ diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..043b559 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,15 @@ +# Copy to .env on the deploy host (never commit .env). +POSTGRES_DB=iled +POSTGRES_USER=iled +POSTGRES_PASSWORD=change-me +REDIS_PASSWORD= + +# API +ASPNETCORE_ENVIRONMENT=Production +Cors__Origins__0=https://shop.example.com +Cors__Origins__1=https://admin.example.com + +# Observability (optional) +OpenTelemetry__OtlpEndpoint= +OpenTelemetry__OtlpHeaders= +OpenTelemetry__ServiceName=iled-api diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api new file mode 100644 index 0000000..4fe3769 --- /dev/null +++ b/docker/Dockerfile.api @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore iLED.sln +RUN dotnet publish src/iLED.Api/iLED.Api.csproj -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 8080 +ENV ASPNETCORE_URLS=http://+:8080 +ENTRYPOINT ["dotnet", "iLED.Api.dll"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..7f76f27 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,72 @@ +services: + postgres: + image: postgres:16-alpine + container_name: iled-postgres + environment: + POSTGRES_DB: iled + POSTGRES_USER: iled + POSTGRES_PASSWORD: iled123 + ports: + - "5433:5432" + volumes: + - iled_pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U iled -d iled"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: iled-redis + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + # Backend observability: logs + metrics + traces (UI http://localhost:5080) + # Login: root@iled.local / Complexpass#123 + openobserve: + image: public.ecr.aws/zinclabs/openobserve:latest + container_name: iled-openobserve + environment: + ZO_ROOT_USER_EMAIL: root@iled.local + ZO_ROOT_USER_PASSWORD: Complexpass#123 + ZO_DATA_DIR: /data + ports: + - "5080:5080" + volumes: + - iled_openobserve:/data + + api: + build: + context: ../src/backend + dockerfile: ../../docker/Dockerfile.api + container_name: iled-api + ports: + - "5000:8080" + environment: + ASPNETCORE_ENVIRONMENT: Development + ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=iled;Username=iled;Password=iled123" + ConnectionStrings__Redis: "redis:6379" + Cors__Origins__0: "http://localhost:4200" + Cors__Origins__1: "http://localhost:4201" + OpenTelemetry__OtlpEndpoint: "http://openobserve:5080/api/default" + OpenTelemetry__OtlpHeaders: "Authorization=Basic cm9vdEBpbGVkLmxvY2FsOkNvbXBsZXhwYXNzIzEyMw==" + OpenTelemetry__ServiceName: "iled-api" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + openobserve: + condition: service_started + profiles: + - full + +volumes: + iled_pgdata: + iled_openobserve: diff --git a/src/admin/.editorconfig b/src/admin/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/src/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/src/admin/.gitignore b/src/admin/.gitignore new file mode 100644 index 0000000..b1d225e --- /dev/null +++ b/src/admin/.gitignore @@ -0,0 +1,43 @@ +# 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 +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/src/admin/.postcssrc.json b/src/admin/.postcssrc.json new file mode 100644 index 0000000..e092dc7 --- /dev/null +++ b/src/admin/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/src/admin/.vscode/extensions.json b/src/admin/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/src/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/src/admin/.vscode/launch.json b/src/admin/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/src/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/src/admin/.vscode/tasks.json b/src/admin/.vscode/tasks.json new file mode 100644 index 0000000..244306f --- /dev/null +++ b/src/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": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/src/admin/README.md b/src/admin/README.md new file mode 100644 index 0000000..eea46cd --- /dev/null +++ b/src/admin/README.md @@ -0,0 +1,59 @@ +# Admin + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.4. + +## Development server + +To start a local development server, run: + +```bash +ng serve +``` + +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. + +## Code scaffolding + +Angular CLI includes powerful code scaffolding tools. To generate a new component, run: + +```bash +ng generate component component-name +``` + +For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: + +```bash +ng generate --help +``` + +## Building + +To build the project run: + +```bash +ng build +``` + +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. + +## Running unit tests + +To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: + +```bash +ng test +``` + +## Running end-to-end tests + +For end-to-end (e2e) testing, run: + +```bash +ng e2e +``` + +Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. + +## Additional Resources + +For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/src/admin/angular.json b/src/admin/angular.json new file mode 100644 index 0000000..25713ad --- /dev/null +++ b/src/admin/angular.json @@ -0,0 +1,81 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm" + }, + "newProjectRoot": "projects", + "projects": { + "admin": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "options": { + "port": 4201 + }, + "configurations": { + "production": { + "buildTarget": "admin:build:production" + }, + "development": { + "buildTarget": "admin:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/src/admin/package-lock.json b/src/admin/package-lock.json new file mode 100644 index 0000000..cf04a1d --- /dev/null +++ b/src/admin/package-lock.json @@ -0,0 +1,9192 @@ +{ + "name": "admin", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "admin", + "version": "0.0.0", + "dependencies": { + "@angular/common": "^21.0.0", + "@angular/compiler": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/router": "^21.0.0", + "@lucide/angular": "^1.23.0", + "@tailwindcss/postcss": "^4.3.2", + "postcss": "^8.5.16", + "rxjs": "~7.8.0", + "tailwindcss": "^4.3.2", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.0.4", + "@angular/cli": "^21.0.4", + "@angular/compiler-cli": "^21.0.0", + "jsdom": "^27.1.0", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/abtesting": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", + "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", + "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", + "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", + "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", + "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", + "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", + "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", + "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", + "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", + "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", + "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", + "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", + "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", + "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.2102.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.18.tgz", + "integrity": "sha512-bEkF4ApWa6FJ2+ZyFPdhQr05PgJv/oKwx+3goNRa4lSxzJO+O3KXr0ivTyE5zEJ8l4JdV3h93bsCP54Rk3v7Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.18", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.18.tgz", + "integrity": "sha512-783AuDmzAjFkOBHey7cx1mXd8CTiMg9LvG+BCxEp8i4EB7cOGYrq3lBzkwhKJq6yYpz7dNgM1QgeLk+NcrFBSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.18.tgz", + "integrity": "sha512-PakHs+Eefx06KORDFyH4wmkIZ/wm4mI4DqhXe4XFuWJOazMQoYJxPmMeby47BD0HhvJN7BskN61CPS8HCAvICg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.18", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.3.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/build": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.18.tgz", + "integrity": "sha512-9Jic0Dud6d0ULPR5SceHV86AzZE42cNWPsbLKrK1dUneaAC44GSVRTmLXcaSX/5Exa3gGjFANQw4qashgxUekQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2102.18", + "@babel/core": "7.29.7", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.4", + "beasties": "0.4.1", + "browserslist": "^4.26.0", + "esbuild": "0.28.1", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.4", + "piscina": "5.2.0", + "rolldown": "1.0.0-rc.4", + "sass": "1.97.3", + "semver": "7.7.4", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.15", + "undici": "7.28.0", + "vite": "7.3.6", + "watchpack": "2.5.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.1" + }, + "peerDependencies": { + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.2.18", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^21.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.18.tgz", + "integrity": "sha512-WOZF4p0SnrmBpNeiOA2pdp9OBvGnOSLWbGdRLXJykyseIiEn7+N+GZp+ZA1aNZB4SWolpYCZfTYwkpo6Sf/LXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2102.18", + "@angular-devkit/core": "21.2.18", + "@angular-devkit/schematics": "21.2.18", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.2.18", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.5.1", + "parse5-html-rewriting-stream": "8.0.0", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.3.6" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.17.tgz", + "integrity": "sha512-hqAQxRfi5ldFE42suAXRcY+JCANrUh7fuSQ/DtZ7L896id5BT/exuv6dWNBC1PyAfQmRbpD5Pt6/pd+tNLyhDQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.17.tgz", + "integrity": "sha512-p+NdjYiwAz9Zmu2yul0LlMXaFjMISVVa24+/MVMoKFeQeI82QE8jDywPlnOSHQHvdCcQVpS7saeEriZzX3JuBQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.17.tgz", + "integrity": "sha512-KithZ3b0HBFH0NbUcswBcjpN9y09vLbarMD7qmGWTnGUBk4W8cn4sbT8zJyv9CRKg9ZcuUBeJYKUfUPn/u/5OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.29.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.17", + "typescript": ">=5.9 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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/@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": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.17.tgz", + "integrity": "sha512-wYHpwIdnUnjQFOJJNqRcGx7LS3u64jT+R9L0TnMR/ViBM9dQgGYImlSikkftg2yrFCNo5aKRxhG2LLskQurVdg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.17.tgz", + "integrity": "sha512-WKu8XeRSNZo+a+aDDZ3M5OtReF7KYqR/PmZ2l1lSf6N5EEAmc+Ky4aqbRhTL/mTSfHrO4+TDJ4C5A2tFmuwIeA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.17", + "@angular/core": "21.2.17", + "@angular/platform-browser": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.17.tgz", + "integrity": "sha512-ROdSliejY37g1EphYmweYdm5cHM8HY3X4tbWt4ubxmhTyYgfN3nxrxfGQ/n7Mz5tDY9VXVLIGDgjLOGYOo4uTQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "21.2.17", + "@angular/common": "21.2.17", + "@angular/core": "21.2.17" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.17.tgz", + "integrity": "sha512-RSCtK5ppAV6y6wfRLHSK2a9Wc/vm8j0wsC+/j9PH9yQmppWFVXDWsg5E39MKOIpnoYVx2+hI6eak6+wYtZTe1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.17", + "@angular/core": "21.2.17", + "@angular/platform-browser": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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/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.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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-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-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "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-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", + "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", + "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", + "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", + "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", + "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", + "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", + "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lucide/angular": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lucide/angular/-/angular-1.23.0.tgz", + "integrity": "sha512-zLt2s9EynuYO/kUd+b8dVeAER/ra8Pwcl5LVH31XRSc5TIoSNXCPQINN9LjTMqaZAeAw2ghwOTWO5FVumB8Ppw==", + "license": "ISC", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=17.0.0", + "@angular/core": ">=17.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", + "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", + "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", + "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.18.tgz", + "integrity": "sha512-XNEWvJSUg/ljLXgGVs7picBOtzVC4abBf2+i/ds9E7GaYYVyH19Mpgk6BOuMHAgRpjPWBAeWiBkTqetiR2Ps5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.18", + "@angular-devkit/schematics": "21.2.18", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.2.tgz", + "integrity": "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "postcss": "^8.5.15", + "tailwindcss": "4.3.2" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/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/@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": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "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/algoliasearch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "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.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/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": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "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": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/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/cliui/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/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/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/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.383", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz", + "integrity": "sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==", + "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/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/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/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": "2.2.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", + "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "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-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.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/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.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "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": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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==", + "license": "ISC" + }, + "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/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "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.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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-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-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/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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/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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/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", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.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/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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/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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.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": ">=20.0.0" + } + }, + "node_modules/listr2/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/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.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "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/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/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/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/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.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "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": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "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.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "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.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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", + "optional": true, + "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/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.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/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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/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/ora": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-map": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.5.tgz", + "integrity": "sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.5.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.1.tgz", + "integrity": "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/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/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "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-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-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", + "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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/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/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.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/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/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/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/rolldown": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", + "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.113.0", + "@rolldown/pluginutils": "1.0.0-rc.4" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-x64": "1.0.0-rc.4", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/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.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/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/sass/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/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?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/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.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.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "BSD-3-Clause", + "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-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": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "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/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.5.tgz", + "integrity": "sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.5" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.5.tgz", + "integrity": "sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==", + "dev": true, + "license": "MIT" + }, + "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/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.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": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.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/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/wrap-ansi/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/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/wrap-ansi/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/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.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "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": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/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/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/src/admin/package.json b/src/admin/package.json new file mode 100644 index 0000000..c9d4ca9 --- /dev/null +++ b/src/admin/package.json @@ -0,0 +1,47 @@ +{ + "name": "admin", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "prettier": { + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] + }, + "private": true, + "packageManager": "npm@10.9.4", + "dependencies": { + "@angular/common": "^21.0.0", + "@angular/compiler": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/router": "^21.0.0", + "@lucide/angular": "^1.23.0", + "@tailwindcss/postcss": "^4.3.2", + "postcss": "^8.5.16", + "rxjs": "~7.8.0", + "tailwindcss": "^4.3.2", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.0.4", + "@angular/cli": "^21.0.4", + "@angular/compiler-cli": "^21.0.0", + "jsdom": "^27.1.0", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } +} diff --git a/src/admin/public/favicon.ico b/src/admin/public/favicon.ico new file mode 100644 index 0000000..57614f9 Binary files /dev/null and b/src/admin/public/favicon.ico differ diff --git a/src/admin/src/app/app.config.ts b/src/admin/src/app/app.config.ts new file mode 100644 index 0000000..c35ac1a --- /dev/null +++ b/src/admin/src/app/app.config.ts @@ -0,0 +1,18 @@ +import { + ApplicationConfig, + provideBrowserGlobalErrorListeners, + provideZonelessChangeDetection, +} from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { routes } from './app.routes'; +import { authInterceptor } from './core/auth.interceptor'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideZonelessChangeDetection(), + provideRouter(routes), + provideHttpClient(withInterceptors([authInterceptor])), + ], +}; diff --git a/src/admin/src/app/app.html b/src/admin/src/app/app.html new file mode 100644 index 0000000..e0118a1 --- /dev/null +++ b/src/admin/src/app/app.html @@ -0,0 +1,342 @@ + + + + + + + + + + + +
+
+
+ +

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: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, + { 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/src/admin/src/app/app.routes.ts b/src/admin/src/app/app.routes.ts new file mode 100644 index 0000000..6234344 --- /dev/null +++ b/src/admin/src/app/app.routes.ts @@ -0,0 +1,75 @@ +import { Routes } from '@angular/router'; +import { adminGuard } from './core/admin.guard'; + +export const routes: Routes = [ + { + path: 'login', + loadComponent: () => + import('./layout/admin-layout.component').then( + (m) => m.AdminLoginComponent, + ), + }, + { + path: '', + canActivate: [adminGuard], + loadComponent: () => + import('./layout/admin-layout.component').then( + (m) => m.AdminLayoutComponent, + ), + children: [ + { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + { + path: 'dashboard', + loadComponent: () => + import('./features/admin-pages.component').then( + (m) => m.DashboardComponent, + ), + }, + { + path: 'products', + loadComponent: () => + import('./features/products-admin.component').then( + (m) => m.ProductsAdminComponent, + ), + }, + { + path: 'orders', + loadComponent: () => + import('./features/orders/orders-admin.component').then( + (m) => m.OrdersAdminComponent, + ), + }, + { + path: 'promotions', + loadComponent: () => + import('./features/promotions-admin.component').then( + (m) => m.PromotionsAdminComponent, + ), + }, + { + path: 'discounts', + redirectTo: 'promotions', + pathMatch: 'full', + }, + { + path: 'banner', + redirectTo: 'promotions', + pathMatch: 'full', + }, + { + path: 'newsletter', + loadComponent: () => + import('./features/admin-pages.component').then( + (m) => m.NewsletterAdminComponent, + ), + }, + { + path: 'reviews', + loadComponent: () => + import('./features/admin-pages.component').then( + (m) => m.ReviewsAdminComponent, + ), + }, + ], + }, +]; diff --git a/src/admin/src/app/app.scss b/src/admin/src/app/app.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/admin/src/app/app.spec.ts b/src/admin/src/app/app.spec.ts new file mode 100644 index 0000000..2b96e18 --- /dev/null +++ b/src/admin/src/app/app.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; +import { App } from './app'; + +describe('App', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [App], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(App); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it('should render title', async () => { + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, admin'); + }); +}); diff --git a/src/admin/src/app/app.ts b/src/admin/src/app/app.ts new file mode 100644 index 0000000..d6aaf4f --- /dev/null +++ b/src/admin/src/app/app.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { I18nService } from './core/i18n/i18n.service'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App implements OnInit { + readonly #i18n = inject(I18nService); + + ngOnInit(): void { + this.#i18n.init(); + } +} diff --git a/src/admin/src/app/core/admin.guard.ts b/src/admin/src/app/core/admin.guard.ts new file mode 100644 index 0000000..b53328f --- /dev/null +++ b/src/admin/src/app/core/admin.guard.ts @@ -0,0 +1,12 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AdminAuthService } from './api.service'; + +export const adminGuard: CanActivateFn = () => { + const auth = inject(AdminAuthService); + const router = inject(Router); + if (auth.isLoggedIn() && auth.isAdmin()) { + return true; + } + return router.createUrlTree(['/login']); +}; diff --git a/src/admin/src/app/core/api.service.ts b/src/admin/src/app/core/api.service.ts new file mode 100644 index 0000000..f131036 --- /dev/null +++ b/src/admin/src/app/core/api.service.ts @@ -0,0 +1,78 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { API_URL } from './constants'; + +@Injectable({ providedIn: 'root' }) +export class ApiService { + readonly #http = inject(HttpClient); + + #headers(): HttpHeaders { + const token = localStorage.getItem('iled_admin_token'); + return token + ? new HttpHeaders({ Authorization: `Bearer ${token}` }) + : new HttpHeaders(); + } + + get(path: string) { + return this.#http.get(`${API_URL}${path}`, { headers: this.#headers() }); + } + + post(path: string, body: unknown) { + return this.#http.post(`${API_URL}${path}`, body, { + headers: this.#headers(), + }); + } + + upload(path: string, file: File) { + const form = new FormData(); + form.append('file', file); + return this.#http.post(`${API_URL}${path}`, form, { + headers: this.#headers(), + }); + } + + put(path: string, body: unknown) { + return this.#http.put(`${API_URL}${path}`, body, { + headers: this.#headers(), + }); + } + + delete(path: string) { + return this.#http.delete(`${API_URL}${path}`, { + headers: this.#headers(), + }); + } +} + +@Injectable({ providedIn: 'root' }) +export class AdminAuthService { + readonly #http = inject(HttpClient); + + login(email: string, password: string) { + return this.#http.post<{ + accessToken: string; + roles: string[]; + }>(`${API_URL}/auth/login`, { email, password }); + } + + isLoggedIn(): boolean { + return !!localStorage.getItem('iled_admin_token'); + } + + isAdmin(): boolean { + return localStorage.getItem('iled_admin_role') === 'Admin'; + } + + store(token: string, roles: string[]): void { + localStorage.setItem('iled_admin_token', token); + localStorage.setItem( + 'iled_admin_role', + roles.includes('Admin') ? 'Admin' : '', + ); + } + + logout(): void { + localStorage.removeItem('iled_admin_token'); + localStorage.removeItem('iled_admin_role'); + } +} diff --git a/src/admin/src/app/core/auth.interceptor.ts b/src/admin/src/app/core/auth.interceptor.ts new file mode 100644 index 0000000..f00ba0b --- /dev/null +++ b/src/admin/src/app/core/auth.interceptor.ts @@ -0,0 +1,24 @@ +import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { catchError, throwError } from 'rxjs'; +import { AdminAuthService } from './api.service'; + +/** + * Clears admin session and redirects to login on 401. + */ +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const auth = inject(AdminAuthService); + const router = inject(Router); + + return next(req).pipe( + catchError((err: unknown) => { + if (err instanceof HttpErrorResponse && err.status === 401) { + auth.logout(); + void router.navigate(['/login']); + } + + return throwError(() => err); + }), + ); +}; diff --git a/src/admin/src/app/core/constants.ts b/src/admin/src/app/core/constants.ts new file mode 100644 index 0000000..6b54a1c --- /dev/null +++ b/src/admin/src/app/core/constants.ts @@ -0,0 +1,3 @@ +export const API_URL = 'http://localhost:5000/api/v1'; +export const API_BASE_URL = 'http://localhost:5000'; +export const SHOP_URL = 'http://localhost:4200'; diff --git a/src/admin/src/app/core/i18n/i18n-currency.pipe.ts b/src/admin/src/app/core/i18n/i18n-currency.pipe.ts new file mode 100644 index 0000000..305b1fe --- /dev/null +++ b/src/admin/src/app/core/i18n/i18n-currency.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform, inject } from '@angular/core'; +import { I18nService } from './i18n.service'; + +@Pipe({ name: 'i18nCurrency', standalone: true, pure: false }) +export class I18nCurrencyPipe implements PipeTransform { + readonly #i18n = inject(I18nService); + + transform(value: number | null | undefined): string { + this.#i18n.locale(); + if (value == null) { + return ''; + } + return this.#i18n.formatCurrency(value); + } +} diff --git a/src/admin/src/app/core/i18n/i18n.models.ts b/src/admin/src/app/core/i18n/i18n.models.ts new file mode 100644 index 0000000..9402bab --- /dev/null +++ b/src/admin/src/app/core/i18n/i18n.models.ts @@ -0,0 +1,20 @@ +export type LocaleCode = 'pl' | 'en' | 'de' | 'cs'; + +export type TranslationMap = Record; + +export interface LocaleOption { + readonly code: LocaleCode; + readonly label: string; + readonly htmlLang: string; + readonly intlLocale: string; +} + +export const LOCALE_OPTIONS: readonly LocaleOption[] = [ + { code: 'pl', label: 'PL', htmlLang: 'pl', intlLocale: 'pl-PL' }, + { code: 'en', label: 'EN', htmlLang: 'en', intlLocale: 'en-GB' }, + { code: 'de', label: 'DE', htmlLang: 'de', intlLocale: 'de-DE' }, + { code: 'cs', label: 'CZ', htmlLang: 'cs', intlLocale: 'cs-CZ' }, +] as const; + +export const DEFAULT_LOCALE: LocaleCode = 'pl'; +export const LOCALE_STORAGE_KEY = 'iled-admin-locale'; diff --git a/src/admin/src/app/core/i18n/i18n.service.ts b/src/admin/src/app/core/i18n/i18n.service.ts new file mode 100644 index 0000000..2061a74 --- /dev/null +++ b/src/admin/src/app/core/i18n/i18n.service.ts @@ -0,0 +1,79 @@ +import { DOCUMENT } from '@angular/common'; +import { Injectable, effect, inject, signal } from '@angular/core'; +import { + DEFAULT_LOCALE, + LOCALE_OPTIONS, + LOCALE_STORAGE_KEY, + LocaleCode, + LocaleOption, +} from './i18n.models'; +import { TRANSLATIONS } from './translations'; + +type TranslateParams = Record; + +@Injectable({ providedIn: 'root' }) +export class I18nService { + readonly #document = inject(DOCUMENT); + readonly #locale = signal(this.#readStoredLocale()); + readonly locale = this.#locale.asReadonly(); + readonly options = LOCALE_OPTIONS; + + constructor() { + effect(() => { + this.#applyDocumentLocale(this.#locale()); + }); + } + + init(): void { + this.#applyDocumentLocale(this.#locale()); + } + + t(key: string, params?: TranslateParams): string { + const dict = TRANSLATIONS[this.#locale()]; + const template = dict[key] ?? TRANSLATIONS[DEFAULT_LOCALE][key] ?? key; + return this.#interpolate(template, params); + } + + setLocale(code: LocaleCode): void { + this.#locale.set(code); + localStorage.setItem(LOCALE_STORAGE_KEY, code); + } + + activeOption(): LocaleOption { + return ( + LOCALE_OPTIONS.find((o) => o.code === this.#locale()) ?? LOCALE_OPTIONS[0] + ); + } + + formatCurrency(value: number): string { + const { intlLocale } = this.activeOption(); + return new Intl.NumberFormat(intlLocale, { + style: 'currency', + currency: 'PLN', + }).format(value); + } + + #readStoredLocale(): LocaleCode { + const stored = localStorage.getItem(LOCALE_STORAGE_KEY); + if (stored === 'pl' || stored === 'en' || stored === 'de' || stored === 'cs') { + return stored; + } + return DEFAULT_LOCALE; + } + + #applyDocumentLocale(code: LocaleCode): void { + const option = LOCALE_OPTIONS.find((o) => o.code === code) ?? LOCALE_OPTIONS[0]; + this.#document.documentElement.lang = option.htmlLang; + this.#document.title = this.t('meta.pageTitle'); + } + + #interpolate(template: string, params?: TranslateParams): string { + if (!params) { + return template; + } + return Object.entries(params).reduce( + (text, [key, value]) => text.replaceAll(`{{${key}}}`, String(value)), + template, + ); + } +} diff --git a/src/admin/src/app/core/i18n/locale-switcher.component.ts b/src/admin/src/app/core/i18n/locale-switcher.component.ts new file mode 100644 index 0000000..f239849 --- /dev/null +++ b/src/admin/src/app/core/i18n/locale-switcher.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { LocaleCode } from './i18n.models'; +import { I18nService } from './i18n.service'; +import { TranslatePipe } from './translate.pipe'; + +@Component({ + selector: 'app-locale-switcher', + standalone: true, + imports: [TranslatePipe], + template: ` + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LocaleSwitcherComponent { + readonly i18n = inject(I18nService); + + handleChange(event: Event): void { + const value = (event.target as HTMLSelectElement).value as LocaleCode; + this.i18n.setLocale(value); + } +} diff --git a/src/admin/src/app/core/i18n/localized-text.pipe.ts b/src/admin/src/app/core/i18n/localized-text.pipe.ts new file mode 100644 index 0000000..0e78120 --- /dev/null +++ b/src/admin/src/app/core/i18n/localized-text.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform, inject } from '@angular/core'; +import { I18nService } from './i18n.service'; +import { LocalizedText } from '../../features/products/product.models'; +import { localizedText } from '../../features/products/localized-text.utils'; + +@Pipe({ + name: 'localizedText', + standalone: true, + pure: false, +}) +export class LocalizedTextPipe implements PipeTransform { + readonly #i18n = inject(I18nService); + + transform(value: LocalizedText | undefined): string { + return localizedText(value, this.#i18n.locale()); + } +} diff --git a/src/admin/src/app/core/i18n/translate.pipe.ts b/src/admin/src/app/core/i18n/translate.pipe.ts new file mode 100644 index 0000000..d044665 --- /dev/null +++ b/src/admin/src/app/core/i18n/translate.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform, inject } from '@angular/core'; +import { I18nService } from './i18n.service'; + +@Pipe({ name: 'translate', standalone: true, pure: false }) +export class TranslatePipe implements PipeTransform { + readonly #i18n = inject(I18nService); + + transform(key: string, params?: Record): string { + this.#i18n.locale(); + return this.#i18n.t(key, params); + } +} diff --git a/src/admin/src/app/core/i18n/translations/cs.ts b/src/admin/src/app/core/i18n/translations/cs.ts new file mode 100644 index 0000000..aded945 --- /dev/null +++ b/src/admin/src/app/core/i18n/translations/cs.ts @@ -0,0 +1,280 @@ +import { TranslationMap } from '../i18n.models'; + +export const CS_TRANSLATIONS: TranslationMap = { + 'meta.pageTitle': 'iLED Admin', + 'a11y.langSwitcher': 'Vyberte jazyk', + 'a11y.mainNav': 'Navigace administrace', + 'nav.dashboard': 'Dashboard', + 'nav.products': 'Produkty', + 'nav.orders': 'Objednávky', + 'nav.promotions': 'Akce', + 'nav.discounts': 'Akce', + 'nav.newsletter': 'Newsletter', + 'nav.reviews': 'Recenze', + 'nav.logout': 'Odhlásit se', + 'nav.openMenu': 'Otevřít menu', + 'nav.closeMenu': 'Zavřít menu', + 'common.search': 'Hledat', + 'common.apply': 'Použít', + 'common.all': 'Vše', + 'common.page': 'Stránka', + 'common.prev': 'Předchozí', + 'common.next': 'Další', + 'common.close': 'Zavřít', + 'common.saved': 'Uloženo', + 'common.deleted': 'Smazáno', + 'auth.loginTitle': 'Přihlášení admin', + 'auth.email': 'E-mail', + 'auth.password': 'Heslo', + 'auth.login': 'Přihlásit se', + 'auth.errorRole': 'Jsou vyžadována oprávnění administrátora.', + 'auth.errorLogin': 'Přihlášení selhalo.', + 'dashboard.title': 'Dashboard', + 'dashboard.ordersToday': 'Objednávky dnes', + 'dashboard.revenueToday': 'Tržby dnes', + 'dashboard.lowStock': 'Nízký stav', + 'dashboard.pending': 'Čekající', + 'dashboard.chartTitle': 'Nové objednávky (14 dní)', + 'dashboard.chartOrders': 'Objednávky', + 'dashboard.errorLoad': 'Nepodařilo se načíst dashboard', + 'products.title': 'Produkty', + 'products.add': 'Přidat produkt', + 'products.edit': 'Upravit', + 'products.save': 'Uložit', + 'products.cancel': 'Zrušit', + 'products.name': 'Název', + 'products.slug': 'Slug', + 'products.price': 'Cena', + 'products.stockMain': 'Hlavní sklad', + 'products.stockSecond': 'Sklad II', + 'products.totalStock': 'Celkem', + 'products.reservedHint': 'Rezervováno v otevřených objednávkách', + 'products.inStock': 'Skladem', + 'products.outOfStock': 'Vyprodáno', + 'products.active': 'Aktivní', + 'products.shortDesc': 'Krátký popis', + 'products.description': 'Popis', + 'products.tabProducts': 'Seznam produktů', + 'products.tabWarehouses': 'Sklady', + 'products.tabGroups': 'Kategorie produktů', + 'products.model': 'Model', + 'products.ean': 'EAN', + 'products.group': 'Skupina', + 'products.noGroup': '— bez skupiny —', + 'products.productCode': 'Kód produktu', + 'products.lowestPrice': 'Nejnižší cena 30 dní', + 'products.deliveryHours': 'Doba dodání (h)', + 'products.bestseller': 'Bestseller', + 'products.newArrival': 'Novinka', + 'products.close': 'Zavřít', + 'products.loading': 'Načítání…', + 'products.sectionBasic': 'Základní údaje', + 'products.sectionNames': 'Názvy', + 'products.sectionShortDesc': 'Krátké popisy', + 'products.sectionDesc': 'Plné popisy', + 'products.sectionParams': 'Parametry', + 'products.sectionStock': 'Skladové zásoby', + 'products.sectionImages': 'Obrázky', + 'products.image': 'Obrázek', + 'products.imageUrl': 'URL obrázku', + 'products.imageAlt': 'Alternativní text', + 'products.addImage': 'Přidat obrázek', + 'products.primaryImage': 'Hlavní', + 'products.imageHint': 'Nahrát soubor (převeden na WebP) nebo zadat URL / cestu obchodu', + 'products.uploadImage': 'Nahrát obrázek', + 'products.uploadHint': 'JPG, PNG, GIF, WebP, BMP — max. 10 MB, konverze na WebP', + 'products.uploading': 'Nahrávání…', + 'products.errorUpload': 'Nepodařilo se nahrát obrázek', + 'products.noImages': 'Žádné obrázky', + 'products.noImage': 'Žádný', + 'products.addParam': 'Přidat parametr', + 'products.param': 'Parametr', + 'products.removeParam': 'Odebrat parametr', + 'products.paramName': 'Název', + 'products.paramValue': 'Hodnota', + 'products.noParams': 'Žádné parametry', + 'products.errorNamePl': 'Polský název je povinný', + 'products.errorLoad': 'Nepodařilo se načíst produkt', + 'products.errorList': 'Nepodařilo se načíst produkty', + 'products.errorSave': 'Nepodařilo se uložit produkt', + 'products.searchPlaceholder': 'Název, slug, model, SKU…', + 'products.activeOnly': 'Jen aktivní', + 'products.inactiveOnly': 'Jen neaktivní', + 'products.lowStockOnly': 'Nízký stav', + 'products.inactive': 'Neaktivní', + 'products.empty': 'Žádné produkty', + 'categories.title': 'Kategorie', + 'categories.name': 'Název', + 'categories.slug': 'Slug', + 'categories.add': 'Přidat kategorii', + 'categories.empty': 'Žádné kategorie', + 'categories.errorLoad': 'Nepodařilo se načíst kategorie', + 'categories.errorSave': 'Nepodařilo se uložit kategorii', + 'banner.title': 'Promo banner', + 'banner.text': 'Text banneru', + 'banner.code': 'Zvýrazněný kód', + 'banner.errorLoad': 'Nepodařilo se načíst banner', + 'banner.errorSave': 'Nepodařilo se uložit banner', + 'warehouses.name': 'Název skladu', + 'warehouses.code': 'Kód', + 'warehouses.sortOrder': 'Pořadí', + 'warehouses.add': 'Přidat sklad', + 'warehouses.delete': 'Smazat', + 'warehouses.errorRequired': 'Vyplňte název a kód skladu', + 'warehouses.errorSave': 'Nepodařilo se přidat sklad', + 'warehouses.errorDelete': 'Sklad nelze smazat (má zásoby)', + 'groups.name': 'Název skupiny', + 'groups.code': 'Kód', + 'groups.sortOrder': 'Pořadí', + 'groups.add': 'Přidat skupinu', + 'groups.edit': 'Upravit', + 'groups.delete': 'Smazat', + 'groups.errorRequired': 'Vyplňte polský název a kód skupiny', + 'groups.errorSave': 'Nepodařilo se uložit skupinu', + 'groups.errorDelete': 'Nepodařilo se smazat skupinu', + 'orders.title': 'Objednávky', + 'orders.tabList': 'Seznam objednávek', + 'orders.tabPickList': 'Kompletace', + 'orders.search': 'Hledat', + 'orders.searchPlaceholder': 'Č. objednávky, e-mail, zákazník…', + 'orders.status': 'Stav', + 'orders.allStatuses': 'Všechny stavy', + 'orders.allPayments': 'Všechny platby', + 'orders.apply': 'Filtrovat', + 'orders.month': 'Měsíc', + 'orders.allMonths': 'Všechny měsíce', + 'orders.date': 'Datum', + 'orders.customer': 'Zákazník', + 'orders.payment': 'Platba', + 'orders.items': 'Položky', + 'orders.empty': 'Žádné objednávky', + 'orders.page': 'Stránka', + 'orders.prev': 'Předchozí', + 'orders.next': 'Další', + 'orders.detail': 'Detail objednávky', + 'orders.shipping': 'Doručení', + 'orders.shipment': 'Zásilka', + 'orders.tracking': 'Sledovací číslo', + 'orders.generateLabel': 'Vygenerovat štítek', + 'orders.downloadLabel': 'Stáhnout / vytisknout štítek', + 'orders.labelNotAvailable': 'Štítek po zaplacení objednávky.', + 'orders.carrier.inPost': 'InPost', + 'orders.carrier.dhl': 'DHL', + 'orders.carrier.dpd': 'DPD', + 'orders.service.parcelLocker': 'Výdejní box', + 'orders.service.courier': 'Kurýr', + 'orders.shipmentStatus.pending': 'Čeká', + 'orders.shipmentStatus.labelGenerated': 'Štítek vygenerován', + 'orders.shipmentStatus.dispatched': 'Odesláno', + 'orders.shipmentStatus.delivered': 'Doručeno', + 'orders.shipmentStatus.failed': 'Chyba', + 'orders.changeStatus': 'Změnit stav', + 'orders.subtotal': 'Mezisoučet', + 'orders.delivery': 'Doprava', + 'orders.discount': 'Sleva', + 'orders.vat': 'DPH', + 'orders.total': 'Celkem', + 'orders.qty': 'Množství', + 'orders.provider': 'Poskytovatel', + 'orders.invoice': 'Faktura', + 'orders.print': 'Tisk', + 'orders.issueDate': 'Datum vystavení', + 'orders.seller': 'Prodejce', + 'orders.buyer': 'Kupující', + 'orders.orderRef': 'Objednávka', + 'orders.refresh': 'Obnovit', + 'orders.warehouseStock': 'Skladové zásoby', + 'orders.sourceOrders': 'Objednávky', + 'orders.pickListSummary': 'Ke kompletaci: {{count}} objednávek (zaplacené / zpracovává se)', + 'orders.pickListEmpty': 'Nic ke kompletaci', + 'orders.pickFrom': 'Sklad', + 'orders.selectWarehouse': '— vyberte sklad —', + 'orders.markCollected': 'Označit jako sestavené', + 'orders.collected': 'Sestaveno', + 'orders.showCollected': 'Zobrazit sestavené', + 'orders.pendingItems': 'Čekající', + 'orders.collectedItems': 'Sestavené', + 'orders.orderWarehouse': 'Sklad pro objednávku', + 'orders.collectOrder': 'Sestavit objednávku', + 'orders.orderCollected': 'Objednávka sestavena', + 'orders.processing': 'Zpracovává se', + 'orders.shipped': 'Odesláno', + 'orders.status.pendingPayment': 'Čeká na platbu', + 'orders.status.paid': 'Zaplaceno', + 'orders.status.processing': 'Zpracovává se', + 'orders.status.shipped': 'Odesláno', + 'orders.status.delivered': 'Doručeno', + 'orders.status.cancelled': 'Zrušeno', + 'orders.status.refunded': 'Vráceno', + 'orders.paymentStatus.pending': 'Čeká', + 'orders.paymentStatus.completed': 'Zaplaceno', + 'orders.paymentStatus.failed': 'Neúspěšné', + 'orders.paymentStatus.refunded': 'Vráceno', + 'discounts.title': 'Slevy', + 'discounts.ruleName': 'Název pravidla', + 'discounts.code': 'Kód', + 'discounts.value': 'Hodnota %', + 'discounts.minOrder': 'Min. objednávka', + 'discounts.create': 'Vytvořit kód', + 'discounts.uses': 'Použití', + 'discounts.activate': 'Aktivovat', + 'discounts.deactivate': 'Deaktivovat', + 'discounts.empty': 'Žádné slevové kódy', + 'discounts.errorLoad': 'Nepodařilo se načíst slevy', + 'discounts.errorSave': 'Nepodařilo se uložit slevu', + 'promotions.title': 'Akce', + 'promotions.add': 'Přidat akci', + 'promotions.edit': 'Upravit akci', + 'promotions.name': 'Název', + 'promotions.typeLabel': 'Typ', + 'promotions.value': 'Hodnota', + 'promotions.minOrder': 'Min. objednávka (PLN)', + 'promotions.codes': 'Kódy / použití', + 'promotions.empty': 'Žádné akce', + 'promotions.errorLoad': 'Nepodařilo se načíst akce', + 'promotions.errorSave': 'Nepodařilo se uložit akci', + 'promotions.codeCreated': 'Kód vytvořen (zkopírován)', + 'promotions.codesFor': 'Kódy vybrané akce', + 'promotions.codeOptional': 'Kód (volitelně)', + 'promotions.maxUsages': 'Max. použití', + 'promotions.generateCode': 'Vygenerovat kód', + 'promotions.noCodes': 'Zatím žádné kódy', + 'promotions.sectionBenefit': 'Výhoda', + 'promotions.sectionRequirements': 'Požadavky', + 'promotions.sectionLimits': 'Limity použití', + 'promotions.sectionBanner': 'Banner', + 'promotions.sectionReward': 'Odměna po nákupu', + 'promotions.showInBanner': 'Zobrazit v banneru obchodu', + 'promotions.issueReward': 'Vydat jednorázový kód po zaplacení', + 'promotions.rewardMin': 'Min. zaplacená objednávka (PLN)', + 'promotions.rewardPrefix': 'Prefix kódu', + 'promotions.rewardDays': 'Platnost (dny)', + 'promotions.maxTotal': 'Max. celkových použití', + 'promotions.maxPerCustomer': 'Max. na zákazníka', + 'promotions.type.percent': 'Procento', + 'promotions.type.fixed': 'Pevná částka', + 'promotions.type.freeShip': 'Doprava zdarma', + 'newsletter.title': 'Newsletter', + 'newsletter.subject': 'Předmět', + 'newsletter.body': 'HTML obsah', + 'newsletter.send': 'Odeslat kampaň', + 'newsletter.subscribers': 'Odběratelé', + 'newsletter.campaigns': 'Kampaně', + 'newsletter.confirmSend': + 'Odeslat tuto kampaň všem aktivním odběratelům?', + 'newsletter.sentOk': 'Kampaň odeslána {{count}} odběratelům', + 'newsletter.emptySubs': 'Žádní odběratelé', + 'newsletter.emptyCamps': 'Žádné kampaně', + 'newsletter.errorLoad': 'Nepodařilo se načíst newsletter', + 'newsletter.errorSend': 'Nepodařilo se odeslat kampaň', + 'reviews.title': 'Recenze', + 'reviews.approve': 'Schválit', + 'reviews.reject': 'Zamítnout', + 'reviews.status': 'Stav', + 'reviews.status.pending': 'Čekající', + 'reviews.status.approved': 'Schválené', + 'reviews.status.rejected': 'Zamítnuté', + 'reviews.empty': 'Žádné recenze', + 'reviews.errorLoad': 'Nepodařilo se načíst recenze', + 'reviews.errorSave': 'Nepodařilo se aktualizovat recenzi', +}; diff --git a/src/admin/src/app/core/i18n/translations/de.ts b/src/admin/src/app/core/i18n/translations/de.ts new file mode 100644 index 0000000..dd4d3e4 --- /dev/null +++ b/src/admin/src/app/core/i18n/translations/de.ts @@ -0,0 +1,280 @@ +import { TranslationMap } from '../i18n.models'; + +export const DE_TRANSLATIONS: TranslationMap = { + 'meta.pageTitle': 'iLED Admin', + 'a11y.langSwitcher': 'Sprache wählen', + 'a11y.mainNav': 'Admin-Navigation', + 'nav.dashboard': 'Dashboard', + 'nav.products': 'Produkte', + 'nav.orders': 'Bestellungen', + 'nav.promotions': 'Aktionen', + 'nav.discounts': 'Aktionen', + 'nav.newsletter': 'Newsletter', + 'nav.reviews': 'Bewertungen', + 'nav.logout': 'Abmelden', + 'nav.openMenu': 'Menü öffnen', + 'nav.closeMenu': 'Menü schließen', + 'common.search': 'Suchen', + 'common.apply': 'Anwenden', + 'common.all': 'Alle', + 'common.page': 'Seite', + 'common.prev': 'Zurück', + 'common.next': 'Weiter', + 'common.close': 'Schließen', + 'common.saved': 'Gespeichert', + 'common.deleted': 'Gelöscht', + 'auth.loginTitle': 'Admin-Anmeldung', + 'auth.email': 'E-Mail', + 'auth.password': 'Passwort', + 'auth.login': 'Anmelden', + 'auth.errorRole': 'Administratorrechte erforderlich.', + 'auth.errorLogin': 'Anmeldung fehlgeschlagen.', + 'dashboard.title': 'Dashboard', + 'dashboard.ordersToday': 'Bestellungen heute', + 'dashboard.revenueToday': 'Umsatz heute', + 'dashboard.lowStock': 'Niedriger Bestand', + 'dashboard.pending': 'Ausstehend', + 'dashboard.chartTitle': 'Neue Bestellungen (14 Tage)', + 'dashboard.chartOrders': 'Bestellungen', + 'dashboard.errorLoad': 'Dashboard konnte nicht geladen werden', + 'products.title': 'Produkte', + 'products.add': 'Produkt hinzufügen', + 'products.edit': 'Bearbeiten', + 'products.save': 'Speichern', + 'products.cancel': 'Abbrechen', + 'products.name': 'Name', + 'products.slug': 'Slug', + 'products.price': 'Preis', + 'products.stockMain': 'Hauptlager', + 'products.stockSecond': 'Lager II', + 'products.totalStock': 'Gesamt', + 'products.reservedHint': 'In offenen Bestellungen reserviert', + 'products.inStock': 'Auf Lager', + 'products.outOfStock': 'Nicht verfügbar', + 'products.active': 'Aktiv', + 'products.shortDesc': 'Kurzbeschreibung', + 'products.description': 'Beschreibung', + 'products.tabProducts': 'Produktliste', + 'products.tabWarehouses': 'Lager', + 'products.tabGroups': 'Produktkategorien', + 'products.model': 'Modell', + 'products.ean': 'EAN', + 'products.group': 'Gruppe', + 'products.noGroup': '— keine Gruppe —', + 'products.productCode': 'Produktcode', + 'products.lowestPrice': 'Niedrigster Preis 30 Tage', + 'products.deliveryHours': 'Lieferzeit (h)', + 'products.bestseller': 'Bestseller', + 'products.newArrival': 'Neuheit', + 'products.close': 'Schließen', + 'products.loading': 'Laden…', + 'products.sectionBasic': 'Grunddaten', + 'products.sectionNames': 'Namen', + 'products.sectionShortDesc': 'Kurzbeschreibungen', + 'products.sectionDesc': 'Vollständige Beschreibungen', + 'products.sectionParams': 'Parameter', + 'products.sectionStock': 'Lagerbestände', + 'products.sectionImages': 'Bilder', + 'products.image': 'Bild', + 'products.imageUrl': 'Bild-URL', + 'products.imageAlt': 'Alt-Text', + 'products.addImage': 'Bild hinzufügen', + 'products.primaryImage': 'Hauptbild', + 'products.imageHint': 'Datei hochladen (zu WebP konvertiert) oder URL / Shop-Pfad eingeben', + 'products.uploadImage': 'Bild hochladen', + 'products.uploadHint': 'JPG, PNG, GIF, WebP, BMP — max. 10 MB, Konvertierung zu WebP', + 'products.uploading': 'Wird hochgeladen…', + 'products.errorUpload': 'Bild konnte nicht hochgeladen werden', + 'products.noImages': 'Keine Bilder', + 'products.noImage': 'Keins', + 'products.addParam': 'Parameter hinzufügen', + 'products.param': 'Parameter', + 'products.removeParam': 'Parameter entfernen', + 'products.paramName': 'Name', + 'products.paramValue': 'Wert', + 'products.noParams': 'Keine Parameter', + 'products.errorNamePl': 'Polnischer Name ist erforderlich', + 'products.errorLoad': 'Produkt konnte nicht geladen werden', + 'products.errorList': 'Produkte konnten nicht geladen werden', + 'products.errorSave': 'Produkt konnte nicht gespeichert werden', + 'products.searchPlaceholder': 'Name, Slug, Modell, SKU…', + 'products.activeOnly': 'Nur aktive', + 'products.inactiveOnly': 'Nur inaktive', + 'products.lowStockOnly': 'Niedriger Bestand', + 'products.inactive': 'Inaktiv', + 'products.empty': 'Keine Produkte', + 'categories.title': 'Kategorien', + 'categories.name': 'Name', + 'categories.slug': 'Slug', + 'categories.add': 'Kategorie hinzufügen', + 'categories.empty': 'Keine Kategorien', + 'categories.errorLoad': 'Kategorien konnten nicht geladen werden', + 'categories.errorSave': 'Kategorie konnte nicht gespeichert werden', + 'banner.title': 'Promo-Banner', + 'banner.text': 'Bannertext', + 'banner.code': 'Hervorgehobener Code', + 'banner.errorLoad': 'Banner konnte nicht geladen werden', + 'banner.errorSave': 'Banner konnte nicht gespeichert werden', + 'warehouses.name': 'Lagername', + 'warehouses.code': 'Code', + 'warehouses.sortOrder': 'Reihenfolge', + 'warehouses.add': 'Lager hinzufügen', + 'warehouses.delete': 'Löschen', + 'warehouses.errorRequired': 'Name und Code ausfüllen', + 'warehouses.errorSave': 'Lager konnte nicht hinzugefügt werden', + 'warehouses.errorDelete': 'Lager kann nicht gelöscht werden (hat Bestand)', + 'groups.name': 'Gruppenname', + 'groups.code': 'Code', + 'groups.sortOrder': 'Reihenfolge', + 'groups.add': 'Gruppe hinzufügen', + 'groups.edit': 'Bearbeiten', + 'groups.delete': 'Löschen', + 'groups.errorRequired': 'Polnischen Namen und Code ausfüllen', + 'groups.errorSave': 'Gruppe konnte nicht gespeichert werden', + 'groups.errorDelete': 'Gruppe konnte nicht gelöscht werden', + 'orders.title': 'Bestellungen', + 'orders.tabList': 'Bestellliste', + 'orders.tabPickList': 'Kommissionierung', + 'orders.search': 'Suchen', + 'orders.searchPlaceholder': 'Bestellnr., E-Mail, Kunde…', + 'orders.status': 'Status', + 'orders.allStatuses': 'Alle Status', + 'orders.allPayments': 'Alle Zahlungen', + 'orders.apply': 'Filtern', + 'orders.month': 'Monat', + 'orders.allMonths': 'Alle Monate', + 'orders.date': 'Datum', + 'orders.customer': 'Kunde', + 'orders.payment': 'Zahlung', + 'orders.items': 'Positionen', + 'orders.empty': 'Keine Bestellungen', + 'orders.page': 'Seite', + 'orders.prev': 'Zurück', + 'orders.next': 'Weiter', + 'orders.detail': 'Bestelldetails', + 'orders.shipping': 'Versand', + 'orders.shipment': 'Sendung', + 'orders.tracking': 'Sendungsnummer', + 'orders.generateLabel': 'Versandetikett erstellen', + 'orders.downloadLabel': 'Etikett herunterladen / drucken', + 'orders.labelNotAvailable': 'Etikett nach Zahlung verfügbar.', + 'orders.carrier.inPost': 'InPost', + 'orders.carrier.dhl': 'DHL', + 'orders.carrier.dpd': 'DPD', + 'orders.service.parcelLocker': 'Paketstation', + 'orders.service.courier': 'Kurier', + 'orders.shipmentStatus.pending': 'Ausstehend', + 'orders.shipmentStatus.labelGenerated': 'Etikett erstellt', + 'orders.shipmentStatus.dispatched': 'Versendet', + 'orders.shipmentStatus.delivered': 'Zugestellt', + 'orders.shipmentStatus.failed': 'Fehler', + 'orders.changeStatus': 'Status ändern', + 'orders.subtotal': 'Zwischensumme', + 'orders.delivery': 'Lieferung', + 'orders.discount': 'Rabatt', + 'orders.vat': 'MwSt.', + 'orders.total': 'Gesamt', + 'orders.qty': 'Menge', + 'orders.provider': 'Anbieter', + 'orders.invoice': 'Rechnung', + 'orders.print': 'Drucken', + 'orders.issueDate': 'Ausstellungsdatum', + 'orders.seller': 'Verkäufer', + 'orders.buyer': 'Käufer', + 'orders.orderRef': 'Bestellung', + 'orders.refresh': 'Aktualisieren', + 'orders.warehouseStock': 'Lagerbestand', + 'orders.sourceOrders': 'Bestellungen', + 'orders.pickListSummary': 'Zu kommissionieren: {{count}} Bestellungen (bezahlt / in Bearbeitung)', + 'orders.pickListEmpty': 'Nichts zu kommissionieren', + 'orders.pickFrom': 'Lager', + 'orders.selectWarehouse': '— Lager wählen —', + 'orders.markCollected': 'Kommissioniert', + 'orders.collected': 'Kommissioniert', + 'orders.showCollected': 'Kommissionierte anzeigen', + 'orders.pendingItems': 'Offen', + 'orders.collectedItems': 'Kommissioniert', + 'orders.orderWarehouse': 'Lager für Bestellung', + 'orders.collectOrder': 'Bestellung kommissionieren', + 'orders.orderCollected': 'Bestellung kommissioniert', + 'orders.processing': 'In Bearbeitung', + 'orders.shipped': 'Versendet', + 'orders.status.pendingPayment': 'Zahlung ausstehend', + 'orders.status.paid': 'Bezahlt', + 'orders.status.processing': 'In Bearbeitung', + 'orders.status.shipped': 'Versendet', + 'orders.status.delivered': 'Zugestellt', + 'orders.status.cancelled': 'Storniert', + 'orders.status.refunded': 'Erstattet', + 'orders.paymentStatus.pending': 'Ausstehend', + 'orders.paymentStatus.completed': 'Bezahlt', + 'orders.paymentStatus.failed': 'Fehlgeschlagen', + 'orders.paymentStatus.refunded': 'Erstattet', + 'discounts.title': 'Rabatte', + 'discounts.ruleName': 'Regelname', + 'discounts.code': 'Code', + 'discounts.value': 'Wert %', + 'discounts.minOrder': 'Min. Bestellung', + 'discounts.create': 'Code erstellen', + 'discounts.uses': 'Nutzungen', + 'discounts.activate': 'Aktivieren', + 'discounts.deactivate': 'Deaktivieren', + 'discounts.empty': 'Keine Rabattcodes', + 'discounts.errorLoad': 'Rabatte konnten nicht geladen werden', + 'discounts.errorSave': 'Rabatt konnte nicht gespeichert werden', + 'promotions.title': 'Aktionen', + 'promotions.add': 'Aktion hinzufügen', + 'promotions.edit': 'Aktion bearbeiten', + 'promotions.name': 'Name', + 'promotions.typeLabel': 'Typ', + 'promotions.value': 'Wert', + 'promotions.minOrder': 'Min. Bestellung (PLN)', + 'promotions.codes': 'Codes / Nutzungen', + 'promotions.empty': 'Keine Aktionen', + 'promotions.errorLoad': 'Aktionen konnten nicht geladen werden', + 'promotions.errorSave': 'Aktion konnte nicht gespeichert werden', + 'promotions.codeCreated': 'Code erstellt (kopiert)', + 'promotions.codesFor': 'Codes der ausgewählten Aktion', + 'promotions.codeOptional': 'Code (optional)', + 'promotions.maxUsages': 'Max. Nutzungen', + 'promotions.generateCode': 'Code erzeugen', + 'promotions.noCodes': 'Noch keine Codes', + 'promotions.sectionBenefit': 'Vorteil', + 'promotions.sectionRequirements': 'Voraussetzungen', + 'promotions.sectionLimits': 'Nutzungslimits', + 'promotions.sectionBanner': 'Banner', + 'promotions.sectionReward': 'Belohnung nach Kauf', + 'promotions.showInBanner': 'Im Shop-Banner zeigen', + 'promotions.issueReward': 'Einmalcode nach bezahlter Bestellung', + 'promotions.rewardMin': 'Min. bezahlter Bestellwert (PLN)', + 'promotions.rewardPrefix': 'Code-Präfix', + 'promotions.rewardDays': 'Gültigkeit (Tage)', + 'promotions.maxTotal': 'Max. Gesamtnutzungen', + 'promotions.maxPerCustomer': 'Max. pro Kunde', + 'promotions.type.percent': 'Prozent', + 'promotions.type.fixed': 'Fester Betrag', + 'promotions.type.freeShip': 'Kostenloser Versand', + 'newsletter.title': 'Newsletter', + 'newsletter.subject': 'Betreff', + 'newsletter.body': 'HTML-Inhalt', + 'newsletter.send': 'Kampagne senden', + 'newsletter.subscribers': 'Abonnenten', + 'newsletter.campaigns': 'Kampagnen', + 'newsletter.confirmSend': + 'Diese Kampagne an alle aktiven Abonnenten senden?', + 'newsletter.sentOk': 'Kampagne an {{count}} Abonnenten gesendet', + 'newsletter.emptySubs': 'Keine Abonnenten', + 'newsletter.emptyCamps': 'Keine Kampagnen', + 'newsletter.errorLoad': 'Newsletter-Daten konnten nicht geladen werden', + 'newsletter.errorSend': 'Kampagne konnte nicht gesendet werden', + 'reviews.title': 'Bewertungen', + 'reviews.approve': 'Genehmigen', + 'reviews.reject': 'Ablehnen', + 'reviews.status': 'Status', + 'reviews.status.pending': 'Ausstehend', + 'reviews.status.approved': 'Genehmigt', + 'reviews.status.rejected': 'Abgelehnt', + 'reviews.empty': 'Keine Bewertungen', + 'reviews.errorLoad': 'Bewertungen konnten nicht geladen werden', + 'reviews.errorSave': 'Bewertung konnte nicht aktualisiert werden', +}; diff --git a/src/admin/src/app/core/i18n/translations/en.ts b/src/admin/src/app/core/i18n/translations/en.ts new file mode 100644 index 0000000..2bb3fbb --- /dev/null +++ b/src/admin/src/app/core/i18n/translations/en.ts @@ -0,0 +1,280 @@ +import { TranslationMap } from '../i18n.models'; + +export const EN_TRANSLATIONS: TranslationMap = { + 'meta.pageTitle': 'iLED Admin', + 'a11y.langSwitcher': 'Choose language', + 'a11y.mainNav': 'Admin navigation', + 'nav.dashboard': 'Dashboard', + 'nav.products': 'Products', + 'nav.orders': 'Orders', + 'nav.promotions': 'Promotions', + 'nav.discounts': 'Promotions', + 'nav.newsletter': 'Newsletter', + 'nav.reviews': 'Reviews', + 'nav.logout': 'Sign out', + 'nav.openMenu': 'Open menu', + 'nav.closeMenu': 'Close menu', + 'common.search': 'Search', + 'common.apply': 'Apply', + 'common.all': 'All', + 'common.page': 'Page', + 'common.prev': 'Previous', + 'common.next': 'Next', + 'common.close': 'Close', + 'common.saved': 'Saved', + 'common.deleted': 'Deleted', + 'auth.loginTitle': 'Admin login', + 'auth.email': 'Email', + 'auth.password': 'Password', + 'auth.login': 'Sign in', + 'auth.errorRole': 'Administrator permissions required.', + 'auth.errorLogin': 'Login failed.', + 'dashboard.title': 'Dashboard', + 'dashboard.ordersToday': 'Orders today', + 'dashboard.revenueToday': 'Revenue today', + 'dashboard.lowStock': 'Low stock', + 'dashboard.pending': 'Pending', + 'dashboard.chartTitle': 'New orders (14 days)', + 'dashboard.chartOrders': 'Orders', + 'dashboard.errorLoad': 'Failed to load dashboard', + 'products.title': 'Products', + 'products.add': 'Add product', + 'products.edit': 'Edit', + 'products.save': 'Save', + 'products.cancel': 'Cancel', + 'products.name': 'Name', + 'products.slug': 'Slug', + 'products.price': 'Price', + 'products.stockMain': 'Main warehouse', + 'products.stockSecond': 'Warehouse II', + 'products.totalStock': 'Total', + 'products.reservedHint': 'Reserved in open orders', + 'products.inStock': 'In stock', + 'products.outOfStock': 'Out of stock', + 'products.active': 'Active', + 'products.shortDesc': 'Short description', + 'products.description': 'Description', + 'products.tabProducts': 'Product list', + 'products.tabWarehouses': 'Warehouses', + 'products.tabGroups': 'Product categories', + 'products.model': 'Model', + 'products.ean': 'EAN', + 'products.group': 'Group', + 'products.noGroup': '— no group —', + 'products.productCode': 'Product code', + 'products.lowestPrice': 'Lowest price 30 days', + 'products.deliveryHours': 'Delivery time (h)', + 'products.bestseller': 'Bestseller', + 'products.newArrival': 'New arrival', + 'products.close': 'Close', + 'products.loading': 'Loading…', + 'products.sectionBasic': 'Basic info', + 'products.sectionNames': 'Names', + 'products.sectionShortDesc': 'Short descriptions', + 'products.sectionDesc': 'Full descriptions', + 'products.sectionParams': 'Parameters', + 'products.sectionStock': 'Warehouse stock', + 'products.sectionImages': 'Images', + 'products.image': 'Image', + 'products.imageUrl': 'Image URL', + 'products.imageAlt': 'Alt text', + 'products.addImage': 'Add image', + 'products.primaryImage': 'Primary', + 'products.imageHint': 'Upload a file (converted to WebP) or enter a URL / shop path', + 'products.uploadImage': 'Upload image', + 'products.uploadHint': 'JPG, PNG, GIF, WebP, BMP — max 10 MB, converted to WebP', + 'products.uploading': 'Uploading…', + 'products.errorUpload': 'Failed to upload image', + 'products.noImages': 'No images', + 'products.noImage': 'None', + 'products.addParam': 'Add parameter', + 'products.param': 'Parameter', + 'products.removeParam': 'Remove parameter', + 'products.paramName': 'Name', + 'products.paramValue': 'Value', + 'products.noParams': 'No parameters', + 'products.errorNamePl': 'Polish name is required', + 'products.errorLoad': 'Failed to load product', + 'products.errorList': 'Failed to load products', + 'products.errorSave': 'Failed to save product', + 'products.searchPlaceholder': 'Name, slug, model, SKU…', + 'products.activeOnly': 'Active only', + 'products.inactiveOnly': 'Inactive only', + 'products.lowStockOnly': 'Low stock', + 'products.inactive': 'Inactive', + 'products.empty': 'No products', + 'categories.title': 'Categories', + 'categories.name': 'Name', + 'categories.slug': 'Slug', + 'categories.add': 'Add category', + 'categories.empty': 'No categories', + 'categories.errorLoad': 'Failed to load categories', + 'categories.errorSave': 'Failed to save category', + 'banner.title': 'Promo banner', + 'banner.text': 'Banner text', + 'banner.code': 'Highlighted code', + 'banner.errorLoad': 'Failed to load banner', + 'banner.errorSave': 'Failed to save banner', + 'warehouses.name': 'Warehouse name', + 'warehouses.code': 'Code', + 'warehouses.sortOrder': 'Sort order', + 'warehouses.add': 'Add warehouse', + 'warehouses.delete': 'Delete', + 'warehouses.errorRequired': 'Fill in warehouse name and code', + 'warehouses.errorSave': 'Failed to add warehouse', + 'warehouses.errorDelete': 'Cannot delete warehouse (has stock)', + 'groups.name': 'Group name', + 'groups.code': 'Code', + 'groups.sortOrder': 'Sort order', + 'groups.add': 'Add group', + 'groups.edit': 'Edit', + 'groups.delete': 'Delete', + 'groups.errorRequired': 'Fill in Polish name and group code', + 'groups.errorSave': 'Failed to save group', + 'groups.errorDelete': 'Failed to delete group', + 'orders.title': 'Orders', + 'orders.tabList': 'Order list', + 'orders.tabPickList': 'Pick list', + 'orders.search': 'Search', + 'orders.searchPlaceholder': 'Order no., email, customer…', + 'orders.status': 'Status', + 'orders.allStatuses': 'All statuses', + 'orders.allPayments': 'All payments', + 'orders.apply': 'Apply', + 'orders.month': 'Month', + 'orders.allMonths': 'All months', + 'orders.date': 'Date', + 'orders.customer': 'Customer', + 'orders.payment': 'Payment', + 'orders.items': 'Items', + 'orders.empty': 'No orders', + 'orders.page': 'Page', + 'orders.prev': 'Previous', + 'orders.next': 'Next', + 'orders.detail': 'Order details', + 'orders.shipping': 'Shipping', + 'orders.shipment': 'Shipment', + 'orders.tracking': 'Tracking number', + 'orders.generateLabel': 'Generate shipping label', + 'orders.downloadLabel': 'Download / print label', + 'orders.labelNotAvailable': 'Label available after payment.', + 'orders.carrier.inPost': 'InPost', + 'orders.carrier.dhl': 'DHL', + 'orders.carrier.dpd': 'DPD', + 'orders.service.parcelLocker': 'Parcel locker', + 'orders.service.courier': 'Courier', + 'orders.shipmentStatus.pending': 'Pending', + 'orders.shipmentStatus.labelGenerated': 'Label generated', + 'orders.shipmentStatus.dispatched': 'Dispatched', + 'orders.shipmentStatus.delivered': 'Delivered', + 'orders.shipmentStatus.failed': 'Failed', + 'orders.changeStatus': 'Change status', + 'orders.subtotal': 'Subtotal', + 'orders.delivery': 'Delivery', + 'orders.discount': 'Discount', + 'orders.vat': 'VAT', + 'orders.total': 'Total', + 'orders.qty': 'Qty', + 'orders.provider': 'Provider', + 'orders.invoice': 'Invoice', + 'orders.print': 'Print', + 'orders.issueDate': 'Issue date', + 'orders.seller': 'Seller', + 'orders.buyer': 'Buyer', + 'orders.orderRef': 'Order', + 'orders.refresh': 'Refresh', + 'orders.warehouseStock': 'Warehouse stock', + 'orders.sourceOrders': 'Orders', + 'orders.pickListSummary': 'To pick: {{count}} orders (paid / processing)', + 'orders.pickListEmpty': 'Nothing to pick', + 'orders.pickFrom': 'Warehouse', + 'orders.selectWarehouse': '— select warehouse —', + 'orders.markCollected': 'Mark collected', + 'orders.collected': 'Collected', + 'orders.showCollected': 'Show collected', + 'orders.pendingItems': 'Pending', + 'orders.collectedItems': 'Collected', + 'orders.orderWarehouse': 'Warehouse for order', + 'orders.collectOrder': 'Collect order', + 'orders.orderCollected': 'Order collected', + 'orders.processing': 'Processing', + 'orders.shipped': 'Shipped', + 'orders.status.pendingPayment': 'Pending payment', + 'orders.status.paid': 'Paid', + 'orders.status.processing': 'Processing', + 'orders.status.shipped': 'Shipped', + 'orders.status.delivered': 'Delivered', + 'orders.status.cancelled': 'Cancelled', + 'orders.status.refunded': 'Refunded', + 'orders.paymentStatus.pending': 'Pending', + 'orders.paymentStatus.completed': 'Paid', + 'orders.paymentStatus.failed': 'Failed', + 'orders.paymentStatus.refunded': 'Refunded', + 'discounts.title': 'Discounts', + 'discounts.ruleName': 'Rule name', + 'discounts.code': 'Code', + 'discounts.value': 'Value %', + 'discounts.minOrder': 'Min. order', + 'discounts.create': 'Create code', + 'discounts.uses': 'Uses', + 'discounts.activate': 'Activate', + 'discounts.deactivate': 'Deactivate', + 'discounts.empty': 'No discount codes', + 'discounts.errorLoad': 'Failed to load discounts', + 'discounts.errorSave': 'Failed to save discount', + 'promotions.title': 'Promotions', + 'promotions.add': 'Add promotion', + 'promotions.edit': 'Edit promotion', + 'promotions.name': 'Name', + 'promotions.typeLabel': 'Type', + 'promotions.value': 'Value', + 'promotions.minOrder': 'Min. order (PLN)', + 'promotions.codes': 'Codes / uses', + 'promotions.empty': 'No promotions', + 'promotions.errorLoad': 'Failed to load promotions', + 'promotions.errorSave': 'Failed to save promotion', + 'promotions.codeCreated': 'Code created (copied)', + 'promotions.codesFor': 'Codes for selected promotion', + 'promotions.codeOptional': 'Code (optional)', + 'promotions.maxUsages': 'Max uses', + 'promotions.generateCode': 'Generate code', + 'promotions.noCodes': 'No codes yet', + 'promotions.sectionBenefit': 'Benefit', + 'promotions.sectionRequirements': 'Requirements', + 'promotions.sectionLimits': 'Usage limits', + 'promotions.sectionBanner': 'Banner', + 'promotions.sectionReward': 'Reward after purchase', + 'promotions.showInBanner': 'Show in site banner', + 'promotions.issueReward': 'Issue one-time code after paid order', + 'promotions.rewardMin': 'Min. paid order total (PLN)', + 'promotions.rewardPrefix': 'Code prefix', + 'promotions.rewardDays': 'Valid days', + 'promotions.maxTotal': 'Max total redemptions', + 'promotions.maxPerCustomer': 'Max per customer', + 'promotions.type.percent': 'Percentage', + 'promotions.type.fixed': 'Fixed amount', + 'promotions.type.freeShip': 'Free shipping', + 'newsletter.title': 'Newsletter', + 'newsletter.subject': 'Subject', + 'newsletter.body': 'HTML body', + 'newsletter.send': 'Send campaign', + 'newsletter.subscribers': 'Subscribers', + 'newsletter.campaigns': 'Campaigns', + 'newsletter.confirmSend': + 'Send this campaign to all active subscribers?', + 'newsletter.sentOk': 'Campaign sent to {{count}} subscribers', + 'newsletter.emptySubs': 'No subscribers', + 'newsletter.emptyCamps': 'No campaigns', + 'newsletter.errorLoad': 'Failed to load newsletter data', + 'newsletter.errorSend': 'Failed to send campaign', + 'reviews.title': 'Reviews', + 'reviews.approve': 'Approve', + 'reviews.reject': 'Reject', + 'reviews.status': 'Status', + 'reviews.status.pending': 'Pending', + 'reviews.status.approved': 'Approved', + 'reviews.status.rejected': 'Rejected', + 'reviews.empty': 'No reviews', + 'reviews.errorLoad': 'Failed to load reviews', + 'reviews.errorSave': 'Failed to update review', +}; diff --git a/src/admin/src/app/core/i18n/translations/index.ts b/src/admin/src/app/core/i18n/translations/index.ts new file mode 100644 index 0000000..4fd23d7 --- /dev/null +++ b/src/admin/src/app/core/i18n/translations/index.ts @@ -0,0 +1,12 @@ +import { LocaleCode, TranslationMap } from '../i18n.models'; +import { CS_TRANSLATIONS } from './cs'; +import { DE_TRANSLATIONS } from './de'; +import { EN_TRANSLATIONS } from './en'; +import { PL_TRANSLATIONS } from './pl'; + +export const TRANSLATIONS: Record = { + pl: PL_TRANSLATIONS, + en: EN_TRANSLATIONS, + de: DE_TRANSLATIONS, + cs: CS_TRANSLATIONS, +}; diff --git a/src/admin/src/app/core/i18n/translations/pl.ts b/src/admin/src/app/core/i18n/translations/pl.ts new file mode 100644 index 0000000..5edde03 --- /dev/null +++ b/src/admin/src/app/core/i18n/translations/pl.ts @@ -0,0 +1,280 @@ +import { TranslationMap } from '../i18n.models'; + +export const PL_TRANSLATIONS: TranslationMap = { + 'meta.pageTitle': 'iLED Admin', + 'a11y.langSwitcher': 'Wybierz język', + 'a11y.mainNav': 'Nawigacja panelu', + 'nav.dashboard': 'Dashboard', + 'nav.products': 'Produkty', + 'nav.orders': 'Zamówienia', + 'nav.promotions': 'Promocje', + 'nav.discounts': 'Promocje', + 'nav.newsletter': 'Newsletter', + 'nav.reviews': 'Opinie', + 'nav.logout': 'Wyloguj', + 'nav.openMenu': 'Otwórz menu', + 'nav.closeMenu': 'Zamknij menu', + 'common.search': 'Szukaj', + 'common.apply': 'Zastosuj', + 'common.all': 'Wszystkie', + 'common.page': 'Strona', + 'common.prev': 'Poprzednia', + 'common.next': 'Następna', + 'common.close': 'Zamknij', + 'common.saved': 'Zapisano', + 'common.deleted': 'Usunięto', + 'auth.loginTitle': 'Logowanie admin', + 'auth.email': 'Email', + 'auth.password': 'Hasło', + 'auth.login': 'Zaloguj się', + 'auth.errorRole': 'Brak uprawnień administratora.', + 'auth.errorLogin': 'Błąd logowania.', + 'dashboard.title': 'Dashboard', + 'dashboard.ordersToday': 'Zamówienia dziś', + 'dashboard.revenueToday': 'Przychód dziś', + 'dashboard.lowStock': 'Niski stan', + 'dashboard.pending': 'Oczekujące', + 'dashboard.chartTitle': 'Nowe zamówienia (14 dni)', + 'dashboard.chartOrders': 'Zamówienia', + 'dashboard.errorLoad': 'Nie udało się wczytać pulpitu', + 'products.title': 'Produkty', + 'products.add': 'Dodaj produkt', + 'products.edit': 'Edytuj', + 'products.save': 'Zapisz', + 'products.cancel': 'Anuluj', + 'products.name': 'Nazwa', + 'products.slug': 'Slug', + 'products.price': 'Cena', + 'products.stockMain': 'Magazyn główny', + 'products.stockSecond': 'Magazyn II', + 'products.totalStock': 'Razem', + 'products.reservedHint': 'Zarezerwowane w otwartych zamówieniach', + 'products.inStock': 'W magazynie', + 'products.outOfStock': 'Brak', + 'products.active': 'Aktywny', + 'products.shortDesc': 'Krótki opis', + 'products.description': 'Opis', + 'products.tabProducts': 'Lista produktów', + 'products.tabWarehouses': 'Magazyny', + 'products.tabGroups': 'Kategorie produktów', + 'products.model': 'Model', + 'products.ean': 'EAN', + 'products.group': 'Grupa', + 'products.noGroup': '— brak grupy —', + 'products.productCode': 'Kod produktu', + 'products.lowestPrice': 'Najniższa cena 30 dni', + 'products.deliveryHours': 'Czas dostawy (h)', + 'products.bestseller': 'Bestseller', + 'products.newArrival': 'Nowość', + 'products.close': 'Zamknij', + 'products.loading': 'Ładowanie…', + 'products.sectionBasic': 'Dane podstawowe', + 'products.sectionNames': 'Nazwy', + 'products.sectionShortDesc': 'Krótkie opisy', + 'products.sectionDesc': 'Pełne opisy', + 'products.sectionParams': 'Parametry', + 'products.sectionStock': 'Stany magazynowe', + 'products.sectionImages': 'Zdjęcia', + 'products.image': 'Zdjęcie', + 'products.imageUrl': 'URL zdjęcia', + 'products.imageAlt': 'Tekst alternatywny', + 'products.addImage': 'Dodaj zdjęcie', + 'products.primaryImage': 'Główne', + 'products.imageHint': 'Prześlij plik (konwertowany do WebP) lub podaj URL / ścieżkę sklepu', + 'products.uploadImage': 'Prześlij zdjęcie', + 'products.uploadHint': 'JPG, PNG, GIF, WebP, BMP — max 10 MB, konwersja do WebP', + 'products.uploading': 'Przesyłanie…', + 'products.errorUpload': 'Nie udało się przesłać zdjęcia', + 'products.noImages': 'Brak zdjęć', + 'products.noImage': 'Brak', + 'products.addParam': 'Dodaj parametr', + 'products.param': 'Parametr', + 'products.removeParam': 'Usuń parametr', + 'products.paramName': 'Nazwa', + 'products.paramValue': 'Wartość', + 'products.noParams': 'Brak parametrów', + 'products.errorNamePl': 'Nazwa PL jest wymagana', + 'products.errorLoad': 'Nie udało się wczytać produktu', + 'products.errorList': 'Nie udało się wczytać produktów', + 'products.errorSave': 'Nie udało się zapisać produktu', + 'products.searchPlaceholder': 'Nazwa, slug, model, SKU…', + 'products.activeOnly': 'Tylko aktywne', + 'products.inactiveOnly': 'Tylko nieaktywne', + 'products.lowStockOnly': 'Niski stan', + 'products.inactive': 'Nieaktywny', + 'products.empty': 'Brak produktów', + 'categories.title': 'Kategorie', + 'categories.name': 'Nazwa', + 'categories.slug': 'Slug', + 'categories.add': 'Dodaj kategorię', + 'categories.empty': 'Brak kategorii', + 'categories.errorLoad': 'Nie udało się wczytać kategorii', + 'categories.errorSave': 'Nie udało się zapisać kategorii', + 'banner.title': 'Baner promo', + 'banner.text': 'Tekst banera', + 'banner.code': 'Podświetlony kod', + 'banner.errorLoad': 'Nie udało się wczytać banera', + 'banner.errorSave': 'Nie udało się zapisać banera', + 'warehouses.name': 'Nazwa magazynu', + 'warehouses.code': 'Kod', + 'warehouses.sortOrder': 'Kolejność', + 'warehouses.add': 'Dodaj magazyn', + 'warehouses.delete': 'Usuń', + 'warehouses.errorRequired': 'Wypełnij nazwę i kod magazynu', + 'warehouses.errorSave': 'Nie udało się dodać magazynu', + 'warehouses.errorDelete': 'Nie można usunąć magazynu (ma stany)', + 'groups.name': 'Nazwa grupy', + 'groups.code': 'Kod', + 'groups.sortOrder': 'Kolejność', + 'groups.add': 'Dodaj grupę', + 'groups.edit': 'Edytuj', + 'groups.delete': 'Usuń', + 'groups.errorRequired': 'Wypełnij nazwę PL i kod grupy', + 'groups.errorSave': 'Nie udało się zapisać grupy', + 'groups.errorDelete': 'Nie udało się usunąć grupy', + 'orders.title': 'Zamówienia', + 'orders.tabList': 'Lista zamówień', + 'orders.tabPickList': 'Kompletacja', + 'orders.search': 'Szukaj', + 'orders.searchPlaceholder': 'Nr zamówienia, email, klient…', + 'orders.status': 'Status', + 'orders.allStatuses': 'Wszystkie statusy', + 'orders.allPayments': 'Wszystkie płatności', + 'orders.apply': 'Filtruj', + 'orders.month': 'Miesiąc', + 'orders.allMonths': 'Wszystkie miesiące', + 'orders.date': 'Data', + 'orders.customer': 'Klient', + 'orders.payment': 'Płatność', + 'orders.items': 'Pozycje', + 'orders.empty': 'Brak zamówień', + 'orders.page': 'Strona', + 'orders.prev': 'Poprzednia', + 'orders.next': 'Następna', + 'orders.detail': 'Szczegóły zamówienia', + 'orders.shipping': 'Dostawa', + 'orders.shipment': 'Przesyłka', + 'orders.tracking': 'Numer śledzenia', + 'orders.generateLabel': 'Generuj etykietę', + 'orders.downloadLabel': 'Pobierz / drukuj etykietę', + 'orders.labelNotAvailable': 'Etykieta dostępna po opłaceniu zamówienia.', + 'orders.carrier.inPost': 'InPost', + 'orders.carrier.dhl': 'DHL', + 'orders.carrier.dpd': 'DPD', + 'orders.service.parcelLocker': 'Paczkomat', + 'orders.service.courier': 'Kurier', + 'orders.shipmentStatus.pending': 'Oczekuje', + 'orders.shipmentStatus.labelGenerated': 'Etykieta wygenerowana', + 'orders.shipmentStatus.dispatched': 'Wysłane', + 'orders.shipmentStatus.delivered': 'Dostarczone', + 'orders.shipmentStatus.failed': 'Błąd', + 'orders.changeStatus': 'Zmień status', + 'orders.subtotal': 'Suma netto', + 'orders.delivery': 'Dostawa', + 'orders.discount': 'Rabat', + 'orders.vat': 'VAT', + 'orders.total': 'Razem', + 'orders.qty': 'Ilość', + 'orders.provider': 'Operator', + 'orders.invoice': 'Faktura', + 'orders.print': 'Drukuj', + 'orders.issueDate': 'Data wystawienia', + 'orders.seller': 'Sprzedawca', + 'orders.buyer': 'Nabywca', + 'orders.orderRef': 'Zamówienie', + 'orders.refresh': 'Odśwież', + 'orders.warehouseStock': 'Stany magazynowe', + 'orders.sourceOrders': 'Zamówienia', + 'orders.pickListSummary': 'Do kompletacji: {{count}} zamówień (opłacone / w realizacji)', + 'orders.pickListEmpty': 'Brak pozycji do kompletacji', + 'orders.pickFrom': 'Magazyn', + 'orders.selectWarehouse': '— wybierz magazyn —', + 'orders.markCollected': 'Skompletowano', + 'orders.collected': 'Skompletowane', + 'orders.showCollected': 'Pokaż skompletowane', + 'orders.pendingItems': 'Do kompletacji', + 'orders.collectedItems': 'Skompletowane', + 'orders.orderWarehouse': 'Magazyn dla zamówienia', + 'orders.collectOrder': 'Skompletuj zamówienie', + 'orders.orderCollected': 'Zamówienie skompletowane', + 'orders.processing': 'W realizacji', + 'orders.shipped': 'Wysłane', + 'orders.status.pendingPayment': 'Oczekuje płatności', + 'orders.status.paid': 'Opłacone', + 'orders.status.processing': 'W realizacji', + 'orders.status.shipped': 'Wysłane', + 'orders.status.delivered': 'Dostarczone', + 'orders.status.cancelled': 'Anulowane', + 'orders.status.refunded': 'Zwrócone', + 'orders.paymentStatus.pending': 'Oczekuje', + 'orders.paymentStatus.completed': 'Opłacone', + 'orders.paymentStatus.failed': 'Nieudane', + 'orders.paymentStatus.refunded': 'Zwrócone', + 'discounts.title': 'Rabaty', + 'discounts.ruleName': 'Nazwa reguły', + 'discounts.code': 'Kod', + 'discounts.value': 'Wartość %', + 'discounts.minOrder': 'Min. zamówienie', + 'discounts.create': 'Utwórz kod', + 'discounts.uses': 'Użycia', + 'discounts.activate': 'Aktywuj', + 'discounts.deactivate': 'Dezaktywuj', + 'discounts.empty': 'Brak kodów rabatowych', + 'discounts.errorLoad': 'Nie udało się wczytać rabatów', + 'discounts.errorSave': 'Nie udało się zapisać rabatu', + 'promotions.title': 'Promocje', + 'promotions.add': 'Dodaj promocję', + 'promotions.edit': 'Edytuj promocję', + 'promotions.name': 'Nazwa', + 'promotions.typeLabel': 'Typ', + 'promotions.value': 'Wartość', + 'promotions.minOrder': 'Min. zamówienie (PLN)', + 'promotions.codes': 'Kody / użycia', + 'promotions.empty': 'Brak promocji', + 'promotions.errorLoad': 'Nie udało się wczytać promocji', + 'promotions.errorSave': 'Nie udało się zapisać promocji', + 'promotions.codeCreated': 'Kod utworzony (skopiowany)', + 'promotions.codesFor': 'Kody wybranej promocji', + 'promotions.codeOptional': 'Kod (opcjonalnie)', + 'promotions.maxUsages': 'Max użyć', + 'promotions.generateCode': 'Generuj kod', + 'promotions.noCodes': 'Brak kodów', + 'promotions.sectionBenefit': 'Korzyść', + 'promotions.sectionRequirements': 'Wymagania', + 'promotions.sectionLimits': 'Limity użyć', + 'promotions.sectionBanner': 'Baner', + 'promotions.sectionReward': 'Nagroda po zakupie', + 'promotions.showInBanner': 'Pokaż w banerze sklepu', + 'promotions.issueReward': 'Wystaw jednorazowy kod po opłaceniu', + 'promotions.rewardMin': 'Min. wartość opłaconego zamówienia (PLN)', + 'promotions.rewardPrefix': 'Prefix kodu', + 'promotions.rewardDays': 'Ważność (dni)', + 'promotions.maxTotal': 'Max użyć łącznie', + 'promotions.maxPerCustomer': 'Max na klienta', + 'promotions.type.percent': 'Procent', + 'promotions.type.fixed': 'Kwota stała', + 'promotions.type.freeShip': 'Darmowa dostawa', + 'newsletter.title': 'Newsletter', + 'newsletter.subject': 'Temat', + 'newsletter.body': 'Treść HTML', + 'newsletter.send': 'Wyślij kampanię', + 'newsletter.subscribers': 'Subskrybenci', + 'newsletter.campaigns': 'Kampanie', + 'newsletter.confirmSend': + 'Wysłać tę kampanię do wszystkich aktywnych subskrybentów?', + 'newsletter.sentOk': 'Kampania wysłana do {{count}} subskrybentów', + 'newsletter.emptySubs': 'Brak subskrybentów', + 'newsletter.emptyCamps': 'Brak kampanii', + 'newsletter.errorLoad': 'Nie udało się wczytać newslettera', + 'newsletter.errorSend': 'Nie udało się wysłać kampanii', + 'reviews.title': 'Opinie', + 'reviews.approve': 'Zatwierdź', + 'reviews.reject': 'Odrzuć', + 'reviews.status': 'Status', + 'reviews.status.pending': 'Oczekujące', + 'reviews.status.approved': 'Zatwierdzone', + 'reviews.status.rejected': 'Odrzucone', + 'reviews.empty': 'Brak opinii', + 'reviews.errorLoad': 'Nie udało się wczytać opinii', + 'reviews.errorSave': 'Nie udało się zaktualizować opinii', +}; diff --git a/src/admin/src/app/core/product-image.util.ts b/src/admin/src/app/core/product-image.util.ts new file mode 100644 index 0000000..22e5aa3 --- /dev/null +++ b/src/admin/src/app/core/product-image.util.ts @@ -0,0 +1,21 @@ +import { API_BASE_URL, SHOP_URL } from './constants'; + +/** Resolves product image paths for admin previews. */ +export function resolveProductImageUrl( + url: string | null | undefined, +): string { + if (!url) { + return ''; + } + + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + if (url.startsWith('/uploads/')) { + return `${API_BASE_URL}${url}`; + } + + const path = url.startsWith('/') ? url : `/${url}`; + return `${SHOP_URL}${path}`; +} diff --git a/src/admin/src/app/features/admin-pages.component.ts b/src/admin/src/app/features/admin-pages.component.ts new file mode 100644 index 0000000..af09343 --- /dev/null +++ b/src/admin/src/app/features/admin-pages.component.ts @@ -0,0 +1,538 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, + signal, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { I18nCurrencyPipe } from '../core/i18n/i18n-currency.pipe'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; +import { ApiService } from '../core/api.service'; +import { I18nService } from '../core/i18n/i18n.service'; +import { AdminListToolbarComponent } from '../shared/admin-list-toolbar.component'; +import { AdminPaginationComponent } from '../shared/admin-pagination.component'; +import { ToastService } from '../shared/toast.service'; +import { ChartPoint, OrdersChartComponent } from './orders-chart.component'; + +interface Paginated { + items: T[]; + page: number; + pageSize: number; + totalCount: number; +} + +@Component({ + selector: 'app-dashboard', + standalone: true, + imports: [I18nCurrencyPipe, TranslatePipe, OrdersChartComponent, RouterLink], + template: ` +

{{ 'dashboard.title' | translate }}

+ @if (loading()) { +

+ {{ 'products.loading' | translate }} +

+ } + @if (error()) { +

{{ error() | translate }}

+ } + @if (stats(); as s) { + + } + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardComponent implements OnInit { + readonly #api = inject(ApiService); + readonly stats = signal(null); + readonly chart = signal([]); + readonly loading = signal(false); + readonly error = signal(''); + + ngOnInit(): void { + this.loading.set(true); + this.#api.get('/admin/dashboard').subscribe({ + next: (s) => { + this.stats.set(s); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + this.error.set('dashboard.errorLoad'); + }, + }); + this.#api + .get('/admin/dashboard/orders-chart?days=14') + .subscribe({ + next: (c) => this.chart.set(c), + }); + } +} + +interface DashboardStats { + ordersToday: number; + revenueToday: number; + lowStockCount: number; + pendingOrders: number; +} + +@Component({ + selector: 'app-discounts-admin', + standalone: true, + imports: [ + FormsModule, + TranslatePipe, + AdminListToolbarComponent, + AdminPaginationComponent, + ], + templateUrl: './discounts-admin.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DiscountsAdminComponent implements OnInit { + readonly #api = inject(ApiService); + readonly #toast = inject(ToastService); + readonly codes = signal([]); + readonly loading = signal(false); + readonly totalCount = signal(0); + ruleName = ''; + code = ''; + value = 20; + minOrderAmount: number | null = null; + validFrom = ''; + validTo = ''; + search = ''; + filterActive: '' | 'true' | 'false' = ''; + page = 1; + readonly pageSize = 20; + + ngOnInit(): void { + this.load(); + } + + load(): void { + this.loading.set(true); + const params = new URLSearchParams({ + page: String(this.page), + pageSize: String(this.pageSize), + }); + if (this.search.trim()) { + params.set('search', this.search.trim()); + } + if (this.filterActive) { + params.set('isActive', this.filterActive); + } + + this.#api + .get>(`/marketing/discount-codes?${params}`) + .subscribe({ + next: (res) => { + this.codes.set(res.items); + this.totalCount.set(res.totalCount); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + this.#toast.error('discounts.errorLoad'); + }, + }); + } + + handleSearch(): void { + this.page = 1; + this.load(); + } + + handleFilterChange(): void { + this.page = 1; + this.load(); + } + + handlePrevPage(): void { + if (this.page > 1) { + this.page -= 1; + this.load(); + } + } + + handleNextPage(): void { + const pages = Math.max(1, Math.ceil(this.totalCount() / this.pageSize)); + if (this.page < pages) { + this.page += 1; + this.load(); + } + } + + create(): void { + this.#api + .post('/marketing/discount-rules', { + name: this.ruleName, + type: 0, + value: this.value, + minimumOrderAmount: this.minOrderAmount, + validFrom: this.validFrom || null, + validTo: this.validTo || null, + }) + .subscribe({ + next: (ruleId) => { + this.#api + .post('/marketing/discount-codes', { + discountRuleId: ruleId, + code: this.code, + maxUsages: 1000, + }) + .subscribe({ + next: () => { + this.ruleName = ''; + this.code = ''; + this.value = 20; + this.minOrderAmount = null; + this.validFrom = ''; + this.validTo = ''; + this.load(); + this.#toast.success('common.saved'); + }, + error: () => this.#toast.error('discounts.errorSave'), + }); + }, + error: () => this.#toast.error('discounts.errorSave'), + }); + } + + toggleActive(row: CodeRow): void { + this.#api + .put(`/marketing/discount-codes/${row.id}/active`, { + isActive: !row.isActive, + }) + .subscribe({ + next: () => { + this.load(); + this.#toast.success('common.saved'); + }, + error: () => this.#toast.error('discounts.errorSave'), + }); + } +} + +interface CodeRow { + id: string; + code: string; + ruleName: string; + usageCount: number; + maxUsages: number | null; + isActive: boolean; +} + +@Component({ + selector: 'app-newsletter-admin', + standalone: true, + imports: [ + FormsModule, + TranslatePipe, + AdminListToolbarComponent, + AdminPaginationComponent, + ], + templateUrl: './newsletter-admin.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NewsletterAdminComponent implements OnInit { + readonly #api = inject(ApiService); + readonly #toast = inject(ToastService); + readonly #i18n = inject(I18nService); + readonly subscribers = signal([]); + readonly campaigns = signal([]); + readonly subTotal = signal(0); + readonly campTotal = signal(0); + readonly loadingSubs = signal(false); + readonly loadingCamps = signal(false); + readonly sending = signal(false); + subject = ''; + bodyHtml = ''; + subSearch = ''; + campSearch = ''; + subPage = 1; + campPage = 1; + readonly pageSize = 20; + + ngOnInit(): void { + this.loadSubscribers(); + this.loadCampaigns(); + } + + loadSubscribers(): void { + this.loadingSubs.set(true); + const params = new URLSearchParams({ + page: String(this.subPage), + pageSize: String(this.pageSize), + }); + if (this.subSearch.trim()) { + params.set('search', this.subSearch.trim()); + } + + this.#api + .get>(`/marketing/subscribers?${params}`) + .subscribe({ + next: (res) => { + this.subscribers.set(res.items); + this.subTotal.set(res.totalCount); + this.loadingSubs.set(false); + }, + error: () => { + this.loadingSubs.set(false); + this.#toast.error('newsletter.errorLoad'); + }, + }); + } + + loadCampaigns(): void { + this.loadingCamps.set(true); + const params = new URLSearchParams({ + page: String(this.campPage), + pageSize: String(this.pageSize), + }); + if (this.campSearch.trim()) { + params.set('search', this.campSearch.trim()); + } + + this.#api + .get>(`/marketing/campaigns?${params}`) + .subscribe({ + next: (res) => { + this.campaigns.set(res.items); + this.campTotal.set(res.totalCount); + this.loadingCamps.set(false); + }, + error: () => { + this.loadingCamps.set(false); + this.#toast.error('newsletter.errorLoad'); + }, + }); + } + + handleSubSearch(): void { + this.subPage = 1; + this.loadSubscribers(); + } + + handleCampSearch(): void { + this.campPage = 1; + this.loadCampaigns(); + } + + handleSubPrev(): void { + if (this.subPage > 1) { + this.subPage -= 1; + this.loadSubscribers(); + } + } + + handleSubNext(): void { + const pages = Math.max(1, Math.ceil(this.subTotal() / this.pageSize)); + if (this.subPage < pages) { + this.subPage += 1; + this.loadSubscribers(); + } + } + + handleCampPrev(): void { + if (this.campPage > 1) { + this.campPage -= 1; + this.loadCampaigns(); + } + } + + handleCampNext(): void { + const pages = Math.max(1, Math.ceil(this.campTotal() / this.pageSize)); + if (this.campPage < pages) { + this.campPage += 1; + this.loadCampaigns(); + } + } + + createCampaign(): void { + if (!window.confirm(this.#i18n.t('newsletter.confirmSend'))) { + return; + } + + this.sending.set(true); + this.#api + .post('/marketing/campaigns', { + subject: this.subject, + bodyHtml: this.bodyHtml, + }) + .subscribe({ + next: (id) => { + this.#api + .post<{ sent: number }>(`/marketing/campaigns/${id}/send`, {}) + .subscribe({ + next: (res) => { + this.sending.set(false); + this.subject = ''; + this.bodyHtml = ''; + this.loadCampaigns(); + this.#toast.success('newsletter.sentOk', { + count: res.sent, + }); + }, + error: () => { + this.sending.set(false); + this.#toast.error('newsletter.errorSend'); + }, + }); + }, + error: () => { + this.sending.set(false); + this.#toast.error('newsletter.errorSend'); + }, + }); + } +} + +interface SubRow { + id: string; + email: string; + isActive: boolean; + createdAt: string; +} + +interface CampRow { + id: string; + subject: string; + status: string; + sentAt: string | null; +} + +@Component({ + selector: 'app-reviews-admin', + standalone: true, + imports: [ + FormsModule, + TranslatePipe, + AdminListToolbarComponent, + AdminPaginationComponent, + ], + templateUrl: './reviews-admin.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReviewsAdminComponent implements OnInit { + readonly #api = inject(ApiService); + readonly #toast = inject(ToastService); + readonly reviews = signal([]); + readonly loading = signal(false); + readonly totalCount = signal(0); + search = ''; + status: 'pending' | 'approved' | 'rejected' | 'all' = 'pending'; + page = 1; + readonly pageSize = 20; + + ngOnInit(): void { + this.load(); + } + + load(): void { + this.loading.set(true); + const params = new URLSearchParams({ + page: String(this.page), + pageSize: String(this.pageSize), + status: this.status, + }); + if (this.search.trim()) { + params.set('search', this.search.trim()); + } + + this.#api.get>(`/reviews?${params}`).subscribe({ + next: (res) => { + this.reviews.set(res.items); + this.totalCount.set(res.totalCount); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + this.#toast.error('reviews.errorLoad'); + }, + }); + } + + handleSearch(): void { + this.page = 1; + this.load(); + } + + handleFilterChange(): void { + this.page = 1; + this.load(); + } + + handlePrevPage(): void { + if (this.page > 1) { + this.page -= 1; + this.load(); + } + } + + handleNextPage(): void { + const pages = Math.max(1, Math.ceil(this.totalCount() / this.pageSize)); + if (this.page < pages) { + this.page += 1; + this.load(); + } + } + + approve(id: string, approved: boolean): void { + this.#api + .put(`/reviews/${id}/approve`, { isApproved: approved }) + .subscribe({ + next: () => { + this.load(); + this.#toast.success('common.saved'); + }, + error: () => this.#toast.error('reviews.errorSave'), + }); + } +} + +interface ReviewRow { + id: string; + productName: string; + authorName: string; + rating: number; + content: string; + status: string; +} diff --git a/src/admin/src/app/features/discounts-admin.component.html b/src/admin/src/app/features/discounts-admin.component.html new file mode 100644 index 0000000..60b3555 --- /dev/null +++ b/src/admin/src/app/features/discounts-admin.component.html @@ -0,0 +1,152 @@ +

{{ 'discounts.title' | translate }}

+ +
+ + + + + + + +
+ + +
+ + +
+
+ + +
+ +
+ +
+ @if (loading()) { +

+ {{ 'products.loading' | translate }} +

+ } @else { + + + + + + + + + + + + @for (c of codes(); track c.id) { + + + + + + + + } @empty { + + + + } + +
+ {{ 'discounts.code' | translate }} + + {{ 'discounts.ruleName' | translate }} + + {{ 'discounts.uses' | translate }} + + {{ 'products.active' | translate }} +
{{ c.code }}{{ c.ruleName }} + {{ c.usageCount }} / {{ c.maxUsages ?? '∞' }} + {{ c.isActive ? '✓' : '—' }} + +
+ {{ 'discounts.empty' | translate }} +
+ + } +
diff --git a/src/admin/src/app/features/newsletter-admin.component.html b/src/admin/src/app/features/newsletter-admin.component.html new file mode 100644 index 0000000..7fc629e --- /dev/null +++ b/src/admin/src/app/features/newsletter-admin.component.html @@ -0,0 +1,140 @@ +

{{ 'newsletter.title' | translate }}

+ +

{{ 'newsletter.send' | translate }}

+
+ + + +
+ +

+ {{ 'newsletter.subscribers' | translate }} ({{ subTotal() }}) +

+ +
+ + +
+ +
+
+ @if (loadingSubs()) { +

+ {{ 'products.loading' | translate }} +

+ } @else { +
    + @for (s of subscribers(); track s.id) { +
  • + {{ s.email }} + @if (!s.isActive) { + + ({{ 'products.inactive' | translate }}) + + } +
  • + } @empty { +
  • + {{ 'newsletter.emptySubs' | translate }} +
  • + } +
+ + } +
+ +

+ {{ 'newsletter.campaigns' | translate }} +

+ +
+ + +
+ +
+
+ @if (loadingCamps()) { +

+ {{ 'products.loading' | translate }} +

+ } @else { +
    + @for (c of campaigns(); track c.id) { +
  • + {{ c.subject }} — {{ c.status }} +
  • + } @empty { +
  • + {{ 'newsletter.emptyCamps' | translate }} +
  • + } +
+ + } +
diff --git a/src/admin/src/app/features/orders-chart.component.ts b/src/admin/src/app/features/orders-chart.component.ts new file mode 100644 index 0000000..b89a737 --- /dev/null +++ b/src/admin/src/app/features/orders-chart.component.ts @@ -0,0 +1,60 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; + +export interface ChartPoint { + date: string; + count: number; +} + +@Component({ + selector: 'app-orders-chart', + standalone: true, + imports: [TranslatePipe], + template: ` +
+

+ {{ 'dashboard.chartTitle' | translate }} +

+ +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrdersChartComponent { + readonly data = input([]); + readonly #maxCount = computed(() => + Math.max(1, ...this.data().map((p) => p.count)), + ); + + barHeight(count: number): number { + const max = this.#maxCount(); + return Math.max(8, (count / max) * 100); + } + + formatLabel(date: string): string { + const [, month, day] = date.split('-'); + return `${day}.${month}`; + } +} diff --git a/src/admin/src/app/features/orders/order-detail-modal.component.html b/src/admin/src/app/features/orders/order-detail-modal.component.html new file mode 100644 index 0000000..7e77e04 --- /dev/null +++ b/src/admin/src/app/features/orders/order-detail-modal.component.html @@ -0,0 +1,262 @@ +@if (open()) { + +} diff --git a/src/admin/src/app/features/orders/order-detail-modal.component.ts b/src/admin/src/app/features/orders/order-detail-modal.component.ts new file mode 100644 index 0000000..feea3e9 --- /dev/null +++ b/src/admin/src/app/features/orders/order-detail-modal.component.ts @@ -0,0 +1,312 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + computed, + effect, + inject, + input, + output, + signal, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ApiService } from '../../core/api.service'; +import { API_BASE_URL } from '../../core/constants'; +import { + AdminOrderDetail, + AdminOrderDetailLine, + ORDER_STATUSES, + OrderStatus, + ShipmentInfo, +} from './order.models'; +import { + applyOrderWarehouse, + buildOrderCollectPayload, + canCollectOrder, + canCollectPickLine, + canUseWarehouseForOrder, + defaultWarehouseForLine, + orderWarehouseOptions, +} from './order-pick.utils'; + +@Component({ + selector: 'app-order-detail-modal', + standalone: true, + imports: [FormsModule, TranslatePipe, I18nCurrencyPipe], + templateUrl: './order-detail-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrderDetailModalComponent { + readonly #api = inject(ApiService); + readonly #cdr = inject(ChangeDetectorRef); + + readonly open = input(false); + readonly orderId = input(null); + + readonly closed = output(); + readonly saved = output(); + readonly invoiceRequested = output(); + + readonly loading = signal(false); + readonly collecting = signal(false); + readonly generatingLabel = signal(false); + readonly detail = signal(null); + selectedStatus: OrderStatus = 'pendingPayment'; + + readonly selectedWarehouse = new Map(); + orderWarehouse = ''; + + readonly statuses = ORDER_STATUSES; + + readonly canPick = computed(() => { + const d = this.detail(); + return d?.status === 'paid' || d?.status === 'processing'; + }); + + constructor() { + effect(() => { + const id = this.orderId(); + if (!this.open() || !id) { + return; + } + + this.load(id); + }); + } + + handleClose(): void { + this.closed.emit(); + } + + handleBackdrop(event: MouseEvent): void { + if (event.target === event.currentTarget) { + this.handleClose(); + } + } + + handleSaveStatus(): void { + const id = this.orderId(); + if (!id) { + return; + } + + this.#api + .put(`/orders/${id}/status`, { status: this.selectedStatus }) + .subscribe({ + next: () => { + this.saved.emit(); + if (id) { + this.load(id); + } + }, + }); + } + + handleInvoice(): void { + const id = this.orderId(); + if (id) { + this.invoiceRequested.emit(id); + } + } + + canGenerateLabel(detail: AdminOrderDetail): boolean { + return ( + (detail.status === 'paid' || detail.status === 'processing') && + !detail.shipment?.labelRelativePath + ); + } + + labelUrl(shipment: ShipmentInfo): string { + if (!shipment.labelRelativePath) { + return ''; + } + + return `${API_BASE_URL}${shipment.labelRelativePath}`; + } + + handleGenerateLabel(): void { + const id = this.orderId(); + if (!id) { + return; + } + + this.generatingLabel.set(true); + this.#api.post(`/shipping/orders/${id}/label`, {}).subscribe({ + next: () => { + this.generatingLabel.set(false); + this.saved.emit(); + this.load(id); + }, + error: () => this.generatingLabel.set(false), + }); + } + + pickLines(detail: AdminOrderDetail): AdminOrderDetailLine[] { + return detail.lines; + } + + pendingCount(detail: AdminOrderDetail): number { + return detail.lines.filter((line) => !line.isCollected).length; + } + + warehouseOptions(detail: AdminOrderDetail) { + return orderWarehouseOptions({ + orderId: detail.id, + orderNumber: detail.orderNumber, + lines: detail.lines.map((line) => ({ + orderLineId: line.id, + orderId: detail.id, + orderNumber: detail.orderNumber, + productVariantId: line.productVariantId, + productName: line.productName, + variantColor: line.variantColor, + sku: line.sku, + quantity: line.quantity, + isCollected: line.isCollected, + pickedFromWarehouseId: line.pickedFromWarehouseId, + pickedFromWarehouseName: line.pickedFromWarehouseName, + pickedFromWarehouseCode: line.pickedFromWarehouseCode, + collectedAt: line.collectedAt, + warehouseStock: line.warehouseStock, + })), + pendingCount: 0, + collectedCount: 0, + }); + } + + getWarehouse(lineId: string): string { + return this.selectedWarehouse.get(lineId) ?? ''; + } + + setWarehouse(lineId: string, warehouseId: string): void { + this.selectedWarehouse.set(lineId, warehouseId); + this.#cdr.markForCheck(); + } + + setOrderWarehouse(warehouseId: string): void { + this.orderWarehouse = warehouseId; + const detail = this.detail(); + if (!detail) { + return; + } + + const group = { + orderId: detail.id, + orderNumber: detail.orderNumber, + lines: detail.lines.map((line) => this.#toPickLine(detail, line)), + pendingCount: 0, + collectedCount: 0, + }; + applyOrderWarehouse(group, warehouseId, this.selectedWarehouse); + this.#cdr.markForCheck(); + } + + canCollectLine(line: AdminOrderDetailLine): boolean { + return canCollectPickLine(this.#toPickLine(this.detail()!, line), this.getWarehouse(line.id)); + } + + canCollectAll(detail: AdminOrderDetail): boolean { + const lines = detail.lines.map((line) => this.#toPickLine(detail, line)); + return canCollectOrder(lines, this.selectedWarehouse); + } + + handleCollectLine(line: AdminOrderDetailLine): void { + const id = this.orderId(); + const warehouseId = this.getWarehouse(line.id); + if (!id || !warehouseId || !this.canCollectLine(line)) { + return; + } + + this.collecting.set(true); + this.#api + .post(`/orders/admin/lines/${line.id}/collect`, { warehouseId }) + .subscribe({ + next: () => { + this.collecting.set(false); + this.saved.emit(); + this.load(id); + }, + error: () => this.collecting.set(false), + }); + } + + handleCollectOrder(): void { + const detail = this.detail(); + const id = this.orderId(); + if (!detail || !id || !this.canCollectAll(detail)) { + return; + } + + const lines = buildOrderCollectPayload( + detail.lines.map((line) => this.#toPickLine(detail, line)), + this.selectedWarehouse, + ); + + this.collecting.set(true); + this.#api.post(`/orders/admin/${id}/collect`, { lines }).subscribe({ + next: () => { + this.collecting.set(false); + this.saved.emit(); + this.load(id); + }, + error: () => this.collecting.set(false), + }); + } + + private load(id: string): void { + this.loading.set(true); + this.#api.get(`/orders/admin/${id}`).subscribe({ + next: (d) => { + this.detail.set(d); + this.selectedStatus = d.status; + this.selectedWarehouse.clear(); + for (const line of d.lines) { + if (!line.isCollected) { + const first = defaultWarehouseForLine(this.#toPickLine(d, line)); + if (first) { + this.selectedWarehouse.set(line.id, first.warehouseId); + } + } + } + const group = { + orderId: d.id, + orderNumber: d.orderNumber, + lines: d.lines.map((line) => this.#toPickLine(d, line)), + pendingCount: 0, + collectedCount: 0, + }; + const option = this.warehouseOptions(d).find((ws) => + canUseWarehouseForOrder(group, ws.warehouseId), + ); + this.orderWarehouse = option?.warehouseId ?? ''; + if (this.orderWarehouse) { + applyOrderWarehouse(group, this.orderWarehouse, this.selectedWarehouse); + } + this.loading.set(false); + this.#cdr.markForCheck(); + }, + error: () => this.loading.set(false), + }); + } + + #toPickLine(detail: AdminOrderDetail, line: AdminOrderDetailLine) { + return { + orderLineId: line.id, + orderId: detail.id, + orderNumber: detail.orderNumber, + productVariantId: line.productVariantId, + productName: line.productName, + variantColor: line.variantColor, + sku: line.sku, + quantity: line.quantity, + isCollected: line.isCollected, + pickedFromWarehouseId: line.pickedFromWarehouseId, + pickedFromWarehouseName: line.pickedFromWarehouseName, + pickedFromWarehouseCode: line.pickedFromWarehouseCode, + collectedAt: line.collectedAt, + warehouseStock: line.warehouseStock, + }; + } +} diff --git a/src/admin/src/app/features/orders/order-invoice-modal.component.html b/src/admin/src/app/features/orders/order-invoice-modal.component.html new file mode 100644 index 0000000..4bbd46e --- /dev/null +++ b/src/admin/src/app/features/orders/order-invoice-modal.component.html @@ -0,0 +1,106 @@ +@if (open()) { + +} diff --git a/src/admin/src/app/features/orders/order-invoice-modal.component.ts b/src/admin/src/app/features/orders/order-invoice-modal.component.ts new file mode 100644 index 0000000..1bba8de --- /dev/null +++ b/src/admin/src/app/features/orders/order-invoice-modal.component.ts @@ -0,0 +1,65 @@ +import { DatePipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + input, + output, + signal, +} from '@angular/core'; +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ApiService } from '../../core/api.service'; +import { OrderInvoice } from './order.models'; + +@Component({ + selector: 'app-order-invoice-modal', + standalone: true, + imports: [DatePipe, TranslatePipe, I18nCurrencyPipe], + templateUrl: './order-invoice-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrderInvoiceModalComponent { + readonly #api = inject(ApiService); + + readonly open = input(false); + readonly orderId = input(null); + + readonly closed = output(); + + readonly loading = signal(false); + readonly invoice = signal(null); + + constructor() { + effect(() => { + const id = this.orderId(); + if (!this.open() || !id) { + return; + } + + this.loading.set(true); + this.#api.get(`/orders/admin/${id}/invoice`).subscribe({ + next: (inv) => { + this.invoice.set(inv); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + }); + } + + handleClose(): void { + this.closed.emit(); + } + + handleBackdrop(event: MouseEvent): void { + if (event.target === event.currentTarget) { + this.handleClose(); + } + } + + handlePrint(): void { + window.print(); + } +} diff --git a/src/admin/src/app/features/orders/order-pick.utils.ts b/src/admin/src/app/features/orders/order-pick.utils.ts new file mode 100644 index 0000000..861f6cc --- /dev/null +++ b/src/admin/src/app/features/orders/order-pick.utils.ts @@ -0,0 +1,128 @@ +import { PickListLine, PickListOrderGroup, PickListWarehouseQty } from './order.models'; + +export interface CollectLinePayload { + orderLineId: string; + warehouseId: string; +} + +export function groupPickListByOrder( + items: PickListLine[], +): PickListOrderGroup[] { + const groups = new Map(); + + for (const line of items) { + const existing = groups.get(line.orderId); + if (existing) { + existing.lines.push(line); + if (line.isCollected) { + existing.collectedCount += 1; + } else { + existing.pendingCount += 1; + } + continue; + } + + groups.set(line.orderId, { + orderId: line.orderId, + orderNumber: line.orderNumber, + lines: [line], + pendingCount: line.isCollected ? 0 : 1, + collectedCount: line.isCollected ? 1 : 0, + }); + } + + return [...groups.values()].sort((a, b) => + a.orderNumber.localeCompare(b.orderNumber), + ); +} + +export function canCollectPickLine( + line: PickListLine, + warehouseId: string, +): boolean { + if (!warehouseId || line.isCollected) { + return false; + } + + const stock = line.warehouseStock.find( + (ws) => ws.warehouseId === warehouseId, + ); + return !!stock && stock.quantity >= line.quantity; +} + +export function buildOrderCollectPayload( + lines: PickListLine[], + warehouses: Map, +): CollectLinePayload[] { + return lines + .filter((line) => !line.isCollected) + .map((line) => ({ + orderLineId: line.orderLineId, + warehouseId: warehouses.get(line.orderLineId) ?? '', + })) + .filter((entry) => !!entry.warehouseId); +} + +export function canCollectOrder( + lines: PickListLine[], + warehouses: Map, +): boolean { + const pending = lines.filter((line) => !line.isCollected); + if (pending.length === 0) { + return false; + } + + return pending.every((line) => + canCollectPickLine(line, warehouses.get(line.orderLineId) ?? ''), + ); +} + +export function defaultWarehouseForLine( + line: PickListLine, +): PickListWarehouseQty | undefined { + return line.warehouseStock.find((ws) => ws.quantity >= line.quantity); +} + +export function applyOrderWarehouse( + group: PickListOrderGroup, + warehouseId: string, + warehouses: Map, +): void { + for (const line of group.lines) { + if (!line.isCollected) { + warehouses.set(line.orderLineId, warehouseId); + } + } +} + +export function orderWarehouseOptions( + group: PickListOrderGroup, +): PickListWarehouseQty[] { + const map = new Map(); + for (const line of group.lines) { + for (const ws of line.warehouseStock) { + map.set(ws.warehouseId, ws); + } + } + + return [...map.values()].sort((a, b) => + a.warehouseName.localeCompare(b.warehouseName), + ); +} + +export function canUseWarehouseForOrder( + group: PickListOrderGroup, + warehouseId: string, +): boolean { + const pending = group.lines.filter((line) => !line.isCollected); + if (pending.length === 0) { + return false; + } + + return pending.every((line) => { + const stock = line.warehouseStock.find( + (ws) => ws.warehouseId === warehouseId, + ); + return !!stock && stock.quantity >= line.quantity; + }); +} diff --git a/src/admin/src/app/features/orders/order.models.ts b/src/admin/src/app/features/orders/order.models.ts new file mode 100644 index 0000000..aa6ac6e --- /dev/null +++ b/src/admin/src/app/features/orders/order.models.ts @@ -0,0 +1,182 @@ +export type OrderStatus = + | 'pendingPayment' + | 'paid' + | 'processing' + | 'shipped' + | 'delivered' + | 'cancelled' + | 'refunded'; + +export type PaymentStatus = 'pending' | 'completed' | 'failed' | 'refunded'; + +export interface AdminOrder { + id: string; + orderNumber: string; + customerEmail: string; + customerFirstName: string; + customerLastName: string; + status: OrderStatus; + paymentStatus?: PaymentStatus | null; + paymentProvider?: string | null; + total: number; + createdAt: string; + itemCount: number; +} + +export interface AdminOrdersResult { + items: AdminOrder[]; + totalCount: number; + page: number; + pageSize: number; +} + +export interface AdminOrderLine { + productName: string; + variantColor: string; + sku: string; + unitPrice: number; + quantity: number; + lineTotal: number; +} + +export interface AdminOrderDetailLine extends AdminOrderLine { + id: string; + productVariantId: string; + isCollected: boolean; + pickedFromWarehouseId?: string | null; + pickedFromWarehouseName?: string | null; + pickedFromWarehouseCode?: string | null; + collectedAt?: string | null; + warehouseStock: PickListWarehouseQty[]; +} + +export type ShippingCarrier = 'inPost' | 'dhl' | 'dpd'; +export type ShippingServiceType = 'parcelLocker' | 'courier'; +export type ShipmentStatus = + | 'pending' + | 'labelGenerated' + | 'dispatched' + | 'delivered' + | 'failed'; + +export interface ShipmentInfo { + id: string; + orderId: string; + carrier: ShippingCarrier; + serviceType: ShippingServiceType; + status: ShipmentStatus; + trackingNumber?: string | null; + externalShipmentId?: string | null; + labelRelativePath?: string | null; + parcelPointName?: string | null; + parcelPointAddress?: string | null; + errorMessage?: string | null; +} + +export interface AdminOrderDetail { + id: string; + orderNumber: string; + status: OrderStatus; + paymentStatus?: PaymentStatus | null; + paymentProvider?: string | null; + paymentExternalId?: string | null; + customerEmail: string; + customerPhone: string; + customerFirstName: string; + customerLastName: string; + shippingCarrier: ShippingCarrier; + shippingServiceType: ShippingServiceType; + parcelPointId?: string | null; + parcelPointName?: string | null; + parcelPointAddress?: string | null; + shippingStreet: string; + shippingCity: string; + shippingPostalCode: string; + shippingCountry: string; + paymentMethod: string; + customerNotes?: string | null; + subtotal: number; + deliveryCost: number; + discountAmount: number; + vatAmount: number; + total: number; + createdAt: string; + shipment?: ShipmentInfo | null; + lines: AdminOrderDetailLine[]; +} + +export interface PickListWarehouseQty { + warehouseId: string; + warehouseName: string; + warehouseCode: string; + quantity: number; +} + +export interface PickListLine { + orderLineId: string; + orderId: string; + orderNumber: string; + productVariantId: string; + productName: string; + variantColor: string; + sku: string; + quantity: number; + isCollected: boolean; + pickedFromWarehouseId?: string | null; + pickedFromWarehouseName?: string | null; + pickedFromWarehouseCode?: string | null; + collectedAt?: string | null; + warehouseStock: PickListWarehouseQty[]; +} + +export interface PickListResult { + items: PickListLine[]; + orderCount: number; + pendingCount: number; + collectedCount: number; +} + +export interface PickListOrderGroup { + orderId: string; + orderNumber: string; + lines: PickListLine[]; + pendingCount: number; + collectedCount: number; +} + +export interface OrderInvoice { + invoiceNumber: string; + issueDate: string; + sellerName: string; + sellerAddress: string; + sellerTaxId: string; + buyerName: string; + buyerEmail: string; + buyerAddress: string; + orderNumber: string; + lines: AdminOrderLine[]; + subtotal: number; + deliveryCost: number; + discountAmount: number; + vatAmount: number; + total: number; + paymentStatus?: PaymentStatus | null; + orderStatus: OrderStatus; +} + +export const ORDER_STATUSES: OrderStatus[] = [ + 'pendingPayment', + 'paid', + 'processing', + 'shipped', + 'delivered', + 'cancelled', + 'refunded', +]; + +export const PAYMENT_STATUSES: PaymentStatus[] = [ + 'pending', + 'completed', + 'failed', + 'refunded', +]; diff --git a/src/admin/src/app/features/orders/orders-admin.component.html b/src/admin/src/app/features/orders/orders-admin.component.html new file mode 100644 index 0000000..039ce41 --- /dev/null +++ b/src/admin/src/app/features/orders/orders-admin.component.html @@ -0,0 +1,358 @@ +

{{ 'orders.title' | translate }}

+ +
+ + +
+ +@if (activeTab() === 'list') { + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ @if (loading()) { +

+ {{ 'products.loading' | translate }} +

+ } @else { + + + + + + + + + + + + + + @for (o of orders(); track o.id) { + + + + + + + + + + } @empty { + + + + } + +
+ #{{ sortIndicator('number') }} + + {{ 'orders.date' | translate }}{{ sortIndicator('date') }} + {{ 'orders.customer' | translate }}{{ 'orders.status' | translate }}{{ 'orders.payment' | translate }}{{ 'orders.items' | translate }} + {{ 'products.price' | translate }}{{ sortIndicator('total') }} +
{{ o.orderNumber }} + {{ o.createdAt | date: 'yyyy-MM-dd HH:mm' }} + +
{{ o.customerFirstName }} {{ o.customerLastName }}
+
{{ o.customerEmail }}
+
+ + {{ 'orders.status.' + o.status | translate }} + + + @if (o.paymentStatus) { + + {{ 'orders.paymentStatus.' + o.paymentStatus | translate }} + + @if (o.paymentProvider) { +
+ {{ o.paymentProvider }} +
+ } + } @else { + — + } +
{{ o.itemCount }}{{ o.total | i18nCurrency }}
+ {{ 'orders.empty' | translate }} +
+ + + } +
+} @else { +
+ @if (pickListLoading()) { +

{{ 'products.loading' | translate }}

+ } @else if (pickList(); as pl) { +
+

+ {{ 'orders.pickListSummary' | translate: { count: pl.orderCount } }} + · {{ 'orders.pendingItems' | translate }}: {{ pl.pendingCount }} + · {{ 'orders.collectedItems' | translate }}: {{ pl.collectedCount }} +

+
+ + +
+
+ + @for (group of pickListGroups(pl.items); track group.orderId) { +
+
+
+

#{{ group.orderNumber }}

+

+ {{ 'orders.pendingItems' | translate }}: {{ group.pendingCount }} + · {{ 'orders.collectedItems' | translate }}: {{ group.collectedCount }} +

+
+ @if (group.pendingCount > 0) { +
+
+ + +
+ +
+ } @else { + + {{ 'orders.orderCollected' | translate }} + + } +
+ + + + + + + + + + + + + + @for (line of group.lines; track line.orderLineId) { + + + + + + + + + } + +
{{ 'products.name' | translate }}SKU{{ 'orders.qty' | translate }}{{ 'orders.warehouseStock' | translate }}{{ 'orders.pickFrom' | translate }}{{ 'orders.status' | translate }}
+
{{ line.productName }}
+
{{ line.variantColor }}
+
{{ line.sku }}{{ line.quantity }} + @for (ws of line.warehouseStock; track ws.warehouseId) { +
+ {{ ws.warehouseName }} ({{ ws.warehouseCode }}): + {{ ws.quantity }} +
+ } @empty { + — + } +
+ @if (!line.isCollected) { + + } @else if (line.pickedFromWarehouseName) { + + {{ line.pickedFromWarehouseName }} + ({{ line.pickedFromWarehouseCode }}) + + } + + @if (line.isCollected) { + + {{ 'orders.collected' | translate }} + + @if (line.collectedAt) { +
+ {{ line.collectedAt | date: 'yyyy-MM-dd HH:mm' }} +
+ } + } @else { + + } +
+
+ } @empty { +

+ {{ 'orders.pickListEmpty' | translate }} +

+ } + } +
+} + + + + diff --git a/src/admin/src/app/features/orders/orders-admin.component.ts b/src/admin/src/app/features/orders/orders-admin.component.ts new file mode 100644 index 0000000..4561abd --- /dev/null +++ b/src/admin/src/app/features/orders/orders-admin.component.ts @@ -0,0 +1,381 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, + OnInit, + signal, +} from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ApiService } from '../../core/api.service'; +import { OrderDetailModalComponent } from './order-detail-modal.component'; +import { OrderInvoiceModalComponent } from './order-invoice-modal.component'; +import { + AdminOrder, + AdminOrdersResult, + ORDER_STATUSES, + OrderStatus, + PAYMENT_STATUSES, + PaymentStatus, + PickListResult, + PickListLine, + PickListOrderGroup, +} from './order.models'; +import { + applyOrderWarehouse, + buildOrderCollectPayload, + canCollectOrder, + canCollectPickLine, + canUseWarehouseForOrder, + defaultWarehouseForLine, + groupPickListByOrder, + orderWarehouseOptions, +} from './order-pick.utils'; +import { AdminListToolbarComponent } from '../../shared/admin-list-toolbar.component'; +import { AdminPaginationComponent } from '../../shared/admin-pagination.component'; + +type OrdersTab = 'list' | 'pickList'; + +type SortBy = 'date' | 'total' | 'number'; +type SortDir = 'asc' | 'desc'; + +@Component({ + selector: 'app-orders-admin', + standalone: true, + imports: [ + FormsModule, + DatePipe, + TranslatePipe, + I18nCurrencyPipe, + AdminListToolbarComponent, + AdminPaginationComponent, + OrderDetailModalComponent, + OrderInvoiceModalComponent, + ], + templateUrl: './orders-admin.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrdersAdminComponent implements OnInit { + readonly #api = inject(ApiService); + readonly #cdr = inject(ChangeDetectorRef); + readonly #route = inject(ActivatedRoute); + + readonly activeTab = signal('list'); + readonly orders = signal([]); + readonly totalCount = signal(0); + readonly loading = signal(false); + + readonly pickList = signal(null); + readonly pickListLoading = signal(false); + readonly showCollected = signal(false); + readonly collectingLineId = signal(null); + readonly collectingOrderId = signal(null); + + readonly selectedWarehouse = new Map(); + readonly selectedOrderWarehouse = new Map(); + + readonly detailOpen = signal(false); + readonly detailOrderId = signal(null); + + readonly invoiceOpen = signal(false); + readonly invoiceOrderId = signal(null); + + readonly orderStatuses = ORDER_STATUSES; + readonly paymentStatuses = PAYMENT_STATUSES; + + search = ''; + filterStatus: OrderStatus | '' = ''; + filterPayment: PaymentStatus | '' = ''; + filterMonth = ''; + sortBy: SortBy = 'date'; + sortDir: SortDir = 'desc'; + page = 1; + readonly pageSize = 20; + + ngOnInit(): void { + const status = this.#route.snapshot.queryParamMap.get('status'); + if (status && (ORDER_STATUSES as readonly string[]).includes(status)) { + this.filterStatus = status as OrderStatus; + } + + this.loadOrders(); + } + + setTab(tab: OrdersTab): void { + this.activeTab.set(tab); + if (tab === 'pickList' && !this.pickList()) { + this.loadPickList(); + } + } + + loadOrders(): void { + this.loading.set(true); + const params = new URLSearchParams({ + page: String(this.page), + pageSize: String(this.pageSize), + sortBy: this.sortBy, + sortDir: this.sortDir, + }); + + if (this.search.trim()) { + params.set('search', this.search.trim()); + } + + if (this.filterStatus) { + params.set('status', this.filterStatus); + } + + if (this.filterPayment) { + params.set('paymentStatus', this.filterPayment); + } + + if (this.filterMonth) { + params.set('month', this.filterMonth); + } + + this.#api + .get(`/orders/admin?${params.toString()}`) + .subscribe({ + next: (res) => { + this.orders.set(res.items); + this.totalCount.set(res.totalCount); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } + + loadPickList(): void { + this.pickListLoading.set(true); + const params = this.showCollected() + ? '?includeCollected=true' + : ''; + this.#api.get(`/orders/admin/pick-list${params}`).subscribe({ + next: (res) => { + this.pickList.set(res); + this.pickListLoading.set(false); + for (const item of res.items) { + if (!item.isCollected && !this.selectedWarehouse.has(item.orderLineId)) { + const first = defaultWarehouseForLine(item); + if (first) { + this.selectedWarehouse.set(item.orderLineId, first.warehouseId); + } + } + } + for (const group of groupPickListByOrder(res.items)) { + if (!this.selectedOrderWarehouse.has(group.orderId)) { + const option = orderWarehouseOptions(group).find((ws) => + canUseWarehouseForOrder(group, ws.warehouseId), + ); + if (option) { + this.selectedOrderWarehouse.set(group.orderId, option.warehouseId); + } + } + } + this.#cdr.markForCheck(); + }, + error: () => this.pickListLoading.set(false), + }); + } + + toggleShowCollected(): void { + this.showCollected.update((v) => !v); + this.loadPickList(); + } + + setWarehouse(lineId: string, warehouseId: string): void { + this.selectedWarehouse.set(lineId, warehouseId); + this.#cdr.markForCheck(); + } + + getWarehouse(lineId: string): string { + return this.selectedWarehouse.get(lineId) ?? ''; + } + + canCollect(line: PickListLine): boolean { + return canCollectPickLine(line, this.getWarehouse(line.orderLineId)); + } + + pickListGroups(items: PickListLine[]): PickListOrderGroup[] { + return groupPickListByOrder(items); + } + + getOrderWarehouse(orderId: string): string { + return this.selectedOrderWarehouse.get(orderId) ?? ''; + } + + setOrderWarehouse(orderId: string, warehouseId: string): void { + this.selectedOrderWarehouse.set(orderId, warehouseId); + const group = this.pickList()?.items + ? groupPickListByOrder(this.pickList()!.items).find( + (g) => g.orderId === orderId, + ) + : undefined; + if (group) { + applyOrderWarehouse(group, warehouseId, this.selectedWarehouse); + } + this.#cdr.markForCheck(); + } + + orderWarehouses(group: PickListOrderGroup) { + return orderWarehouseOptions(group); + } + + canCollectOrderGroup(group: PickListOrderGroup): boolean { + return canCollectOrder(group.lines, this.selectedWarehouse); + } + + handleCollectOrder(group: PickListOrderGroup): void { + if (!this.canCollectOrderGroup(group)) { + return; + } + + const lines = buildOrderCollectPayload( + group.lines, + this.selectedWarehouse, + ); + this.collectingOrderId.set(group.orderId); + this.#api + .post(`/orders/admin/${group.orderId}/collect`, { lines }) + .subscribe({ + next: () => { + this.collectingOrderId.set(null); + this.loadPickList(); + this.loadOrders(); + }, + error: () => this.collectingOrderId.set(null), + }); + } + + handleCollect(line: PickListLine): void { + const warehouseId = this.getWarehouse(line.orderLineId); + if (!warehouseId || !this.canCollect(line)) { + return; + } + + this.collectingLineId.set(line.orderLineId); + this.#api + .post(`/orders/admin/lines/${line.orderLineId}/collect`, { + warehouseId, + }) + .subscribe({ + next: () => { + this.collectingLineId.set(null); + this.loadPickList(); + this.loadOrders(); + }, + error: () => this.collectingLineId.set(null), + }); + } + + handleSearch(): void { + this.page = 1; + this.loadOrders(); + } + + handleFilterChange(): void { + this.page = 1; + this.loadOrders(); + } + + handleSort(column: SortBy): void { + if (this.sortBy === column) { + this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'; + } else { + this.sortBy = column; + this.sortDir = 'desc'; + } + + this.loadOrders(); + } + + sortIndicator(column: SortBy): string { + if (this.sortBy !== column) { + return ''; + } + + return this.sortDir === 'asc' ? ' ↑' : ' ↓'; + } + + totalPages(): number { + return Math.max(1, Math.ceil(this.totalCount() / this.pageSize)); + } + + handlePrevPage(): void { + if (this.page > 1) { + this.page -= 1; + this.loadOrders(); + } + } + + handleNextPage(): void { + if (this.page < this.totalPages()) { + this.page += 1; + this.loadOrders(); + } + } + + openDetail(order: AdminOrder): void { + this.detailOrderId.set(order.id); + this.detailOpen.set(true); + } + + closeDetail(): void { + this.detailOpen.set(false); + this.detailOrderId.set(null); + } + + handleDetailSaved(): void { + this.loadOrders(); + if (this.activeTab() === 'pickList') { + this.loadPickList(); + } + } + + openInvoice(orderId: string): void { + this.invoiceOrderId.set(orderId); + this.invoiceOpen.set(true); + } + + closeInvoice(): void { + this.invoiceOpen.set(false); + this.invoiceOrderId.set(null); + } + + paymentBadgeClass(status?: PaymentStatus | null): string { + switch (status) { + case 'completed': + return 'bg-green-100 text-green-800'; + case 'pending': + return 'bg-yellow-100 text-yellow-800'; + case 'failed': + return 'bg-red-100 text-red-800'; + case 'refunded': + return 'bg-gray-100 text-gray-700'; + default: + return 'bg-iled-bg text-iled-grey-dark'; + } + } + + statusBadgeClass(status: OrderStatus): string { + switch (status) { + case 'paid': + case 'delivered': + return 'bg-green-100 text-green-800'; + case 'processing': + case 'shipped': + return 'bg-blue-100 text-blue-800'; + case 'pendingPayment': + return 'bg-yellow-100 text-yellow-800'; + case 'cancelled': + case 'refunded': + return 'bg-red-100 text-red-800'; + default: + return 'bg-iled-bg text-iled-grey-dark'; + } + } +} diff --git a/src/admin/src/app/features/products-admin.component.html b/src/admin/src/app/features/products-admin.component.html new file mode 100644 index 0000000..c85d200 --- /dev/null +++ b/src/admin/src/app/features/products-admin.component.html @@ -0,0 +1,415 @@ +

{{ 'products.title' | translate }}

+ + + +@if (activeTab() === 'products') { +
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ @if (loading()) { +

+ {{ 'products.loading' | translate }} +

+ } @else if (error()) { +

{{ error() | translate }}

+ } @else { + + + + + + + + + @for (w of warehouses(); track w.id) { + + } + + + + + + @for (p of products(); track p.id) { + + + + + + + @for (w of warehouses(); track w.id) { + + } + + + + } @empty { + + + + } + +
+ {{ 'products.image' | translate }} + + {{ 'products.name' | translate }}{{ sortIndicator('name') }} + + {{ 'products.model' | translate }} + + {{ 'products.group' | translate }} + + {{ 'products.price' | translate }}{{ sortIndicator('price') }} + {{ w.name }} + {{ 'products.totalStock' | translate + }}{{ sortIndicator('stock') }} +
+
+ @if (p.primaryImageUrl) { + + } @else { +
+ — +
+ } +
+
+
{{ p.name }}
+
{{ p.slug }}
+ @if (!p.isActive) { + + {{ 'products.inactive' | translate }} + + } +
{{ p.model || '—' }} + @if (groupNames(p.productGroupId); as names) { + {{ names | localizedText }} + } @else { + — + } + {{ p.basePrice | i18nCurrency }}{{ warehouseQty(p, w.id) }} + {{ p.totalStock }} + @if (p.reservedStock > 0) { + (-{{ p.reservedStock }}) + } + + +
+ {{ 'products.empty' | translate }} +
+ + + } +
+} + +@if (activeTab() === 'warehouses') { +
+ + + +
+ + @if (warehouseError()) { +

{{ warehouseError() | translate }}

+ } + +
+ + + + + + + + + + + @for (w of warehouses(); track w.id) { + + @if (editingWarehouseId() === w.id) { + + + + + } @else { + + + + + } + + } + +
+ {{ 'warehouses.name' | translate }} + + {{ 'warehouses.code' | translate }} + + {{ 'warehouses.sortOrder' | translate }} +
+ + {{ w.code }} + + + + + {{ w.name }}{{ w.code }}{{ w.sortOrder }} + + +
+
+} + +@if (activeTab() === 'groups') { +
+ +
+ +
+ + + + + + + + + + + @for (g of groups(); track g.id) { + + + + + + + } + +
{{ 'groups.name' | translate }}{{ 'groups.code' | translate }} + {{ 'groups.sortOrder' | translate }} +
{{ g.names | localizedText }}{{ g.code }}{{ g.sortOrder }} + +
+
+} + + + + diff --git a/src/admin/src/app/features/products-admin.component.ts b/src/admin/src/app/features/products-admin.component.ts new file mode 100644 index 0000000..efae0ad --- /dev/null +++ b/src/admin/src/app/features/products-admin.component.ts @@ -0,0 +1,369 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, + signal, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { I18nCurrencyPipe } from '../core/i18n/i18n-currency.pipe'; +import { LocalizedTextPipe } from '../core/i18n/localized-text.pipe'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; +import { ApiService } from '../core/api.service'; +import { resolveProductImageUrl } from '../core/product-image.util'; +import { AdminListToolbarComponent } from '../shared/admin-list-toolbar.component'; +import { AdminPaginationComponent } from '../shared/admin-pagination.component'; +import { ToastService } from '../shared/toast.service'; +import { GroupFormModalComponent } from './products/group-form-modal.component'; +import { ProductFormModalComponent } from './products/product-form-modal.component'; +import { + AdminProduct, + CategoryDto, + ProductGroupDto, + WarehouseDto, +} from './products/product.models'; + +type ProductsTab = 'products' | 'warehouses' | 'groups'; +type SortBy = 'name' | 'price' | 'stock'; +type SortDir = 'asc' | 'desc'; + +interface PaginatedProducts { + items: AdminProduct[]; + page: number; + pageSize: number; + totalCount: number; +} + +@Component({ + selector: 'app-products-admin', + standalone: true, + imports: [ + FormsModule, + TranslatePipe, + I18nCurrencyPipe, + LocalizedTextPipe, + AdminListToolbarComponent, + AdminPaginationComponent, + ProductFormModalComponent, + GroupFormModalComponent, + ], + templateUrl: './products-admin.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductsAdminComponent implements OnInit { + readonly #api = inject(ApiService); + readonly #route = inject(ActivatedRoute); + readonly #router = inject(Router); + readonly #toast = inject(ToastService); + + readonly activeTab = signal('products'); + readonly products = signal([]); + readonly warehouses = signal([]); + readonly groups = signal([]); + readonly categories = signal([]); + readonly loading = signal(false); + readonly error = signal(''); + readonly totalCount = signal(0); + + readonly modalOpen = signal(false); + readonly modalMode = signal<'create' | 'edit'>('create'); + readonly editingProductId = signal(null); + + readonly groupModalOpen = signal(false); + readonly groupModalMode = signal<'create' | 'edit'>('create'); + readonly editingGroup = signal(null); + + newWarehouseName = ''; + newWarehouseCode = ''; + warehouseError = signal(''); + editingWarehouseId = signal(null); + editWarehouseName = ''; + editWarehouseSort = 0; + + search = ''; + filterActive: '' | 'true' | 'false' = ''; + filterGroupId = ''; + filterLowStock = false; + sortBy: SortBy = 'name'; + sortDir: SortDir = 'asc'; + page = 1; + readonly pageSize = 20; + + ngOnInit(): void { + this.loadWarehouses(); + this.loadGroups(); + this.#api.get('/categories').subscribe({ + next: (cats) => this.categories.set(cats), + }); + + this.filterLowStock = + this.#route.snapshot.queryParamMap.get('lowStock') === '1'; + this.loadProducts(); + } + + setTab(tab: ProductsTab): void { + this.activeTab.set(tab); + } + + loadProducts(): void { + this.loading.set(true); + this.error.set(''); + const params = new URLSearchParams({ + page: String(this.page), + pageSize: String(this.pageSize), + sortBy: this.sortBy, + sortDir: this.sortDir, + }); + + if (this.search.trim()) { + params.set('search', this.search.trim()); + } + + if (this.filterActive) { + params.set('isActive', this.filterActive); + } + + if (this.filterGroupId) { + params.set('groupId', this.filterGroupId); + } + + if (this.filterLowStock) { + params.set('lowStock', 'true'); + } + + this.#api + .get(`/admin/products?${params.toString()}`) + .subscribe({ + next: (res) => { + this.products.set(res.items); + this.totalCount.set(res.totalCount); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + this.error.set('products.errorList'); + this.#toast.error('products.errorList'); + }, + }); + } + + loadWarehouses(): void { + this.#api.get('/admin/warehouses').subscribe({ + next: (w) => this.warehouses.set(w), + }); + } + + loadGroups(): void { + this.#api.get('/admin/product-groups').subscribe({ + next: (g) => this.groups.set(g), + }); + } + + handleSearch(): void { + this.page = 1; + this.#syncLowStockQuery(); + this.loadProducts(); + } + + handleFilterChange(): void { + this.page = 1; + this.#syncLowStockQuery(); + this.loadProducts(); + } + + handleSort(column: SortBy): void { + if (this.sortBy === column) { + this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'; + } else { + this.sortBy = column; + this.sortDir = 'asc'; + } + + this.loadProducts(); + } + + sortIndicator(column: SortBy): string { + if (this.sortBy !== column) { + return ''; + } + + return this.sortDir === 'asc' ? ' ↑' : ' ↓'; + } + + handlePrevPage(): void { + if (this.page > 1) { + this.page -= 1; + this.loadProducts(); + } + } + + handleNextPage(): void { + const totalPages = Math.max( + 1, + Math.ceil(this.totalCount() / this.pageSize), + ); + if (this.page < totalPages) { + this.page += 1; + this.loadProducts(); + } + } + + warehouseQty(product: AdminProduct, warehouseId: string): number { + const variant = product.variants[0]; + if (!variant) { + return 0; + } + + return ( + variant.warehouseStocks.find((ws) => ws.warehouseId === warehouseId) + ?.quantity ?? 0 + ); + } + + groupNames(groupId?: string | null) { + if (!groupId) { + return undefined; + } + + return this.groups().find((g) => g.id === groupId)?.names; + } + + productImage(url?: string | null): string { + return resolveProductImageUrl(url); + } + + openCreate(): void { + this.modalMode.set('create'); + this.editingProductId.set(null); + this.modalOpen.set(true); + } + + openEdit(product: AdminProduct): void { + this.modalMode.set('edit'); + this.editingProductId.set(product.id); + this.modalOpen.set(true); + } + + closeModal(): void { + this.modalOpen.set(false); + this.editingProductId.set(null); + } + + handleSaved(): void { + this.closeModal(); + this.loadProducts(); + this.#toast.success('common.saved'); + } + + openCreateGroup(): void { + this.groupModalMode.set('create'); + this.editingGroup.set(null); + this.groupModalOpen.set(true); + } + + openEditGroup(group: ProductGroupDto): void { + this.groupModalMode.set('edit'); + this.editingGroup.set(group); + this.groupModalOpen.set(true); + } + + closeGroupModal(): void { + this.groupModalOpen.set(false); + this.editingGroup.set(null); + } + + handleGroupSaved(): void { + this.closeGroupModal(); + this.loadGroups(); + this.#toast.success('common.saved'); + } + + handleGroupDeleted(): void { + this.closeGroupModal(); + this.loadGroups(); + this.loadProducts(); + this.#toast.success('common.deleted'); + } + + addWarehouse(event: Event): void { + event.preventDefault(); + this.warehouseError.set(''); + + const name = this.newWarehouseName.trim(); + const code = this.newWarehouseCode.trim().toUpperCase(); + + if (!name || !code) { + this.warehouseError.set('warehouses.errorRequired'); + return; + } + + const sortOrder = this.warehouses().length + 1; + this.#api + .post('/admin/warehouses', { name, code, sortOrder }) + .subscribe({ + next: () => { + this.newWarehouseName = ''; + this.newWarehouseCode = ''; + this.loadWarehouses(); + this.#toast.success('common.saved'); + }, + error: () => this.warehouseError.set('warehouses.errorSave'), + }); + } + + startEditWarehouse(warehouse: WarehouseDto): void { + this.editingWarehouseId.set(warehouse.id); + this.editWarehouseName = warehouse.name; + this.editWarehouseSort = warehouse.sortOrder; + } + + cancelEditWarehouse(): void { + this.editingWarehouseId.set(null); + } + + saveWarehouse(warehouse: WarehouseDto): void { + const name = this.editWarehouseName.trim(); + if (!name) { + this.warehouseError.set('warehouses.errorRequired'); + return; + } + + this.#api + .put(`/admin/warehouses/${warehouse.id}`, { + name, + sortOrder: this.editWarehouseSort, + }) + .subscribe({ + next: () => { + this.editingWarehouseId.set(null); + this.loadWarehouses(); + this.#toast.success('common.saved'); + }, + error: () => this.warehouseError.set('warehouses.errorSave'), + }); + } + + deleteWarehouse(warehouse: WarehouseDto): void { + if (!confirm(`Usunąć magazyn „${warehouse.name}”?`)) { + return; + } + + this.warehouseError.set(''); + this.#api.delete(`/admin/warehouses/${warehouse.id}`).subscribe({ + next: () => { + this.loadWarehouses(); + this.#toast.success('common.deleted'); + }, + error: () => this.warehouseError.set('warehouses.errorDelete'), + }); + } + + #syncLowStockQuery(): void { + void this.#router.navigate([], { + relativeTo: this.#route, + queryParams: this.filterLowStock ? { lowStock: '1' } : {}, + replaceUrl: true, + }); + } +} diff --git a/src/admin/src/app/features/products/group-form-modal.component.html b/src/admin/src/app/features/products/group-form-modal.component.html new file mode 100644 index 0000000..c38c0cb --- /dev/null +++ b/src/admin/src/app/features/products/group-form-modal.component.html @@ -0,0 +1,106 @@ +@if (open()) { + +} diff --git a/src/admin/src/app/features/products/group-form-modal.component.ts b/src/admin/src/app/features/products/group-form-modal.component.ts new file mode 100644 index 0000000..bf30b43 --- /dev/null +++ b/src/admin/src/app/features/products/group-form-modal.component.ts @@ -0,0 +1,141 @@ +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + input, + output, + signal, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ApiService } from '../../core/api.service'; +import { + EMPTY_LOCALIZED, + LocalizedText, + ProductGroupDto, +} from './product.models'; + +@Component({ + selector: 'app-group-form-modal', + standalone: true, + imports: [FormsModule, TranslatePipe], + templateUrl: './group-form-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class GroupFormModalComponent { + readonly #api = inject(ApiService); + + readonly open = input(false); + readonly mode = input<'create' | 'edit'>('create'); + readonly group = input(null); + readonly nextSortOrder = input(1); + + readonly closed = output(); + readonly saved = output(); + readonly deleted = output(); + + readonly error = signal(''); + + names: LocalizedText = { ...EMPTY_LOCALIZED }; + code = ''; + sortOrder = 1; + + readonly locales: Array<{ key: keyof LocalizedText; label: string }> = [ + { key: 'pl', label: 'PL' }, + { key: 'en', label: 'EN' }, + { key: 'de', label: 'DE' }, + { key: 'cs', label: 'CZ' }, + ]; + + constructor() { + effect(() => { + if (!this.open()) { + return; + } + + this.error.set(''); + if (this.mode() === 'create') { + this.names = { ...EMPTY_LOCALIZED }; + this.code = ''; + this.sortOrder = this.nextSortOrder(); + return; + } + + const g = this.group(); + if (g) { + this.names = { ...g.names }; + this.code = g.code; + this.sortOrder = g.sortOrder; + } + }); + } + + handleClose(): void { + this.closed.emit(); + } + + handleBackdrop(event: MouseEvent): void { + if (event.target === event.currentTarget) { + this.handleClose(); + } + } + + handleSubmit(event: Event): void { + event.preventDefault(); + this.error.set(''); + + if (!this.names.pl.trim()) { + this.error.set('groups.errorRequired'); + return; + } + + if (this.mode() === 'create') { + const code = this.code.trim().toUpperCase(); + if (!code) { + this.error.set('groups.errorRequired'); + return; + } + + this.#api + .post('/admin/product-groups', { + names: this.names, + code, + sortOrder: this.sortOrder, + }) + .subscribe({ + next: () => this.saved.emit(), + error: () => this.error.set('groups.errorSave'), + }); + return; + } + + const id = this.group()?.id; + if (!id) { + return; + } + + this.#api + .put(`/admin/product-groups/${id}`, { + names: this.names, + sortOrder: this.sortOrder, + }) + .subscribe({ + next: () => this.saved.emit(), + error: () => this.error.set('groups.errorSave'), + }); + } + + handleDelete(): void { + const g = this.group(); + if (!g || !confirm(`Usunąć grupę „${g.names.pl}”?`)) { + return; + } + + this.error.set(''); + this.#api.delete(`/admin/product-groups/${g.id}`).subscribe({ + next: () => this.deleted.emit(), + error: () => this.error.set('groups.errorDelete'), + }); + } +} diff --git a/src/admin/src/app/features/products/localized-text.utils.ts b/src/admin/src/app/features/products/localized-text.utils.ts new file mode 100644 index 0000000..6404efe --- /dev/null +++ b/src/admin/src/app/features/products/localized-text.utils.ts @@ -0,0 +1,13 @@ +import { LocaleCode } from '../../core/i18n/i18n.models'; +import { LocalizedText } from './product.models'; + +export function localizedText( + text: LocalizedText | undefined, + locale: LocaleCode, +): string { + if (!text) { + return ''; + } + + return text[locale]?.trim() || text.pl?.trim() || ''; +} diff --git a/src/admin/src/app/features/products/product-form-modal.component.html b/src/admin/src/app/features/products/product-form-modal.component.html new file mode 100644 index 0000000..2bfb300 --- /dev/null +++ b/src/admin/src/app/features/products/product-form-modal.component.html @@ -0,0 +1,390 @@ +@if (open()) { + +} diff --git a/src/admin/src/app/features/products/product-form-modal.component.ts b/src/admin/src/app/features/products/product-form-modal.component.ts new file mode 100644 index 0000000..5bf8064 --- /dev/null +++ b/src/admin/src/app/features/products/product-form-modal.component.ts @@ -0,0 +1,432 @@ +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + input, + output, + signal, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { LocalizedTextPipe } from '../../core/i18n/localized-text.pipe'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ApiService } from '../../core/api.service'; +import { resolveProductImageUrl } from '../../core/product-image.util'; +import { + AdminProductDetail, + CategoryDto, + EMPTY_LOCALIZED, + LocalizedText, + ProductAttributeDto, + ProductImageDto, + ProductGroupDto, + WarehouseDto, + WarehouseQuantity, +} from './product.models'; + +@Component({ + selector: 'app-product-form-modal', + standalone: true, + imports: [FormsModule, TranslatePipe, LocalizedTextPipe], + templateUrl: './product-form-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductFormModalComponent { + readonly #api = inject(ApiService); + + readonly open = input(false); + readonly mode = input<'create' | 'edit'>('create'); + readonly productId = input(null); + readonly warehouses = input([]); + readonly groups = input([]); + readonly categories = input([]); + + readonly closed = output(); + readonly saved = output(); + + readonly loading = signal(false); + readonly error = signal(''); + readonly uploadingImageIndex = signal(null); + + slug = ''; + model = ''; + ean = ''; + productCode = ''; + basePrice = 0; + lowestPrice30Days = 0; + categoryId = ''; + productGroupId = ''; + deliveryHours = 24; + isActive = true; + isBestseller = false; + isNewArrival = false; + + names: LocalizedText = { ...EMPTY_LOCALIZED }; + shortDescriptions: LocalizedText = { ...EMPTY_LOCALIZED }; + descriptions: LocalizedText = { ...EMPTY_LOCALIZED }; + + attributes: ProductAttributeDto[] = []; + images: ProductImageDto[] = []; + stockByWarehouse: Record = {}; + variantId = ''; + + readonly locales: Array<{ key: keyof LocalizedText; label: string }> = [ + { key: 'pl', label: 'PL' }, + { key: 'en', label: 'EN' }, + { key: 'de', label: 'DE' }, + { key: 'cs', label: 'CZ' }, + ]; + + constructor() { + effect(() => { + if (!this.open()) { + return; + } + + if (this.mode() === 'create') { + this.resetForm(); + return; + } + + const id = this.productId(); + if (id) { + this.loadProduct(id); + } + }); + } + + handleClose(): void { + this.closed.emit(); + } + + handleBackdrop(event: MouseEvent): void { + if (event.target === event.currentTarget) { + this.handleClose(); + } + } + + addAttribute(): void { + this.attributes = [ + ...this.attributes, + { names: { ...EMPTY_LOCALIZED }, values: { ...EMPTY_LOCALIZED } }, + ]; + } + + removeAttribute(index: number): void { + this.attributes = this.attributes.filter((_, i) => i !== index); + } + + addImage(): void { + this.images = [ + ...this.images, + { + url: '', + altText: '', + sortOrder: this.images.length, + isPrimary: this.images.length === 0, + }, + ]; + } + + removeImage(index: number): void { + const wasPrimary = this.images[index]?.isPrimary; + this.images = this.images.filter((_, i) => i !== index); + if (wasPrimary && this.images.length > 0) { + this.images[0].isPrimary = true; + } + this.images = this.images.map((img, i) => ({ ...img, sortOrder: i })); + } + + setPrimaryImage(index: number): void { + this.images = this.images.map((img, i) => ({ + ...img, + isPrimary: i === index, + })); + } + + imagePreview(url: string): string { + return resolveProductImageUrl(url); + } + + handleImageUpload(index: number, event: Event): void { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + input.value = ''; + if (!file) { + return; + } + + this.#uploadToIndex(index, file); + } + + handleNewImageUpload(event: Event): void { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + input.value = ''; + if (!file) { + return; + } + + const index = this.images.length; + this.addImage(); + this.#uploadToIndex(index, file); + } + + #uploadToIndex(index: number, file: File): void { + this.#api + .upload<{ url: string }>('/admin/products/images/upload', file) + .subscribe({ + next: (res) => { + this.images[index] = { + ...this.images[index], + url: res.url, + altText: this.images[index].altText || this.names.pl, + }; + this.images = [...this.images]; + this.uploadingImageIndex.set(null); + }, + error: () => { + this.uploadingImageIndex.set(null); + this.error.set('products.errorUpload'); + }, + }); + } + + handleSubmit(event: Event): void { + event.preventDefault(); + this.error.set(''); + + if (!this.names.pl.trim()) { + this.error.set('products.errorNamePl'); + return; + } + + if (this.mode() === 'create') { + this.createProduct(); + return; + } + + this.updateProduct(); + } + + private resetForm(): void { + this.slug = ''; + this.model = ''; + this.ean = ''; + this.productCode = `P-${Date.now()}`; + this.basePrice = 499.99; + this.lowestPrice30Days = 499.99; + this.categoryId = this.categories()[0]?.id ?? ''; + this.productGroupId = ''; + this.deliveryHours = 24; + this.isActive = true; + this.isBestseller = false; + this.isNewArrival = true; + this.names = { ...EMPTY_LOCALIZED }; + this.shortDescriptions = { ...EMPTY_LOCALIZED }; + this.descriptions = { ...EMPTY_LOCALIZED }; + this.attributes = []; + this.images = [ + { + url: `/assets/products/${this.slug || 'placeholder'}.svg`, + altText: '', + sortOrder: 0, + isPrimary: true, + }, + ]; + this.variantId = ''; + this.initStock(); + } + + private initStock(): void { + this.stockByWarehouse = {}; + for (const w of this.warehouses()) { + this.stockByWarehouse[w.id] = 0; + } + } + + private loadProduct(id: string): void { + this.loading.set(true); + this.#api.get(`/admin/products/${id}`).subscribe({ + next: (detail) => { + this.slug = detail.slug; + this.model = detail.model; + this.ean = detail.ean ?? ''; + this.productCode = detail.productCode; + this.basePrice = detail.basePrice; + this.lowestPrice30Days = detail.lowestPrice30Days; + this.categoryId = detail.categoryId; + this.productGroupId = detail.productGroupId ?? ''; + this.deliveryHours = detail.deliveryHours; + this.isActive = detail.isActive; + this.isBestseller = detail.isBestseller; + this.isNewArrival = detail.isNewArrival; + this.names = { ...detail.names }; + this.shortDescriptions = { ...detail.shortDescriptions }; + this.descriptions = { ...detail.descriptions }; + this.attributes = detail.attributes.map((a) => ({ + id: a.id, + names: { ...a.names }, + values: { ...a.values }, + filterGroup: a.filterGroup, + })); + this.images = detail.images.length + ? detail.images.map((img) => ({ ...img })) + : [ + { + url: '', + altText: '', + sortOrder: 0, + isPrimary: true, + }, + ]; + + const variant = detail.variants[0]; + this.variantId = variant?.id ?? ''; + this.initStock(); + if (variant) { + for (const ws of variant.warehouseStocks) { + this.stockByWarehouse[ws.warehouseId] = ws.quantity; + } + } + + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + this.error.set('products.errorLoad'); + }, + }); + } + + private filteredImages(): ProductImageDto[] { + return this.images + .filter((img) => img.url.trim()) + .map((img, index) => ({ + url: img.url.trim(), + altText: img.altText?.trim() || this.names.pl.trim() || null, + sortOrder: index, + isPrimary: img.isPrimary, + })); + } + + private syncImages(productId: string, onDone: () => void): void { + this.#api + .put(`/admin/products/${productId}/images`, { + images: this.filteredImages(), + }) + .subscribe({ + next: onDone, + error: () => this.error.set('products.errorSave'), + }); + } + + private warehouseQuantities(): WarehouseQuantity[] { + return this.warehouses().map((w) => ({ + warehouseId: w.id, + quantity: this.stockByWarehouse[w.id] ?? 0, + })); + } + + private filteredAttributes(): ProductAttributeDto[] { + return this.attributes.filter( + (a) => a.names.pl.trim() && a.values.pl.trim(), + ); + } + + private createProduct(): void { + const quantities = this.warehouseQuantities(); + const totalQty = quantities.reduce((sum, q) => sum + q.quantity, 0); + + this.#api + .post('/products', { + slug: this.slug, + names: this.names, + shortDescriptions: this.shortDescriptions, + descriptions: this.descriptions, + basePrice: this.basePrice, + lowestPrice30Days: this.lowestPrice30Days, + productCode: this.productCode, + model: this.model, + ean: this.ean || null, + categoryId: this.categoryId, + productGroupId: this.productGroupId || null, + isBestseller: this.isBestseller, + isNewArrival: this.isNewArrival, + deliveryHours: this.deliveryHours, + variants: [ + { + sku: `${this.slug}-01`, + colorName: 'czarny', + colorHex: '#000000', + priceAdjustment: 0, + quantity: totalQty, + isDefault: true, + warehouseQuantities: quantities, + }, + ], + attributes: this.filteredAttributes().map((a) => ({ + names: a.names, + values: a.values, + filterGroup: a.filterGroup ?? null, + })), + }) + .subscribe({ + next: (id) => { + this.syncImages(String(id), () => this.saved.emit()); + }, + error: () => this.error.set('products.errorSave'), + }); + } + + private updateProduct(): void { + const id = this.productId(); + if (!id) { + return; + } + + this.#api + .put(`/products/${id}`, { + id, + slug: this.slug, + names: this.names, + shortDescriptions: this.shortDescriptions, + descriptions: this.descriptions, + basePrice: this.basePrice, + lowestPrice30Days: this.lowestPrice30Days, + productCode: this.productCode, + model: this.model, + ean: this.ean || null, + categoryId: this.categoryId, + productGroupId: this.productGroupId || null, + isBestseller: this.isBestseller, + isNewArrival: this.isNewArrival, + isActive: this.isActive, + deliveryHours: this.deliveryHours, + attributes: this.filteredAttributes().map((a) => ({ + names: a.names, + values: a.values, + filterGroup: a.filterGroup ?? null, + })), + }) + .subscribe({ + next: () => { + const finish = () => this.saved.emit(); + if (!this.variantId) { + this.syncImages(id, finish); + return; + } + + this.#api + .put(`/products/variants/${this.variantId}/warehouse-stock`, { + quantities: this.warehouseQuantities(), + }) + .subscribe({ + next: () => this.syncImages(id, finish), + error: () => this.error.set('products.errorSave'), + }); + }, + error: () => this.error.set('products.errorSave'), + }); + } +} diff --git a/src/admin/src/app/features/products/product.models.ts b/src/admin/src/app/features/products/product.models.ts new file mode 100644 index 0000000..b31b8af --- /dev/null +++ b/src/admin/src/app/features/products/product.models.ts @@ -0,0 +1,108 @@ +export interface LocalizedText { + pl: string; + en: string; + de: string; + cs: string; +} + +export const EMPTY_LOCALIZED: LocalizedText = { + pl: '', + en: '', + de: '', + cs: '', +}; + +export interface WarehouseDto { + id: string; + name: string; + code: string; + sortOrder: number; +} + +export interface ProductGroupDto { + id: string; + names: LocalizedText; + code: string; + sortOrder: number; +} + +export interface CategoryDto { + id: string; + name: string; + slug: string; +} + +export interface WarehouseStockDto { + warehouseId: string; + warehouseName: string; + warehouseCode: string; + quantity: number; +} + +export interface ProductAttributeDto { + id?: string; + names: LocalizedText; + values: LocalizedText; + filterGroup?: string | null; +} + +export interface ProductImageDto { + id?: string; + url: string; + altText?: string | null; + sortOrder: number; + isPrimary: boolean; +} + +export interface AdminVariant { + id: string; + sku: string; + colorName: string; + totalStock: number; + reservedStock: number; + warehouseStocks: WarehouseStockDto[]; +} + +export interface AdminProduct { + id: string; + name: string; + slug: string; + model: string; + ean?: string | null; + productGroupId?: string | null; + productGroupName?: string | null; + basePrice: number; + isActive: boolean; + isInStock: boolean; + totalStock: number; + reservedStock: number; + primaryImageUrl?: string | null; + variants: AdminVariant[]; +} + +export interface AdminProductDetail { + id: string; + slug: string; + names: LocalizedText; + shortDescriptions: LocalizedText; + descriptions: LocalizedText; + basePrice: number; + lowestPrice30Days: number; + productCode: string; + model: string; + ean?: string | null; + categoryId: string; + productGroupId?: string | null; + isBestseller: boolean; + isNewArrival: boolean; + isActive: boolean; + deliveryHours: number; + attributes: ProductAttributeDto[]; + images: ProductImageDto[]; + variants: AdminVariant[]; +} + +export interface WarehouseQuantity { + warehouseId: string; + quantity: number; +} diff --git a/src/admin/src/app/features/promotions-admin.component.html b/src/admin/src/app/features/promotions-admin.component.html new file mode 100644 index 0000000..a99442f --- /dev/null +++ b/src/admin/src/app/features/promotions-admin.component.html @@ -0,0 +1,363 @@ +

{{ 'promotions.title' | translate }}

+ +
+ +
+ + +
+ + +
+
+ + +
+ +
+ +
+ @if (loading()) { +

+ {{ 'products.loading' | translate }} +

+ } @else { + + + + + + + + + + + + + + @for (p of promotions(); track p.id) { + + + + + + + + + + } @empty { + + + + } + +
{{ 'promotions.name' | translate }}{{ 'promotions.typeLabel' | translate }}{{ 'promotions.value' | translate }}{{ 'promotions.minOrder' | translate }}{{ 'promotions.codes' | translate }}{{ 'products.active' | translate }}
{{ p.name }}{{ typeLabel(p.type) | translate }}{{ p.value }}{{ p.minimumOrderAmount ?? '—' }}{{ p.codeCount }} / {{ p.redemptionCount }}{{ p.isActive ? '✓' : '—' }} + + +
+ {{ 'promotions.empty' | translate }} +
+ + } +
+ +@if (selectedId()) { +
+

+ {{ 'promotions.codesFor' | translate }} +

+
+ + + +
+
    + @for (c of codes(); track c.id) { +
  • + + {{ c.code }} + — {{ c.usageCount }} / {{ c.maxUsages ?? '∞' }} + @if (c.issuedToEmail) { + + ({{ c.issuedToEmail }}) + + } + + +
  • + } @empty { +
  • {{ 'promotions.noCodes' | translate }}
  • + } +
+
+} + +@if (formOpen()) { +
+
+

+ {{ + (editingId() ? 'promotions.edit' : 'promotions.add') | translate + }} +

+ +
+ + {{ 'promotions.sectionBenefit' | translate }} + + +
+ + +
+
+ +
+ + {{ 'promotions.sectionRequirements' | translate }} + + +
+ + +
+
+ +
+ + {{ 'promotions.sectionLimits' | translate }} + +
+ + +
+
+ +
+ + {{ 'promotions.sectionBanner' | translate }} + + +
+ @for (locale of locales; track locale.key) { +
+ + +
+ } +
+ +
+ +
+ + {{ 'promotions.sectionReward' | translate }} + + + +
+ + +
+
+ + + +
+ + +
+
+
+} diff --git a/src/admin/src/app/features/promotions-admin.component.ts b/src/admin/src/app/features/promotions-admin.component.ts new file mode 100644 index 0000000..d243ae5 --- /dev/null +++ b/src/admin/src/app/features/promotions-admin.component.ts @@ -0,0 +1,353 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, + signal, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ApiService } from '../core/api.service'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; +import { AdminListToolbarComponent } from '../shared/admin-list-toolbar.component'; +import { AdminPaginationComponent } from '../shared/admin-pagination.component'; +import { ToastService } from '../shared/toast.service'; + +interface Paginated { + items: T[]; + totalCount: number; +} + +interface LocalizedText { + pl: string; + en: string; + de: string; + cs: string; +} + +interface Promotion { + id: string; + name: string; + type: number | string; + value: number; + minimumOrderAmount: number | null; + validFrom: string | null; + validTo: string | null; + isActive: boolean; + maxTotalRedemptions: number | null; + maxRedemptionsPerCustomer: number | null; + showInBanner: boolean; + bannerTexts: LocalizedText; + bannerHighlightedCode: string | null; + issueRewardOnPaidOrder: boolean; + rewardMinPaidOrderTotal: number | null; + rewardCodePrefix: string | null; + rewardValidDays: number | null; + codeCount: number; + redemptionCount: number; +} + +interface PromoCode { + id: string; + code: string; + usageCount: number; + maxUsages: number | null; + isActive: boolean; + source: number | string; + issuedToEmail: string | null; + validUntil: string | null; + createdAt: string; +} + +const EMPTY_BANNER: LocalizedText = { + pl: '', + en: '', + de: '', + cs: '', +}; + +const EMPTY_FORM = { + name: '', + type: 0, + value: 20, + minimumOrderAmount: null as number | null, + validFrom: '', + validTo: '', + isActive: true, + maxTotalRedemptions: null as number | null, + maxRedemptionsPerCustomer: null as number | null, + showInBanner: false, + bannerTexts: { ...EMPTY_BANNER } as LocalizedText, + bannerHighlightedCode: '', + issueRewardOnPaidOrder: false, + rewardMinPaidOrderTotal: null as number | null, + rewardCodePrefix: '', + rewardValidDays: 30 as number | null, +}; + +@Component({ + selector: 'app-promotions-admin', + standalone: true, + imports: [ + FormsModule, + TranslatePipe, + AdminListToolbarComponent, + AdminPaginationComponent, + ], + templateUrl: './promotions-admin.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PromotionsAdminComponent implements OnInit { + readonly #api = inject(ApiService); + readonly #toast = inject(ToastService); + + readonly promotions = signal([]); + readonly codes = signal([]); + readonly loading = signal(false); + readonly totalCount = signal(0); + readonly editingId = signal(null); + readonly selectedId = signal(null); + readonly formOpen = signal(false); + + search = ''; + filterActive: '' | 'true' | 'false' = ''; + page = 1; + readonly pageSize = 20; + form = { + ...EMPTY_FORM, + bannerTexts: { ...EMPTY_BANNER }, + }; + newCode = ''; + newCodeMaxUsages: number | null = 1000; + readonly locales: Array<{ key: keyof LocalizedText; label: string }> = [ + { key: 'pl', label: 'PL' }, + { key: 'en', label: 'EN' }, + { key: 'de', label: 'DE' }, + { key: 'cs', label: 'CZ' }, + ]; + + ngOnInit(): void { + this.load(); + } + + load(): void { + this.loading.set(true); + const params = new URLSearchParams({ + page: String(this.page), + pageSize: String(this.pageSize), + }); + if (this.search.trim()) { + params.set('search', this.search.trim()); + } + if (this.filterActive) { + params.set('isActive', this.filterActive); + } + + this.#api + .get>(`/marketing/promotions?${params}`) + .subscribe({ + next: (res) => { + this.promotions.set(res.items); + this.totalCount.set(res.totalCount); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + this.#toast.error('promotions.errorLoad'); + }, + }); + } + + handleSearch(): void { + this.page = 1; + this.load(); + } + + handleFilterChange(): void { + this.page = 1; + this.load(); + } + + handlePrevPage(): void { + if (this.page > 1) { + this.page -= 1; + this.load(); + } + } + + handleNextPage(): void { + const pages = Math.max(1, Math.ceil(this.totalCount() / this.pageSize)); + if (this.page < pages) { + this.page += 1; + this.load(); + } + } + + openCreate(): void { + this.editingId.set(null); + this.form = { + ...EMPTY_FORM, + bannerTexts: { ...EMPTY_BANNER }, + }; + this.formOpen.set(true); + } + + openEdit(p: Promotion): void { + this.editingId.set(p.id); + this.form = { + name: p.name, + type: typeof p.type === 'string' ? this.typeToNum(p.type) : p.type, + value: p.value, + minimumOrderAmount: p.minimumOrderAmount, + validFrom: p.validFrom ? p.validFrom.slice(0, 10) : '', + validTo: p.validTo ? p.validTo.slice(0, 10) : '', + isActive: p.isActive, + maxTotalRedemptions: p.maxTotalRedemptions, + maxRedemptionsPerCustomer: p.maxRedemptionsPerCustomer, + showInBanner: p.showInBanner, + bannerTexts: { + pl: p.bannerTexts?.pl ?? '', + en: p.bannerTexts?.en ?? '', + de: p.bannerTexts?.de ?? '', + cs: p.bannerTexts?.cs ?? '', + }, + bannerHighlightedCode: p.bannerHighlightedCode ?? '', + issueRewardOnPaidOrder: p.issueRewardOnPaidOrder, + rewardMinPaidOrderTotal: p.rewardMinPaidOrderTotal, + rewardCodePrefix: p.rewardCodePrefix ?? '', + rewardValidDays: p.rewardValidDays, + }; + this.formOpen.set(true); + this.selectPromotion(p.id); + } + + closeForm(): void { + this.formOpen.set(false); + } + + save(): void { + const body = { + name: this.form.name, + type: Number(this.form.type), + value: this.form.value, + minimumOrderAmount: this.form.minimumOrderAmount, + validFrom: this.form.validFrom || null, + validTo: this.form.validTo || null, + isActive: this.form.isActive, + maxTotalRedemptions: this.form.maxTotalRedemptions, + maxRedemptionsPerCustomer: this.form.maxRedemptionsPerCustomer, + showInBanner: this.form.showInBanner, + bannerTexts: this.form.bannerTexts, + bannerHighlightedCode: this.form.bannerHighlightedCode || null, + issueRewardOnPaidOrder: this.form.issueRewardOnPaidOrder, + rewardMinPaidOrderTotal: this.form.rewardMinPaidOrderTotal, + rewardCodePrefix: this.form.rewardCodePrefix || null, + rewardValidDays: this.form.rewardValidDays, + }; + + const id = this.editingId(); + const onOk = (): void => { + this.formOpen.set(false); + this.load(); + this.#toast.success('common.saved'); + }; + const onErr = (): void => this.#toast.error('promotions.errorSave'); + + if (id) { + this.#api + .put(`/marketing/promotions/${id}`, { ...body, id }) + .subscribe({ next: onOk, error: onErr }); + return; + } + + this.#api + .post('/marketing/promotions', body) + .subscribe({ next: onOk, error: onErr }); + } + + toggleActive(p: Promotion): void { + this.#api + .put(`/marketing/promotions/${p.id}/active`, { + isActive: !p.isActive, + }) + .subscribe({ + next: () => { + this.load(); + this.#toast.success('common.saved'); + }, + error: () => this.#toast.error('promotions.errorSave'), + }); + } + + selectPromotion(id: string): void { + this.selectedId.set(id); + this.#api + .get>(`/marketing/promotions/${id}/codes?pageSize=50`) + .subscribe({ + next: (res) => this.codes.set(res.items), + error: () => this.#toast.error('promotions.errorLoad'), + }); + } + + createCode(): void { + const id = this.selectedId(); + if (!id) { + return; + } + + this.#api + .post<{ code: string }>(`/marketing/promotions/${id}/codes`, { + code: this.newCode || null, + maxUsages: this.newCodeMaxUsages, + }) + .subscribe({ + next: (res) => { + this.newCode = ''; + this.selectPromotion(id); + this.load(); + this.#toast.success('promotions.codeCreated'); + void navigator.clipboard?.writeText(res.code); + }, + error: () => this.#toast.error('promotions.errorSave'), + }); + } + + toggleCode(c: PromoCode): void { + this.#api + .put(`/marketing/discount-codes/${c.id}/active`, { + isActive: !c.isActive, + }) + .subscribe({ + next: () => { + const id = this.selectedId(); + if (id) { + this.selectPromotion(id); + } + this.#toast.success('common.saved'); + }, + error: () => this.#toast.error('promotions.errorSave'), + }); + } + + typeLabel(type: number | string): string { + const n = typeof type === 'string' ? this.typeToNum(type) : type; + switch (n) { + case 1: + return 'promotions.type.fixed'; + case 2: + return 'promotions.type.freeShip'; + default: + return 'promotions.type.percent'; + } + } + + private typeToNum(type: string): number { + switch (type.toLowerCase()) { + case 'fixedamount': + return 1; + case 'freeshipping': + return 2; + default: + return 0; + } + } +} diff --git a/src/admin/src/app/features/reviews-admin.component.html b/src/admin/src/app/features/reviews-admin.component.html new file mode 100644 index 0000000..cc496cb --- /dev/null +++ b/src/admin/src/app/features/reviews-admin.component.html @@ -0,0 +1,92 @@ +

{{ 'reviews.title' | translate }}

+ + +
+ + +
+
+ + +
+ +
+ +@if (loading()) { +

{{ 'products.loading' | translate }}

+} @else { + @for (r of reviews(); track r.id) { +
+
+ {{ r.productName }} + + — {{ r.authorName }} ({{ r.rating }}/5) + + + {{ 'reviews.status.' + r.status | translate }} + +
+

{{ r.content }}

+ @if (r.status === 'pending') { +
+ + +
+ } +
+ } @empty { +

{{ 'reviews.empty' | translate }}

+ } + +
+ +
+} diff --git a/src/admin/src/app/layout/admin-layout.component.html b/src/admin/src/app/layout/admin-layout.component.html new file mode 100644 index 0000000..d44edc1 --- /dev/null +++ b/src/admin/src/app/layout/admin-layout.component.html @@ -0,0 +1,129 @@ +
+ @if (sidebarOpen()) { + + } + + + +
+
+ + iLED Admin +
+ +
+ +
+
+ + +
diff --git a/src/admin/src/app/layout/admin-layout.component.ts b/src/admin/src/app/layout/admin-layout.component.ts new file mode 100644 index 0000000..8bf82ff --- /dev/null +++ b/src/admin/src/app/layout/admin-layout.component.ts @@ -0,0 +1,146 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + signal, +} from '@angular/core'; +import { + Router, + RouterLink, + RouterLinkActive, + RouterOutlet, +} from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { + LucideLayoutDashboard, + LucideLightbulb, + LucideLogOut, + LucideMail, + LucideMenu, + LucideMessageSquare, + LucidePackage, + LucidePercent, + LucideShoppingCart, + LucideX, +} from '@lucide/angular'; +import { LocaleSwitcherComponent } from '../core/i18n/locale-switcher.component'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; +import { AdminAuthService } from '../core/api.service'; +import { ToastHostComponent } from '../shared/toast-host.component'; + +@Component({ + selector: 'app-admin-layout', + standalone: true, + imports: [ + RouterOutlet, + RouterLink, + RouterLinkActive, + TranslatePipe, + LocaleSwitcherComponent, + ToastHostComponent, + LucideLightbulb, + LucideLayoutDashboard, + LucidePackage, + LucideShoppingCart, + LucidePercent, + LucideMail, + LucideMessageSquare, + LucideLogOut, + LucideMenu, + LucideX, + ], + templateUrl: './admin-layout.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdminLayoutComponent { + readonly #auth = inject(AdminAuthService); + readonly #router = inject(Router); + readonly sidebarOpen = signal(false); + + openSidebar(): void { + this.sidebarOpen.set(true); + } + + closeSidebar(): void { + this.sidebarOpen.set(false); + } + + logout(): void { + this.#auth.logout(); + this.#router.navigate(['/login']); + } +} + +@Component({ + selector: 'app-admin-login', + standalone: true, + imports: [LucideLightbulb, FormsModule, TranslatePipe], + template: ` +
+
+
+ + iLED Admin +
+ + + @if (error()) { + + } + +
+
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdminLoginComponent { + readonly #auth = inject(AdminAuthService); + readonly #router = inject(Router); + readonly error = signal(''); + + handleLogin(event: Event): void { + event.preventDefault(); + const form = event.target as HTMLFormElement; + const email = (form.elements[0] as HTMLInputElement).value; + const password = (form.elements[1] as HTMLInputElement).value; + + this.#auth.login(email, password).subscribe({ + next: (res) => { + if (!res.roles.includes('Admin')) { + this.error.set('auth.errorRole'); + return; + } + this.#auth.store(res.accessToken, res.roles); + this.#router.navigate(['/dashboard']); + }, + error: () => this.error.set('auth.errorLogin'), + }); + } +} diff --git a/src/admin/src/app/shared/admin-list-toolbar.component.ts b/src/admin/src/app/shared/admin-list-toolbar.component.ts new file mode 100644 index 0000000..d343c1a --- /dev/null +++ b/src/admin/src/app/shared/admin-list-toolbar.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * Shared filter/search chrome for admin list screens. + */ +@Component({ + selector: 'app-admin-list-toolbar', + standalone: true, + template: ` +
+ +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdminListToolbarComponent {} diff --git a/src/admin/src/app/shared/admin-pagination.component.ts b/src/admin/src/app/shared/admin-pagination.component.ts new file mode 100644 index 0000000..54c01f0 --- /dev/null +++ b/src/admin/src/app/shared/admin-pagination.component.ts @@ -0,0 +1,60 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, + output, +} from '@angular/core'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; + +/** + * Prev/next pagination footer for server-driven admin lists. + */ +@Component({ + selector: 'app-admin-pagination', + standalone: true, + imports: [TranslatePipe], + template: ` + @if (totalCount() > pageSize()) { +
+ + {{ 'common.page' | translate }} {{ page() }} / + {{ totalPages() }} ({{ totalCount() }}) + +
+ + +
+
+ } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdminPaginationComponent { + readonly page = input.required(); + readonly pageSize = input.required(); + readonly totalCount = input.required(); + + readonly prev = output(); + readonly next = output(); + + readonly totalPages = computed(() => + Math.max(1, Math.ceil(this.totalCount() / this.pageSize())), + ); +} diff --git a/src/admin/src/app/shared/toast-host.component.ts b/src/admin/src/app/shared/toast-host.component.ts new file mode 100644 index 0000000..0e3ba57 --- /dev/null +++ b/src/admin/src/app/shared/toast-host.component.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; +import { ToastService } from './toast.service'; + +@Component({ + selector: 'app-toast-host', + standalone: true, + imports: [TranslatePipe], + template: ` +
+ @for (m of toasts.messages(); track m.id) { +
+
+ {{ m.text | translate: m.params }} + +
+
+ } +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ToastHostComponent { + readonly toasts = inject(ToastService); + + toastClass(kind: string): string { + switch (kind) { + case 'success': + return 'bg-green-800 text-white'; + case 'error': + return 'bg-red-700 text-white'; + default: + return 'bg-iled-black text-white'; + } + } +} diff --git a/src/admin/src/app/shared/toast.service.ts b/src/admin/src/app/shared/toast.service.ts new file mode 100644 index 0000000..b0b137e --- /dev/null +++ b/src/admin/src/app/shared/toast.service.ts @@ -0,0 +1,58 @@ +import { Injectable, signal } from '@angular/core'; + +export type ToastKind = 'success' | 'error' | 'info'; + +export interface ToastMessage { + id: number; + kind: ToastKind; + text: string; + params?: Record; +} + +/** + * Lightweight toast queue for admin feedback. + */ +@Injectable({ providedIn: 'root' }) +export class ToastService { + readonly #messages = signal([]); + readonly messages = this.#messages.asReadonly(); + #nextId = 1; + + success( + text: string, + params?: Record, + ): void { + this.#push('success', text, params); + } + + error( + text: string, + params?: Record, + ): void { + this.#push('error', text, params); + } + + info( + text: string, + params?: Record, + ): void { + this.#push('info', text, params); + } + + dismiss(id: number): void { + this.#messages.update((list) => list.filter((m) => m.id !== id)); + } + + #push( + kind: ToastKind, + text: string, + params?: Record, + ): void { + const id = this.#nextId++; + this.#messages.update((list) => [ + ...list, + { id, kind, text, params }, + ]); + setTimeout(() => this.dismiss(id), 4000); + } +} diff --git a/src/admin/src/index.html b/src/admin/src/index.html new file mode 100644 index 0000000..8ccd923 --- /dev/null +++ b/src/admin/src/index.html @@ -0,0 +1,14 @@ + + + + + iLED Admin + + + + + + + + + diff --git a/src/admin/src/main.ts b/src/admin/src/main.ts new file mode 100644 index 0000000..5df75f9 --- /dev/null +++ b/src/admin/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig) + .catch((err) => console.error(err)); diff --git a/src/admin/src/styles.css b/src/admin/src/styles.css new file mode 100644 index 0000000..64285b7 --- /dev/null +++ b/src/admin/src/styles.css @@ -0,0 +1,53 @@ +@import 'tailwindcss'; +@source './**/*.{html,ts}'; + +@theme { + --color-iled-primary: #ffbc39; + --color-iled-black: #000000; + --color-iled-grey-dark: #8c8c8c; + --color-iled-grey-light: #d9d9d9; + --color-iled-bg: #f5f5f5; + --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; +} + +@layer base { + body { + @apply m-0 font-sans text-iled-black bg-iled-bg antialiased; + } +} + +@layer utilities { + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } +} + +@media print { + body * { + visibility: hidden; + } + + #invoice-print-area, + #invoice-print-area * { + visibility: visible; + } + + #invoice-print-area { + position: absolute; + left: 0; + top: 0; + width: 100%; + } + + .invoice-backdrop { + background: white !important; + } +} diff --git a/src/admin/src/styles/_tokens.scss b/src/admin/src/styles/_tokens.scss new file mode 100644 index 0000000..4648431 --- /dev/null +++ b/src/admin/src/styles/_tokens.scss @@ -0,0 +1 @@ +$color-primary: #ffbc39; diff --git a/src/admin/tsconfig.app.json b/src/admin/tsconfig.app.json new file mode 100644 index 0000000..264f459 --- /dev/null +++ b/src/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": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/src/admin/tsconfig.json b/src/admin/tsconfig.json new file mode 100644 index 0000000..2ab7442 --- /dev/null +++ b/src/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": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/src/admin/tsconfig.spec.json b/src/admin/tsconfig.spec.json new file mode 100644 index 0000000..d383706 --- /dev/null +++ b/src/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": [ + "vitest/globals" + ] + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.spec.ts" + ] +} diff --git a/src/backend/body.json b/src/backend/body.json new file mode 100644 index 0000000..aa67c28 --- /dev/null +++ b/src/backend/body.json @@ -0,0 +1 @@ +{"status":{"statusCode":"ERROR_SYNTAX","code":"103","codeLiteral":"ERROR_SYNTAX","statusDesc":"Bad syntax"}} \ No newline at end of file diff --git a/src/backend/iLED.slnx b/src/backend/iLED.slnx new file mode 100644 index 0000000..05ee5e8 --- /dev/null +++ b/src/backend/iLED.slnx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/backend/src/iLED.Api/Controllers/AdminController.cs b/src/backend/src/iLED.Api/Controllers/AdminController.cs new file mode 100644 index 0000000..44b14be --- /dev/null +++ b/src/backend/src/iLED.Api/Controllers/AdminController.cs @@ -0,0 +1,188 @@ +using Asp.Versioning; +using iLED.Application.Catalog.Commands; +using iLED.Application.Catalog.Dtos; +using iLED.Application.Catalog.Queries; +using iLED.Application.Orders.Commands; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace iLED.Api.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/admin/products")] +[Authorize(Roles = "Admin")] +public sealed class AdminProductsController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task GetList( + [FromQuery] GetAdminProductsQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpGet("{id:guid}")] + public async Task GetDetail( + Guid id, + CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetAdminProductQuery(id), cancellationToken); + return Ok(result); + } + + [HttpPut("{id:guid}/images")] + public async Task SyncImages( + Guid id, + [FromBody] SyncProductImagesRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new SyncProductImagesCommand(id, request.Images), + cancellationToken); + return NoContent(); + } + + [HttpPost("images/upload")] + [RequestSizeLimit(10 * 1024 * 1024)] + public async Task UploadImage( + IFormFile file, + CancellationToken cancellationToken) + { + if (file.Length == 0) + { + return BadRequest(new { message = "Plik jest pusty." }); + } + + await using var stream = file.OpenReadStream(); + var result = await mediator.Send( + new UploadProductImageCommand(stream, file.FileName), + cancellationToken); + + return Ok(result); + } +} + +public sealed record SyncProductImagesRequest( + IReadOnlyList Images); + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/admin/warehouses")] +[Authorize(Roles = "Admin")] +public sealed class AdminWarehousesController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task GetAll(CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetWarehousesQuery(), cancellationToken); + return Ok(result); + } + + [HttpPost] + public async Task Create( + [FromBody] CreateWarehouseCommand command, + CancellationToken cancellationToken) + { + var id = await mediator.Send(command, cancellationToken); + return Ok(id); + } + + [HttpPut("{id:guid}")] + public async Task Update( + Guid id, + [FromBody] UpdateWarehouseRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new UpdateWarehouseCommand(id, request.Name, request.SortOrder), + cancellationToken); + return NoContent(); + } + + [HttpDelete("{id:guid}")] + public async Task Delete( + Guid id, + CancellationToken cancellationToken) + { + await mediator.Send(new DeleteWarehouseCommand(id), cancellationToken); + return NoContent(); + } +} + +public sealed record UpdateWarehouseRequest(string Name, int SortOrder); + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/admin/product-groups")] +[Authorize(Roles = "Admin")] +public sealed class AdminProductGroupsController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task GetAll(CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetProductGroupsQuery(), cancellationToken); + return Ok(result); + } + + [HttpPost] + public async Task Create( + [FromBody] CreateProductGroupCommand command, + CancellationToken cancellationToken) + { + var id = await mediator.Send(command, cancellationToken); + return Ok(id); + } + + [HttpPut("{id:guid}")] + public async Task Update( + Guid id, + [FromBody] UpdateProductGroupRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new UpdateProductGroupCommand(id, request.Names, request.SortOrder), + cancellationToken); + return NoContent(); + } + + [HttpDelete("{id:guid}")] + public async Task Delete( + Guid id, + CancellationToken cancellationToken) + { + await mediator.Send(new DeleteProductGroupCommand(id), cancellationToken); + return NoContent(); + } +} + +public sealed record UpdateProductGroupRequest( + LocalizedTextDto Names, + int SortOrder); + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/admin/dashboard")] +[Authorize(Roles = "Admin")] +public sealed class DashboardController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task GetStats(CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetDashboardStatsQuery(), cancellationToken); + return Ok(result); + } + + [HttpGet("orders-chart")] + public async Task GetOrdersChart( + [FromQuery] int days = 14, + CancellationToken cancellationToken = default) + { + var result = await mediator.Send( + new GetDashboardOrdersChartQuery(days), cancellationToken); + return Ok(result); + } +} diff --git a/src/backend/src/iLED.Api/Controllers/AuthController.cs b/src/backend/src/iLED.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..629665f --- /dev/null +++ b/src/backend/src/iLED.Api/Controllers/AuthController.cs @@ -0,0 +1,48 @@ +using Asp.Versioning; +using iLED.Application.Identity.Commands; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace iLED.Api.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/auth")] +public sealed class AuthController(IMediator mediator) : ControllerBase +{ + [HttpPost("register")] + public async Task Register( + [FromBody] RegisterCommand command, + CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return Ok(result); + } + + [HttpPost("login")] + public async Task Login( + [FromBody] LoginCommand command, + CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return Ok(result); + } + + [HttpPost("refresh")] + public async Task Refresh( + [FromBody] RefreshTokenCommand command, + CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return Ok(result); + } + + [HttpGet("me")] + [Authorize] + public async Task Me(CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetCurrentUserQuery(), cancellationToken); + return result is null ? Unauthorized() : Ok(result); + } +} diff --git a/src/backend/src/iLED.Api/Controllers/CartController.cs b/src/backend/src/iLED.Api/Controllers/CartController.cs new file mode 100644 index 0000000..06a59fc --- /dev/null +++ b/src/backend/src/iLED.Api/Controllers/CartController.cs @@ -0,0 +1,86 @@ +using Asp.Versioning; +using iLED.Application.Cart.Commands; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace iLED.Api.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/cart")] +public sealed class CartController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task Get(CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetCartQuery(), cancellationToken); + return Ok(result); + } + + [HttpPost("items")] + public async Task AddItem( + [FromBody] AddToCartCommand command, + CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return Ok(result); + } + + [HttpPut("items/{itemId:guid}")] + public async Task UpdateItem( + Guid itemId, + [FromBody] UpdateQuantityRequest request, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new UpdateCartItemCommand(itemId, request.Quantity), + cancellationToken); + return Ok(result); + } + + [HttpDelete("items/{itemId:guid}")] + public async Task RemoveItem( + Guid itemId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new RemoveCartItemCommand(itemId), + cancellationToken); + return Ok(result); + } +} + +public sealed record UpdateQuantityRequest(int Quantity); + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/favorites")] +[Authorize] +public sealed class FavoritesController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task Get(CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetFavoritesQuery(), cancellationToken); + return Ok(result); + } + + [HttpPost("{productId:guid}")] + public async Task Add( + Guid productId, + CancellationToken cancellationToken) + { + await mediator.Send(new AddFavoriteCommand(productId), cancellationToken); + return NoContent(); + } + + [HttpDelete("{productId:guid}")] + public async Task Remove( + Guid productId, + CancellationToken cancellationToken) + { + await mediator.Send(new RemoveFavoriteCommand(productId), cancellationToken); + return NoContent(); + } +} diff --git a/src/backend/src/iLED.Api/Controllers/CatalogController.cs b/src/backend/src/iLED.Api/Controllers/CatalogController.cs new file mode 100644 index 0000000..ddcd8cb --- /dev/null +++ b/src/backend/src/iLED.Api/Controllers/CatalogController.cs @@ -0,0 +1,170 @@ +using Asp.Versioning; +using iLED.Application.Catalog.Commands; +using iLED.Application.Catalog.Queries; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace iLED.Api.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/products")] +public sealed class ProductsController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task GetList( + [FromQuery] GetProductListQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpGet("filters")] + public async Task GetFilters( + [FromQuery] string? locale, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetProductFilterOptionsQuery(locale), cancellationToken); + return Ok(result); + } + + [HttpGet("{slug}")] + public async Task GetDetail( + string slug, + [FromQuery] string? locale, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetProductDetailQuery(slug, locale), cancellationToken); + return Ok(result); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task Create( + [FromBody] CreateProductCommand command, + CancellationToken cancellationToken) + { + var id = await mediator.Send(command, cancellationToken); + return CreatedAtAction(nameof(GetDetail), new { slug = command.Slug }, id); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Update( + Guid id, + [FromBody] UpdateProductCommand command, + CancellationToken cancellationToken) + { + if (id != command.Id) + { + return BadRequest(); + } + + await mediator.Send(command, cancellationToken); + return NoContent(); + } + + [HttpPut("variants/{variantId:guid}/stock")] + [Authorize(Roles = "Admin")] + public async Task UpdateStock( + Guid variantId, + [FromBody] UpdateStockRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new UpdateStockCommand(variantId, request.Quantity), + cancellationToken); + return NoContent(); + } + + [HttpPut("variants/{variantId:guid}/warehouse-stock")] + [Authorize(Roles = "Admin")] + public async Task UpdateWarehouseStock( + Guid variantId, + [FromBody] UpdateWarehouseStockRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new UpdateWarehouseStockCommand(variantId, request.Quantities), + cancellationToken); + return NoContent(); + } + + [HttpPost("{productId:guid}/images")] + [Authorize(Roles = "Admin")] + public async Task AddImage( + Guid productId, + [FromBody] AddProductImageRequest request, + CancellationToken cancellationToken) + { + var id = await mediator.Send( + new AddProductImageCommand( + productId, + request.Url, + request.AltText, + request.SortOrder, + request.IsPrimary), + cancellationToken); + return Ok(id); + } +} + +public sealed record UpdateStockRequest(int Quantity); + +public sealed record UpdateWarehouseStockRequest( + IReadOnlyList Quantities); + +public sealed record AddProductImageRequest( + string Url, + string? AltText, + int SortOrder, + bool IsPrimary); + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/categories")] +public sealed class CategoriesController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task GetAll(CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetCategoriesQuery(), cancellationToken); + return Ok(result); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task Create( + [FromBody] CreateCategoryCommand command, + CancellationToken cancellationToken) + { + var id = await mediator.Send(command, cancellationToken); + return Ok(id); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Update( + Guid id, + [FromBody] UpdateCategoryRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new UpdateCategoryCommand( + id, + request.Name, + request.Slug, + request.Description), + cancellationToken); + return NoContent(); + } +} + +public sealed record UpdateCategoryRequest( + string Name, + string Slug, + string? Description); diff --git a/src/backend/src/iLED.Api/Controllers/MarketingController.cs b/src/backend/src/iLED.Api/Controllers/MarketingController.cs new file mode 100644 index 0000000..74e34a9 --- /dev/null +++ b/src/backend/src/iLED.Api/Controllers/MarketingController.cs @@ -0,0 +1,305 @@ +using Asp.Versioning; +using iLED.Application.Marketing.Commands; +using iLED.Application.Reviews.Commands; +using iLED.Domain.Enums; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace iLED.Api.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/marketing")] +public sealed class MarketingController(IMediator mediator) : ControllerBase +{ + [HttpGet("banner")] + public async Task GetBanner( + [FromQuery] string? locale, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetPromoBannerQuery(locale), cancellationToken); + return Ok(result); + } + + [HttpPost("newsletter/subscribe")] + public async Task Subscribe( + [FromBody] SubscribeRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new SubscribeNewsletterCommand(request.Email), + cancellationToken); + return NoContent(); + } + + [HttpPost("discount/validate")] + public async Task ValidateDiscount( + [FromBody] ValidateDiscountRequest request, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new ValidateDiscountCodeQuery( + request.Code, + request.Subtotal, + request.CustomerEmail), + cancellationToken); + return Ok(result); + } + + [HttpGet("promotions")] + [Authorize(Roles = "Admin")] + public async Task GetPromotions( + [FromQuery] GetPromotionsQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpGet("promotions/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task GetPromotion( + Guid id, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetPromotionQuery(id), cancellationToken); + return Ok(result); + } + + [HttpPost("promotions")] + [Authorize(Roles = "Admin")] + public async Task CreatePromotion( + [FromBody] UpsertPromotionCommand command, + CancellationToken cancellationToken) + { + var id = await mediator.Send( + command with { Id = null }, + cancellationToken); + return Ok(id); + } + + [HttpPut("promotions/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task UpdatePromotion( + Guid id, + [FromBody] UpsertPromotionCommand command, + CancellationToken cancellationToken) + { + await mediator.Send(command with { Id = id }, cancellationToken); + return NoContent(); + } + + [HttpPut("promotions/{id:guid}/active")] + [Authorize(Roles = "Admin")] + public async Task SetPromotionActive( + Guid id, + [FromBody] SetActiveRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new SetPromotionActiveCommand(id, request.IsActive), + cancellationToken); + return NoContent(); + } + + [HttpGet("promotions/{id:guid}/codes")] + [Authorize(Roles = "Admin")] + public async Task GetPromotionCodes( + Guid id, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + CancellationToken cancellationToken = default) + { + var result = await mediator.Send( + new GetPromotionCodesQuery(id, page, pageSize), + cancellationToken); + return Ok(result); + } + + [HttpPost("promotions/{id:guid}/codes")] + [Authorize(Roles = "Admin")] + public async Task CreatePromotionCode( + Guid id, + [FromBody] CreateCodeRequest request, + CancellationToken cancellationToken) + { + var code = await mediator.Send( + new CreatePromotionCodeCommand(id, request.Code, request.MaxUsages), + cancellationToken); + return Ok(new { code }); + } + + [HttpPut("discount-codes/{id:guid}/active")] + [Authorize(Roles = "Admin")] + public async Task SetDiscountCodeActive( + Guid id, + [FromBody] SetActiveRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new SetPromotionCodeActiveCommand(id, request.IsActive), + cancellationToken); + return NoContent(); + } + + // Legacy endpoints kept for older admin builds + [HttpPut("banner")] + [Authorize(Roles = "Admin")] + public async Task UpdateBanner( + [FromBody] UpdatePromoBannerCommand command, + CancellationToken cancellationToken) + { + await mediator.Send(command, cancellationToken); + return NoContent(); + } + + [HttpPost("discount-rules")] + [Authorize(Roles = "Admin")] + public async Task CreateDiscountRule( + [FromBody] CreateDiscountRuleCommand command, + CancellationToken cancellationToken) + { + var id = await mediator.Send(command, cancellationToken); + return Ok(id); + } + + [HttpPost("discount-codes")] + [Authorize(Roles = "Admin")] + public async Task CreateDiscountCode( + [FromBody] CreateDiscountCodeCommand command, + CancellationToken cancellationToken) + { + var code = await mediator.Send( + new CreatePromotionCodeCommand( + command.DiscountRuleId, + command.Code, + command.MaxUsages), + cancellationToken); + return Ok(code); + } + + [HttpGet("discount-codes")] + [Authorize(Roles = "Admin")] + public async Task GetDiscountCodes( + [FromQuery] GetDiscountCodesQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpGet("subscribers")] + [Authorize(Roles = "Admin")] + public async Task GetSubscribers( + [FromQuery] GetSubscribersQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpPost("campaigns")] + [Authorize(Roles = "Admin")] + public async Task CreateCampaign( + [FromBody] CreateCampaignCommand command, + CancellationToken cancellationToken) + { + var id = await mediator.Send(command, cancellationToken); + return Ok(id); + } + + [HttpGet("campaigns")] + [Authorize(Roles = "Admin")] + public async Task GetCampaigns( + [FromQuery] GetCampaignsQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpPost("campaigns/{campaignId:guid}/send")] + [Authorize(Roles = "Admin")] + public async Task SendCampaign( + Guid campaignId, + CancellationToken cancellationToken) + { + var sent = await mediator.Send( + new SendCampaignCommand(campaignId), + cancellationToken); + return Ok(new { sent }); + } +} + +public sealed record SubscribeRequest(string Email); + +public sealed record ValidateDiscountRequest( + string Code, + decimal Subtotal = 0, + string? CustomerEmail = null); + +public sealed record SetActiveRequest(bool IsActive); + +public sealed record CreateCodeRequest(string? Code, int? MaxUsages); + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/reviews")] +public sealed class ReviewsController(IMediator mediator) : ControllerBase +{ + [HttpGet("product/{slug}")] + public async Task GetForProduct( + string slug, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetProductReviewsQuery(slug), cancellationToken); + return Ok(result); + } + + [HttpPost] + public async Task Create( + [FromBody] CreateReviewCommand command, + CancellationToken cancellationToken) + { + var id = await mediator.Send(command, cancellationToken); + return Ok(id); + } + + [HttpGet("pending")] + [Authorize(Roles = "Admin")] + public async Task GetPending(CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetPendingReviewsQuery(), cancellationToken); + return Ok(result); + } + + [HttpGet] + [Authorize(Roles = "Admin")] + public async Task GetAdminList( + [FromQuery] GetAdminReviewsQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpPut("{reviewId:guid}/approve")] + [Authorize(Roles = "Admin")] + public async Task Approve( + Guid reviewId, + [FromBody] ApproveRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new ApproveReviewCommand(reviewId, request.IsApproved), + cancellationToken); + return NoContent(); + } +} + +public sealed record ApproveRequest(bool IsApproved); diff --git a/src/backend/src/iLED.Api/Controllers/OrdersController.cs b/src/backend/src/iLED.Api/Controllers/OrdersController.cs new file mode 100644 index 0000000..d83f9f0 --- /dev/null +++ b/src/backend/src/iLED.Api/Controllers/OrdersController.cs @@ -0,0 +1,187 @@ +using Asp.Versioning; +using iLED.Application.Orders.Commands; +using iLED.Application.Payments.Commands; +using iLED.Domain.Enums; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace iLED.Api.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/orders")] +public sealed class OrdersController(IMediator mediator) : ControllerBase +{ + [HttpPost] + public async Task Create( + [FromBody] CreateOrderCommand command, + CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return Ok(result); + } + + [HttpGet("{orderId:guid}")] + public async Task Get( + Guid orderId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetOrderQuery(orderId), cancellationToken); + return Ok(result); + } + + [HttpGet("my")] + [Authorize] + public async Task GetMyOrders(CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetUserOrdersQuery(), cancellationToken); + return Ok(result); + } + + [HttpGet("admin")] + [Authorize(Roles = "Admin")] + public async Task GetAdminOrders( + [FromQuery] GetAdminOrdersQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpGet("admin/pick-list")] + [Authorize(Roles = "Admin")] + public async Task GetPickList( + [FromQuery] GetPickListQuery query, + CancellationToken cancellationToken) + { + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + [HttpGet("admin/{orderId:guid}")] + [Authorize(Roles = "Admin")] + public async Task GetAdminOrderDetail( + Guid orderId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetAdminOrderDetailQuery(orderId), + cancellationToken); + return Ok(result); + } + + [HttpGet("admin/{orderId:guid}/invoice")] + [Authorize(Roles = "Admin")] + public async Task GetInvoice( + Guid orderId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetOrderInvoiceQuery(orderId), + cancellationToken); + return Ok(result); + } + + [HttpPut("{orderId:guid}/status")] + [Authorize(Roles = "Admin")] + public async Task UpdateStatus( + Guid orderId, + [FromBody] UpdateStatusRequest request, + CancellationToken cancellationToken) + { + await mediator.Send( + new UpdateOrderStatusCommand(orderId, request.Status), + cancellationToken); + return NoContent(); + } + [HttpPost("admin/lines/{orderLineId:guid}/collect")] + [Authorize(Roles = "Admin")] + public async Task CollectOrderLine( + Guid orderLineId, + [FromBody] CollectOrderLineBody request, + CancellationToken cancellationToken) + { + await mediator.Send( + new CollectOrderLineCommand(orderLineId, request.WarehouseId), + cancellationToken); + return NoContent(); + } + + [HttpPost("admin/{orderId:guid}/collect")] + [Authorize(Roles = "Admin")] + public async Task CollectOrder( + Guid orderId, + [FromBody] CollectOrderBody request, + CancellationToken cancellationToken) + { + await mediator.Send( + new CollectOrderCommand(orderId, request.Lines), + cancellationToken); + return NoContent(); + } +} + +public sealed record CollectOrderLineBody(Guid WarehouseId); + +public sealed record CollectOrderBody( + IReadOnlyList Lines); + +public sealed record UpdateStatusRequest(OrderStatus Status); + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/payments")] +public sealed class PaymentsController(IMediator mediator) : ControllerBase +{ + [HttpPost("{orderId:guid}/initiate")] + public async Task Initiate( + Guid orderId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new InitiatePaymentCommand(orderId), cancellationToken); + return Ok(result); + } + + [HttpPost("{orderId:guid}/sandbox-complete")] + public async Task SandboxComplete( + Guid orderId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new CompleteSandboxPaymentCommand(orderId), + cancellationToken); + return result.Success ? Ok(result) : BadRequest(result); + } + + /// + /// Pulls payment status from PayU (needed when webhooks cannot reach localhost). + /// + [HttpPost("{orderId:guid}/sync")] + public async Task Sync( + Guid orderId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new SyncPaymentStatusCommand(orderId), + cancellationToken); + return result.Success ? Ok(result) : BadRequest(result); + } + + [HttpPost("webhook")] + public async Task Webhook( + CancellationToken cancellationToken) + { + using var reader = new StreamReader(Request.Body); + var payload = await reader.ReadToEndAsync(cancellationToken); + var signature = Request.Headers["OpenPayu-Signature"].FirstOrDefault() ?? string.Empty; + + await mediator.Send( + new ProcessPaymentWebhookCommand(payload, signature), + cancellationToken); + return Ok(); + } +} diff --git a/src/backend/src/iLED.Api/Controllers/SeoController.cs b/src/backend/src/iLED.Api/Controllers/SeoController.cs new file mode 100644 index 0000000..7d15f0f --- /dev/null +++ b/src/backend/src/iLED.Api/Controllers/SeoController.cs @@ -0,0 +1,62 @@ +using System.Text; +using System.Xml.Linq; +using Asp.Versioning; +using iLED.Application.Common.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace iLED.Api.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/seo")] +public sealed class SeoController( + IAppDbContext db, + IConfiguration configuration) : ControllerBase +{ + [HttpGet("sitemap.xml")] + [Produces("application/xml")] + public async Task Sitemap(CancellationToken cancellationToken) + { + var shopBase = configuration["Seo:ShopBaseUrl"] ?? "http://localhost:4200"; + shopBase = shopBase.TrimEnd('/'); + + var ns = XNamespace.Get("http://www.sitemaps.org/schemas/sitemap/0.9"); + var urls = new List + { + SitemapUrl(ns, $"{shopBase}/", "1.0", "daily"), + SitemapUrl(ns, $"{shopBase}/products", "0.9", "daily"), + SitemapUrl(ns, $"{shopBase}/privacy", "0.3", "yearly"), + SitemapUrl(ns, $"{shopBase}/terms", "0.3", "yearly"), + }; + + var slugs = await db.Products + .Where(p => p.IsActive) + .Select(p => p.Slug) + .ToListAsync(cancellationToken); + + foreach (var slug in slugs) + { + urls.Add( + SitemapUrl(ns, $"{shopBase}/products/{slug}", "0.8", "weekly")); + } + + var doc = new XDocument( + new XDeclaration("1.0", "utf-8", null), + new XElement(ns + "urlset", urls)); + + return Content(doc.ToString(), "application/xml", Encoding.UTF8); + } + + private static XElement SitemapUrl( + XNamespace ns, + string loc, + string priority, + string changefreq) => + new( + ns + "url", + new XElement(ns + "loc", loc), + new XElement(ns + "changefreq", changefreq), + new XElement(ns + "priority", priority)); +} diff --git a/src/backend/src/iLED.Api/Controllers/ShippingController.cs b/src/backend/src/iLED.Api/Controllers/ShippingController.cs new file mode 100644 index 0000000..31a2442 --- /dev/null +++ b/src/backend/src/iLED.Api/Controllers/ShippingController.cs @@ -0,0 +1,65 @@ +using Asp.Versioning; +using iLED.Application.Shipping.Commands; +using iLED.Domain.Enums; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace iLED.Api.Controllers; + +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/shipping")] +public sealed class ShippingController(IMediator mediator) : ControllerBase +{ + [HttpGet("parcel-points")] + public async Task SearchParcelPoints( + [FromQuery] ShippingCarrier carrier, + [FromQuery] string? query = null, + [FromQuery] double? latitude = null, + [FromQuery] double? longitude = null, + CancellationToken cancellationToken = default) + { + var result = await mediator.Send( + new SearchParcelPointsQuery( + carrier, + query ?? string.Empty, + latitude, + longitude), + cancellationToken); + return Ok(result); + } + + [HttpGet("config")] + public async Task GetConfig(CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetShippingWidgetConfigQuery(), + cancellationToken); + return Ok(result); + } + + [HttpPost("orders/{orderId:guid}/label")] + [Authorize(Roles = "Admin")] + public async Task CreateLabel( + Guid orderId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new CreateShipmentLabelCommand(orderId), + cancellationToken); + return Ok(result); + } + + [HttpGet("orders/{orderId:guid}")] + [Authorize(Roles = "Admin")] + public async Task GetShipment( + Guid orderId, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new GetOrderShipmentQuery(orderId), + cancellationToken); + return result is null ? NotFound() : Ok(result); + } +} diff --git a/src/backend/src/iLED.Api/OpenTelemetrySetup.cs b/src/backend/src/iLED.Api/OpenTelemetrySetup.cs new file mode 100644 index 0000000..dbaaf2f --- /dev/null +++ b/src/backend/src/iLED.Api/OpenTelemetrySetup.cs @@ -0,0 +1,61 @@ +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace iLED.Api; + +internal static class OpenTelemetrySetup +{ + public static void AddIledOpenTelemetry( + this IServiceCollection services, + IConfiguration configuration, + ILoggingBuilder logging) + { + var endpoint = configuration["OpenTelemetry:OtlpEndpoint"]; + if (string.IsNullOrWhiteSpace(endpoint)) + { + return; + } + + var serviceName = configuration["OpenTelemetry:ServiceName"] ?? "iled-api"; + var headers = configuration["OpenTelemetry:OtlpHeaders"]; + var resource = ResourceBuilder.CreateDefault().AddService(serviceName); + + void ConfigureOtlp(OpenTelemetry.Exporter.OtlpExporterOptions o) + { + o.Endpoint = new Uri(endpoint); + o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf; + if (!string.IsNullOrWhiteSpace(headers)) + { + o.Headers = headers; + } + } + + services.AddOpenTelemetry() + .ConfigureResource(r => r.AddService(serviceName)) + .WithTracing(tracing => + { + tracing + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(ConfigureOtlp); + }) + .WithMetrics(metrics => + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddOtlpExporter(ConfigureOtlp); + }); + + logging.AddOpenTelemetry(options => + { + options.SetResourceBuilder(resource); + options.IncludeFormattedMessage = true; + options.IncludeScopes = true; + options.AddOtlpExporter(ConfigureOtlp); + }); + } +} diff --git a/src/backend/src/iLED.Api/Program.cs b/src/backend/src/iLED.Api/Program.cs new file mode 100644 index 0000000..43bd02e --- /dev/null +++ b/src/backend/src/iLED.Api/Program.cs @@ -0,0 +1,113 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Json.Serialization; +using Asp.Versioning; +using iLED.Api; +using iLED.Application; +using iLED.Application.Common.Exceptions; +using iLED.Infrastructure; +using iLED.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddApplication(); +builder.Services.AddInfrastructure(builder.Configuration); +builder.Services.AddIledOpenTelemetry( + builder.Configuration, + builder.Logging); + +var jwtSecret = builder.Configuration["Jwt:Secret"] ?? + "iLED-Super-Secret-Key-Change-In-Production-2026!"; +var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "iLED"; +var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "iLED"; + +builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.MapInboundClaims = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtIssuer, + ValidAudience = jwtAudience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtSecret)), + NameClaimType = ClaimTypes.NameIdentifier, + RoleClaimType = ClaimTypes.Role, + }; + }); + +builder.Services.AddAuthorization(); +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = + System.Text.Json.JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.Converters.Add( + new JsonStringEnumConverter( + System.Text.Json.JsonNamingPolicy.CamelCase)); + }); +builder.Services.AddApiVersioning(options => +{ + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ReportApiVersions = true; +}).AddMvc(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("ShopCors", policy => + { + policy.WithOrigins( + builder.Configuration.GetSection("Cors:Origins").Get() ?? + ["http://localhost:4200", "http://localhost:4201"]) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); + +builder.Services.AddHealthChecks() + .AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")!) + .AddRedis(builder.Configuration.GetConnectionString("Redis")!); + +var app = builder.Build(); + +app.Use(async (context, next) => +{ + try + { + await next(); + } + catch (ValidationException ex) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync(new + { + message = ex.Message, + errorCode = ex.ErrorCode, + minOrderAmount = ex.MinOrderAmount, + }); + } +}); + +app.UseCors("ShopCors"); +app.UseStaticFiles(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +app.MapHealthChecks("/health"); + +await SeedData.InitializeAsync(app.Services); + +app.Run(); diff --git a/src/backend/src/iLED.Api/Properties/launchSettings.json b/src/backend/src/iLED.Api/Properties/launchSettings.json new file mode 100644 index 0000000..5d0b36b --- /dev/null +++ b/src/backend/src/iLED.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7067;http://localhost:5023", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/backend/src/iLED.Api/appsettings.Development.json b/src/backend/src/iLED.Api/appsettings.Development.json new file mode 100644 index 0000000..0988dbf --- /dev/null +++ b/src/backend/src/iLED.Api/appsettings.Development.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "PayU": { + "UseSandbox": true, + "PosId": "300746", + "ClientId": "300746", + "ClientSecret": "2ee86a66e5d97e3fadc400c9f19b065d", + "SecondKey": "b6ca15b0d1020e8094d9b5f8d163db54", + "OAuthUrl": "https://secure.snd.payu.com/pl/standard/user/oauth/authorize", + "OrdersUrl": "https://secure.snd.payu.com/api/v2_1/orders", + "NotifyUrl": "http://localhost:5000/api/v1/payments/webhook", + "ContinueUrl": "http://localhost:4200/order", + "StatusPollEnabled": true, + "StatusPollIntervalSeconds": 30 + }, + "Shipping": { + "UseSandbox": true + } +} diff --git a/src/backend/src/iLED.Api/appsettings.json b/src/backend/src/iLED.Api/appsettings.json new file mode 100644 index 0000000..1c827ec --- /dev/null +++ b/src/backend/src/iLED.Api/appsettings.json @@ -0,0 +1,78 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5433;Database=iled;Username=iled;Password=iled123", + "Redis": "localhost:6379" + }, + "Jwt": { + "Issuer": "iLED", + "Audience": "iLED", + "Secret": "iLED-Super-Secret-Key-Change-In-Production-2026!", + "ExpiryMinutes": 60 + }, + "Seo": { + "ShopBaseUrl": "http://localhost:4200" + }, + "OpenTelemetry": { + "ServiceName": "iled-api", + "OtlpEndpoint": "", + "OtlpHeaders": "" + }, + "PayU": { + "UseSandbox": true, + "PosId": "", + "ClientId": "", + "ClientSecret": "", + "SecondKey": "", + "OAuthUrl": "https://secure.snd.payu.com/pl/standard/user/oauth/authorize", + "OrdersUrl": "https://secure.snd.payu.com/api/v2_1/orders", + "NotifyUrl": "http://localhost:5000/api/v1/payments/webhook", + "ContinueUrl": "http://localhost:4200/order", + "StatusPollEnabled": true, + "StatusPollIntervalSeconds": 60 + }, + "Shipping": { + "UseSandbox": true, + "InPost": { + "GeowidgetPublicToken": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkVzROZW9TeXk0OHpCOHg4emdZX2t5dFNiWHY3blZ0eFVGVFpzWV9TUFA4In0.eyJleHAiOjIxMDY4ODEyMzAsImlhdCI6MTc5MTUyMTIzMCwianRpIjoiZTY2YWVhOTEtYzcxYi00ZDUyLWE5YzQtZjhjMzljNjZiNWMxIiwiaXNzIjoiaHR0cHM6Ly9zYW5kYm94LWxvZ2luLmlucG9zdC5wbC9hdXRoL3JlYWxtcy9leHRlcm5hbCIsInN1YiI6ImY6N2ZiZjQxYmEtYTEzZC00MGQzLTk1ZjYtOThhMmIxYmFlNjdiOmp4ZG9nQzRMenNNbUM2Q1NrMnRKUmF5TVRkUXUyeTVVcWNkekloajZBNUEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzaGlweCIsInNlc3Npb25fc3RhdGUiOiI5YjU4OWQzZi00YWNmLTQ4MDItODMzNC1kNmI4NzE1MWZmMTciLCJzY29wZSI6Im9wZW5pZCBhcGk6YXBpcG9pbnRzIiwic2lkIjoiOWI1ODlkM2YtNGFjZi00ODAyLTgzMzQtZDZiODcxNTFmZjE3IiwiYWxsb3dlZF9yZWZlcnJlcnMiOiIiLCJ1dWlkIjoiMzU2Yjg5YjYtMWRmNi00YzMxLTlkNjEtNWNmMzY4Y2M5YjVjIn0.cwP_pwLtg8CZlwvT7dCMEcmM4Zl1gO82nPAuyS_UJzq8rF6aiIjOp23Q_32sGftQAy4oDBG08xQQsrN24FQCaLbJRCM5-dFOqEkarqAtWlIt2kCYzvgPSbejVnIuTFC71mLyMA7IVLu2ucCiRB93u5xal3pjRM7CYADFW35RsYasrO5g7tm-2IlqWW8Qb_7r2XAgpqSM8I0stRMEvHgD46EM2b3TAlK-x42a_We9gVGKpEH5VQrPuEJd7VBjFDZWZoRebVhuwn0cv5Utm6roDexsaXyKM7coqOBfQfD0OhQz2yW3T_OkXnAwne5WrwPf8kG0fBE7JBPA5Px98IVTZw", + "GeowidgetSandboxToken": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkVzROZW9TeXk0OHpCOHg4emdZX2t5dFNiWHY3blZ0eFVGVFpzWV9TUFA4In0.eyJleHAiOjIxMDY4ODEyMzAsImlhdCI6MTc5MTUyMTIzMCwianRpIjoiZTY2YWVhOTEtYzcxYi00ZDUyLWE5YzQtZjhjMzljNjZiNWMxIiwiaXNzIjoiaHR0cHM6Ly9zYW5kYm94LWxvZ2luLmlucG9zdC5wbC9hdXRoL3JlYWxtcy9leHRlcm5hbCIsInN1YiI6ImY6N2ZiZjQxYmEtYTEzZC00MGQzLTk1ZjYtOThhMmIxYmFlNjdiOmp4ZG9nQzRMenNNbUM2Q1NrMnRKUmF5TVRkUXUyeTVVcWNkekloajZBNUEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzaGlweCIsInNlc3Npb25fc3RhdGUiOiI5YjU4OWQzZi00YWNmLTQ4MDItODMzNC1kNmI4NzE1MWZmMTciLCJzY29wZSI6Im9wZW5pZCBhcGk6YXBpcG9pbnRzIiwic2lkIjoiOWI1ODlkM2YtNGFjZi00ODAyLTgzMzQtZDZiODcxNTFmZjE3IiwiYWxsb3dlZF9yZWZlcnJlcnMiOiIiLCJ1dWlkIjoiMzU2Yjg5YjYtMWRmNi00YzMxLTlkNjEtNWNmMzY4Y2M5YjVjIn0.cwP_pwLtg8CZlwvT7dCMEcmM4Zl1gO82nPAuyS_UJzq8rF6aiIjOp23Q_32sGftQAy4oDBG08xQQsrN24FQCaLbJRCM5-dFOqEkarqAtWlIt2kCYzvgPSbejVnIuTFC71mLyMA7IVLu2ucCiRB93u5xal3pjRM7CYADFW35RsYasrO5g7tm-2IlqWW8Qb_7r2XAgpqSM8I0stRMEvHgD46EM2b3TAlK-x42a_We9gVGKpEH5VQrPuEJd7VBjFDZWZoRebVhuwn0cv5Utm6roDexsaXyKM7coqOBfQfD0OhQz2yW3T_OkXnAwne5WrwPf8kG0fBE7JBPA5Px98IVTZw", + "GeowidgetConfig": "parcelCollect", + "GeowidgetSdkBaseUrl": "https://sandbox-easy-geowidget-sdk.easypack24.net", + "ShipXBaseUrl": "https://sandbox-api-shipx-pl.easypack24.net", + "ShipXToken": "", + "OrganizationId": "" + }, + "Dhl": { + "WidgetEnabled": true, + "WidgetUrl": "https://parcelshop.dhl.pl/mapa", + "ApiBaseUrl": "https://api-sandbox.dhl.com/parcel/de/shipping/v2", + "ApiKey": "", + "ApiSecret": "" + }, + "Dpd": { + "WidgetEnabled": true, + "WidgetUrl": "https://pudofinder.dpd.com.pl/", + "ApiBaseUrl": "", + "Login": "", + "Password": "", + "Fid": "" + }, + "Labels": { + "RelativePath": "uploads/labels" + } + }, + "Cors": { + "Origins": [ + "http://localhost:4200", + "http://localhost:4201", + "http://127.0.0.1:4200", + "http://127.0.0.1:4201" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/backend/src/iLED.Api/iLED.Api.csproj b/src/backend/src/iLED.Api/iLED.Api.csproj new file mode 100644 index 0000000..224ff65 --- /dev/null +++ b/src/backend/src/iLED.Api/iLED.Api.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/src/backend/src/iLED.Api/iLED.Api.http b/src/backend/src/iLED.Api/iLED.Api.http new file mode 100644 index 0000000..ae69c8a --- /dev/null +++ b/src/backend/src/iLED.Api/iLED.Api.http @@ -0,0 +1,6 @@ +@iLED.Api_HostAddress = http://localhost:5023 + +GET {{iLED.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/backend/src/iLED.Api/wwwroot/uploads/products/.gitkeep b/src/backend/src/iLED.Api/wwwroot/uploads/products/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/backend/src/iLED.Api/wwwroot/uploads/products/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/backend/src/iLED.Api/wwwroot/uploads/products/429662dcb19c45a9923cc389e71a525a.webp b/src/backend/src/iLED.Api/wwwroot/uploads/products/429662dcb19c45a9923cc389e71a525a.webp new file mode 100644 index 0000000..34e5d97 Binary files /dev/null and b/src/backend/src/iLED.Api/wwwroot/uploads/products/429662dcb19c45a9923cc389e71a525a.webp differ diff --git a/src/backend/src/iLED.Api/wwwroot/uploads/products/8f0c93281bb54d44a326efd7eeb0dc33.webp b/src/backend/src/iLED.Api/wwwroot/uploads/products/8f0c93281bb54d44a326efd7eeb0dc33.webp new file mode 100644 index 0000000..70aba87 Binary files /dev/null and b/src/backend/src/iLED.Api/wwwroot/uploads/products/8f0c93281bb54d44a326efd7eeb0dc33.webp differ diff --git a/src/backend/src/iLED.Api/wwwroot/uploads/products/a95b243e5f624710ac8e7d044689c12f.webp b/src/backend/src/iLED.Api/wwwroot/uploads/products/a95b243e5f624710ac8e7d044689c12f.webp new file mode 100644 index 0000000..34e5d97 Binary files /dev/null and b/src/backend/src/iLED.Api/wwwroot/uploads/products/a95b243e5f624710ac8e7d044689c12f.webp differ diff --git a/src/backend/src/iLED.Api/wwwroot/uploads/products/c1840b3121884e6fa329be9b5ede8ba8.webp b/src/backend/src/iLED.Api/wwwroot/uploads/products/c1840b3121884e6fa329be9b5ede8ba8.webp new file mode 100644 index 0000000..a8f7ec9 Binary files /dev/null and b/src/backend/src/iLED.Api/wwwroot/uploads/products/c1840b3121884e6fa329be9b5ede8ba8.webp differ diff --git a/src/backend/src/iLED.Application/Cart/Commands/CartCommands.cs b/src/backend/src/iLED.Application/Cart/Commands/CartCommands.cs new file mode 100644 index 0000000..edf4f3d --- /dev/null +++ b/src/backend/src/iLED.Application/Cart/Commands/CartCommands.cs @@ -0,0 +1,224 @@ +using iLED.Application.Common.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Cart.Commands; + +public sealed record CartItemDto( + Guid Id, + Guid ProductId, + Guid VariantId, + string ProductName, + string ProductSlug, + string VariantColor, + decimal UnitPrice, + int Quantity, + string? ImageUrl, + bool IsInStock, + decimal LineTotal); + +public sealed record CartDto( + Guid Id, + IReadOnlyList Items, + decimal Subtotal, + int ItemCount); + +public interface ICartService +{ + Task GetCartAsync(CancellationToken cancellationToken); + + Task AddItemAsync( + Guid productId, + Guid variantId, + int quantity, + CancellationToken cancellationToken); + + Task UpdateItemAsync( + Guid itemId, + int quantity, + CancellationToken cancellationToken); + + Task RemoveItemAsync( + Guid itemId, + CancellationToken cancellationToken); + + Task MergeGuestCartAsync( + string sessionId, + string userId, + CancellationToken cancellationToken); +} + +public sealed record GetCartQuery() : IRequest; + +public sealed class GetCartQueryHandler(ICartService cart) + : IRequestHandler +{ + public Task Handle( + GetCartQuery request, + CancellationToken cancellationToken) => + cart.GetCartAsync(cancellationToken); +} + +public sealed record AddToCartCommand( + Guid ProductId, + Guid VariantId, + int Quantity = 1) : IRequest; + +public sealed class AddToCartCommandHandler(ICartService cart) + : IRequestHandler +{ + public Task Handle( + AddToCartCommand request, + CancellationToken cancellationToken) => + cart.AddItemAsync( + request.ProductId, + request.VariantId, + request.Quantity, + cancellationToken); +} + +public sealed record UpdateCartItemCommand(Guid ItemId, int Quantity) + : IRequest; + +public sealed class UpdateCartItemCommandHandler(ICartService cart) + : IRequestHandler +{ + public Task Handle( + UpdateCartItemCommand request, + CancellationToken cancellationToken) => + cart.UpdateItemAsync(request.ItemId, request.Quantity, cancellationToken); +} + +public sealed record RemoveCartItemCommand(Guid ItemId) : IRequest; + +public sealed class RemoveCartItemCommandHandler(ICartService cart) + : IRequestHandler +{ + public Task Handle( + RemoveCartItemCommand request, + CancellationToken cancellationToken) => + cart.RemoveItemAsync(request.ItemId, cancellationToken); +} + +public sealed record FavoriteDto( + Guid Id, + Guid ProductId, + string ProductName, + string ProductSlug, + string ShortDescription, + decimal Price, + string? ImageUrl, + bool IsInStock); + +public sealed record GetFavoritesQuery() : IRequest>; + +public sealed class GetFavoritesQueryHandler( + IAppDbContext db, + ICurrentUserService currentUser) + : IRequestHandler> +{ + public async Task> Handle( + GetFavoritesQuery request, + CancellationToken cancellationToken) + { + if (currentUser.UserId is null) + { + return []; + } + + var locale = currentUser.Locale; + var rows = await db.FavoriteItems + .AsNoTracking() + .Where(f => f.UserId == currentUser.UserId) + .Select(f => new + { + f.Id, + f.ProductId, + f.Product.Name, + f.Product.Names, + f.Product.Slug, + f.Product.ShortDescription, + f.Product.ShortDescriptions, + f.Product.BasePrice, + ImageUrl = f.Product.Images + .OrderBy(i => i.SortOrder) + .Select(i => i.Url) + .FirstOrDefault(), + IsInStock = f.Product.Variants.Any(v => v.Stock.Quantity > 0), + }) + .ToListAsync(cancellationToken); + + return rows.Select(f => new FavoriteDto( + f.Id, + f.ProductId, + f.Names.Resolve(locale, f.Name), + f.Slug, + f.ShortDescriptions.Resolve(locale, f.ShortDescription), + f.BasePrice, + f.ImageUrl, + f.IsInStock)).ToList(); + } +} + +public sealed record AddFavoriteCommand(Guid ProductId) : IRequest; + +public sealed class AddFavoriteCommandHandler( + IAppDbContext db, + ICurrentUserService currentUser) + : IRequestHandler +{ + public async Task Handle( + AddFavoriteCommand request, + CancellationToken cancellationToken) + { + if (currentUser.UserId is null) + { + throw new Common.Exceptions.ValidationException( + "Musisz być zalogowany."); + } + + var exists = await db.FavoriteItems.AnyAsync( + f => f.UserId == currentUser.UserId && + f.ProductId == request.ProductId, + cancellationToken); + + if (!exists) + { + db.Add(new Domain.Cart.FavoriteItem + { + UserId = currentUser.UserId, + ProductId = request.ProductId, + }); + await db.SaveChangesAsync(cancellationToken); + } + } +} + +public sealed record RemoveFavoriteCommand(Guid ProductId) : IRequest; + +public sealed class RemoveFavoriteCommandHandler( + IAppDbContext db, + ICurrentUserService currentUser) + : IRequestHandler +{ + public async Task Handle( + RemoveFavoriteCommand request, + CancellationToken cancellationToken) + { + if (currentUser.UserId is null) + { + return; + } + + var item = await db.FavoriteItems.FirstOrDefaultAsync( + f => f.UserId == currentUser.UserId && + f.ProductId == request.ProductId, + cancellationToken); + + if (item is not null) + { + db.Remove(item); + await db.SaveChangesAsync(cancellationToken); + } + } +} diff --git a/src/backend/src/iLED.Application/Catalog/Commands/CatalogCommands.cs b/src/backend/src/iLED.Application/Catalog/Commands/CatalogCommands.cs new file mode 100644 index 0000000..26d3761 --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Commands/CatalogCommands.cs @@ -0,0 +1,437 @@ +using iLED.Application.Catalog; +using iLED.Application.Catalog.Dtos; +using iLED.Application.Common.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Catalog.Commands; + +public sealed record CreateProductCommand( + string Slug, + LocalizedTextDto Names, + LocalizedTextDto ShortDescriptions, + LocalizedTextDto Descriptions, + decimal BasePrice, + decimal LowestPrice30Days, + string ProductCode, + string Model, + string? Ean, + Guid CategoryId, + Guid? ProductGroupId, + bool IsBestseller, + bool IsNewArrival, + int DeliveryHours, + IReadOnlyList Variants, + IReadOnlyList Attributes) : IRequest; + +public sealed record CreateVariantDto( + string Sku, + string ColorName, + string ColorHex, + decimal PriceAdjustment, + int Quantity, + bool IsDefault, + IReadOnlyList? WarehouseQuantities = null); + +public sealed record WarehouseQuantityDto(Guid WarehouseId, int Quantity); + +public sealed record CreateAttributeDto( + LocalizedTextDto Names, + LocalizedTextDto Values, + string? FilterGroup); + +public sealed class CreateProductCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreateProductCommand request, + CancellationToken cancellationToken) + { + var product = new Domain.Catalog.Product + { + Slug = request.Slug, + BasePrice = request.BasePrice, + LowestPrice30Days = request.LowestPrice30Days, + ProductCode = request.ProductCode, + Model = request.Model, + Ean = request.Ean, + CategoryId = request.CategoryId, + ProductGroupId = request.ProductGroupId, + IsBestseller = request.IsBestseller, + IsNewArrival = request.IsNewArrival, + DeliveryHours = request.DeliveryHours, + }; + + ProductLocalization.ApplyToProduct( + product, + request.Names, + request.ShortDescriptions, + request.Descriptions); + + foreach (var v in request.Variants) + { + var variant = new Domain.Catalog.ProductVariant + { + Sku = v.Sku, + ColorName = v.ColorName, + ColorHex = v.ColorHex, + PriceAdjustment = v.PriceAdjustment, + IsDefault = v.IsDefault, + Stock = new Domain.Catalog.Stock { Quantity = 0 }, + }; + + if (v.WarehouseQuantities is { Count: > 0 }) + { + foreach (var wq in v.WarehouseQuantities) + { + variant.WarehouseStocks.Add(new Domain.Catalog.WarehouseStock + { + WarehouseId = wq.WarehouseId, + Quantity = wq.Quantity, + }); + } + + variant.Stock.Quantity = v.WarehouseQuantities.Sum(wq => wq.Quantity); + } + else + { + variant.Stock.Quantity = v.Quantity; + } + + product.Variants.Add(variant); + } + + foreach (var a in request.Attributes) + { + var attribute = new Domain.Catalog.ProductAttribute + { + FilterGroup = a.FilterGroup, + }; + ProductLocalization.ApplyToAttribute(attribute, a.Names, a.Values); + product.Attributes.Add(attribute); + } + + db.Add(product); + await db.SaveChangesAsync(cancellationToken); + + await AssignDefaultWarehouseStockAsync(db, product, cancellationToken); + + return product.Id; + } + + private static async Task AssignDefaultWarehouseStockAsync( + IAppDbContext db, + Domain.Catalog.Product product, + CancellationToken cancellationToken) + { + var mainWarehouse = await db.Warehouses + .FirstOrDefaultAsync(w => w.Code == "MAIN", cancellationToken); + + if (mainWarehouse is null) + { + return; + } + + foreach (var variant in product.Variants) + { + if (variant.WarehouseStocks.Count > 0) + { + continue; + } + + db.Add(new Domain.Catalog.WarehouseStock + { + ProductVariantId = variant.Id, + WarehouseId = mainWarehouse.Id, + Quantity = variant.Stock.Quantity, + }); + } + + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record UpdateWarehouseStockCommand( + Guid VariantId, + IReadOnlyList Quantities) : IRequest; + +public sealed class UpdateWarehouseStockCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + UpdateWarehouseStockCommand request, + CancellationToken cancellationToken) + { + var variant = await db.ProductVariants + .Include(v => v.Stock) + .Include(v => v.WarehouseStocks) + .FirstOrDefaultAsync(v => v.Id == request.VariantId, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Variant not found."); + + foreach (var entry in request.Quantities) + { + var existing = variant.WarehouseStocks + .FirstOrDefault(ws => ws.WarehouseId == entry.WarehouseId); + + if (existing is not null) + { + existing.Quantity = entry.Quantity; + existing.UpdatedAt = DateTime.UtcNow; + } + else + { + variant.WarehouseStocks.Add(new Domain.Catalog.WarehouseStock + { + WarehouseId = entry.WarehouseId, + Quantity = entry.Quantity, + }); + } + } + + variant.Stock.Quantity = variant.WarehouseStocks.Sum(ws => ws.Quantity); + variant.Stock.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record UpdateProductCommand( + Guid Id, + string Slug, + LocalizedTextDto Names, + LocalizedTextDto ShortDescriptions, + LocalizedTextDto Descriptions, + decimal BasePrice, + decimal LowestPrice30Days, + string ProductCode, + string Model, + string? Ean, + Guid CategoryId, + Guid? ProductGroupId, + bool IsBestseller, + bool IsNewArrival, + bool IsActive, + int DeliveryHours, + IReadOnlyList Attributes) : IRequest; + +public sealed class UpdateProductCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + UpdateProductCommand request, + CancellationToken cancellationToken) + { + var product = await db.Products + .FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Product not found."); + + product.Slug = request.Slug; + product.BasePrice = request.BasePrice; + product.LowestPrice30Days = request.LowestPrice30Days; + product.ProductCode = request.ProductCode; + product.Model = request.Model; + product.Ean = request.Ean; + product.CategoryId = request.CategoryId; + product.ProductGroupId = request.ProductGroupId; + product.IsBestseller = request.IsBestseller; + product.IsNewArrival = request.IsNewArrival; + product.IsActive = request.IsActive; + product.DeliveryHours = request.DeliveryHours; + product.UpdatedAt = DateTime.UtcNow; + + ProductLocalization.ApplyToProduct( + product, + request.Names, + request.ShortDescriptions, + request.Descriptions); + + await db.ProductAttributes + .Where(a => a.ProductId == request.Id) + .ExecuteDeleteAsync(cancellationToken); + + foreach (var a in request.Attributes) + { + var attribute = new Domain.Catalog.ProductAttribute + { + ProductId = product.Id, + FilterGroup = a.FilterGroup, + }; + ProductLocalization.ApplyToAttribute(attribute, a.Names, a.Values); + db.Add(attribute); + } + + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record UpdateStockCommand(Guid VariantId, int Quantity) : IRequest; + +public sealed class UpdateStockCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + UpdateStockCommand request, + CancellationToken cancellationToken) + { + var variant = await db.ProductVariants + .Include(v => v.Stock) + .FirstOrDefaultAsync(v => v.Id == request.VariantId, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Variant not found."); + + variant.Stock.Quantity = request.Quantity; + variant.Stock.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record CreateCategoryCommand( + string Name, + string Slug, + string? Description, + Guid? ParentCategoryId) : IRequest; + +public sealed class CreateCategoryCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreateCategoryCommand request, + CancellationToken cancellationToken) + { + var category = new Domain.Catalog.Category + { + Name = request.Name, + Slug = request.Slug, + Description = request.Description, + ParentCategoryId = request.ParentCategoryId, + }; + db.Add(category); + await db.SaveChangesAsync(cancellationToken); + return category.Id; + } +} + +public sealed record UpdateCategoryCommand( + Guid Id, + string Name, + string Slug, + string? Description) : IRequest; + +public sealed class UpdateCategoryCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + UpdateCategoryCommand request, + CancellationToken cancellationToken) + { + var category = await db.Categories + .FirstOrDefaultAsync(c => c.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException( + "Category not found."); + + category.Name = request.Name.Trim(); + category.Slug = request.Slug.Trim().ToLowerInvariant(); + category.Description = request.Description; + category.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record GetCategoriesQuery() : IRequest>; + +public sealed class GetCategoriesQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetCategoriesQuery request, + CancellationToken cancellationToken) + { + return await db.Categories + .AsNoTracking() + .OrderBy(c => c.Name) + .Select(c => new CategoryDto(c.Id, c.Name, c.Slug)) + .ToListAsync(cancellationToken); + } +} + +public sealed record AddProductImageCommand( + Guid ProductId, + string Url, + string? AltText, + int SortOrder, + bool IsPrimary) : IRequest; + +public sealed class AddProductImageCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + AddProductImageCommand request, + CancellationToken cancellationToken) + { + var image = new Domain.Catalog.ProductImage + { + ProductId = request.ProductId, + Url = request.Url, + AltText = request.AltText, + SortOrder = request.SortOrder, + IsPrimary = request.IsPrimary, + }; + db.Add(image); + await db.SaveChangesAsync(cancellationToken); + return image.Id; + } +} + +public sealed record SyncProductImageDto( + string Url, + string? AltText, + int SortOrder, + bool IsPrimary); + +public sealed record SyncProductImagesCommand( + Guid ProductId, + IReadOnlyList Images) : IRequest; + +public sealed class SyncProductImagesCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + SyncProductImagesCommand request, + CancellationToken cancellationToken) + { + var exists = await db.Products + .AnyAsync(p => p.Id == request.ProductId, cancellationToken); + + if (!exists) + { + throw new Common.Exceptions.NotFoundException("Product not found."); + } + + await db.ProductImages + .Where(i => i.ProductId == request.ProductId) + .ExecuteDeleteAsync(cancellationToken); + + var entries = request.Images + .Where(i => !string.IsNullOrWhiteSpace(i.Url)) + .ToList(); + + var primaryIndex = entries.FindIndex(e => e.IsPrimary); + if (primaryIndex < 0 && entries.Count > 0) + { + primaryIndex = 0; + } + + for (var i = 0; i < entries.Count; i++) + { + var entry = entries[i]; + db.Add(new Domain.Catalog.ProductImage + { + ProductId = request.ProductId, + Url = entry.Url.Trim(), + AltText = entry.AltText, + SortOrder = entry.SortOrder >= 0 ? entry.SortOrder : i, + IsPrimary = i == primaryIndex, + }); + } + + await db.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/backend/src/iLED.Application/Catalog/Commands/UploadProductImageCommand.cs b/src/backend/src/iLED.Application/Catalog/Commands/UploadProductImageCommand.cs new file mode 100644 index 0000000..5eb6805 --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Commands/UploadProductImageCommand.cs @@ -0,0 +1,26 @@ +using iLED.Application.Common.Interfaces; +using MediatR; + +namespace iLED.Application.Catalog.Commands; + +public sealed record UploadProductImageCommand( + Stream ImageStream, + string FileName) : IRequest; + +public sealed record UploadProductImageResult(string Url); + +public sealed class UploadProductImageCommandHandler(IProductImageStorage storage) + : IRequestHandler +{ + public async Task Handle( + UploadProductImageCommand request, + CancellationToken cancellationToken) + { + var url = await storage.SaveAsWebpAsync( + request.ImageStream, + request.FileName, + cancellationToken); + + return new UploadProductImageResult(url); + } +} diff --git a/src/backend/src/iLED.Application/Catalog/Commands/WarehouseCommands.cs b/src/backend/src/iLED.Application/Catalog/Commands/WarehouseCommands.cs new file mode 100644 index 0000000..e893bbe --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Commands/WarehouseCommands.cs @@ -0,0 +1,237 @@ +using iLED.Application.Catalog; +using iLED.Application.Catalog.Dtos; +using iLED.Application.Common.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Catalog.Commands; + +public sealed record GetWarehousesQuery() + : IRequest>; + +public sealed class GetWarehousesQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetWarehousesQuery request, + CancellationToken cancellationToken) + { + return await db.Warehouses + .AsNoTracking() + .OrderBy(w => w.SortOrder) + .Select(w => new WarehouseDto( + w.Id, + w.Name, + w.Code, + w.SortOrder)) + .ToListAsync(cancellationToken); + } +} + +public sealed record CreateWarehouseCommand( + string Name, + string Code, + int SortOrder) : IRequest; + +public sealed class CreateWarehouseCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreateWarehouseCommand request, + CancellationToken cancellationToken) + { + var code = request.Code.Trim().ToUpperInvariant(); + var exists = await db.Warehouses + .AnyAsync(w => w.Code == code, cancellationToken); + + if (exists) + { + throw new Common.Exceptions.ValidationException( + "Magazyn o podanym kodzie już istnieje."); + } + + var warehouse = new Domain.Catalog.Warehouse + { + Name = request.Name.Trim(), + Code = code, + SortOrder = request.SortOrder, + }; + + db.Add(warehouse); + await db.SaveChangesAsync(cancellationToken); + return warehouse.Id; + } +} + +public sealed record UpdateWarehouseCommand( + Guid Id, + string Name, + int SortOrder) : IRequest; + +public sealed class UpdateWarehouseCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + UpdateWarehouseCommand request, + CancellationToken cancellationToken) + { + var warehouse = await db.Warehouses + .FirstOrDefaultAsync(w => w.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Warehouse not found."); + + warehouse.Name = request.Name.Trim(); + warehouse.SortOrder = request.SortOrder; + warehouse.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record DeleteWarehouseCommand(Guid Id) : IRequest; + +public sealed class DeleteWarehouseCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + DeleteWarehouseCommand request, + CancellationToken cancellationToken) + { + var warehouse = await db.Warehouses + .FirstOrDefaultAsync(w => w.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Warehouse not found."); + + var hasStock = await db.WarehouseStocks + .AnyAsync( + ws => ws.WarehouseId == request.Id && ws.Quantity > 0, + cancellationToken); + + if (hasStock) + { + throw new Common.Exceptions.ValidationException( + "Nie można usunąć magazynu ze stanem magazynowym."); + } + + db.Remove(warehouse); + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record CreateProductGroupCommand( + LocalizedTextDto Names, + string Code, + int SortOrder) : IRequest; + +public sealed class CreateProductGroupCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreateProductGroupCommand request, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(request.Names.Pl)) + { + throw new Common.Exceptions.ValidationException( + "Nazwa PL jest wymagana."); + } + + var code = request.Code.Trim().ToUpperInvariant(); + var exists = await db.ProductGroups + .AnyAsync(g => g.Code == code, cancellationToken); + + if (exists) + { + throw new Common.Exceptions.ValidationException( + "Grupa o podanym kodzie już istnieje."); + } + + var group = new Domain.Catalog.ProductGroup + { + Code = code, + SortOrder = request.SortOrder, + }; + ProductLocalization.ApplyToGroup(group, request.Names); + + db.Add(group); + await db.SaveChangesAsync(cancellationToken); + return group.Id; + } +} + +public sealed record UpdateProductGroupCommand( + Guid Id, + LocalizedTextDto Names, + int SortOrder) : IRequest; + +public sealed class UpdateProductGroupCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + UpdateProductGroupCommand request, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(request.Names.Pl)) + { + throw new Common.Exceptions.ValidationException( + "Nazwa PL jest wymagana."); + } + + var group = await db.ProductGroups + .FirstOrDefaultAsync(g => g.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Group not found."); + + group.SortOrder = request.SortOrder; + group.UpdatedAt = DateTime.UtcNow; + ProductLocalization.ApplyToGroup(group, request.Names); + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record DeleteProductGroupCommand(Guid Id) : IRequest; + +public sealed class DeleteProductGroupCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + DeleteProductGroupCommand request, + CancellationToken cancellationToken) + { + var group = await db.ProductGroups + .FirstOrDefaultAsync(g => g.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Group not found."); + + db.Remove(group); + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record GetProductGroupsQuery() + : IRequest>; + +public sealed class GetProductGroupsQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetProductGroupsQuery request, + CancellationToken cancellationToken) + { + var groups = await db.ProductGroups + .AsNoTracking() + .OrderBy(g => g.SortOrder) + .Select(g => new + { + g.Id, + g.Name, + g.Names, + g.Code, + g.SortOrder, + }) + .ToListAsync(cancellationToken); + + return groups + .Select(g => new ProductGroupDto( + g.Id, + ProductLocalization.ToDto(g.Names, g.Name), + g.Code, + g.SortOrder)) + .ToList(); + } +} diff --git a/src/backend/src/iLED.Application/Catalog/Dtos/AdminProductDtos.cs b/src/backend/src/iLED.Application/Catalog/Dtos/AdminProductDtos.cs new file mode 100644 index 0000000..abc0ce2 --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Dtos/AdminProductDtos.cs @@ -0,0 +1,84 @@ +namespace iLED.Application.Catalog.Dtos; + +public sealed record LocalizedTextDto( + string Pl, + string En, + string De, + string Cs); + +public sealed record ProductGroupDto( + Guid Id, + LocalizedTextDto Names, + string Code, + int SortOrder); + +public sealed record AdminAttributeDto( + Guid? Id, + LocalizedTextDto Names, + LocalizedTextDto Values, + string? FilterGroup); + +public sealed record WarehouseDto( + Guid Id, + string Name, + string Code, + int SortOrder); + +public sealed record WarehouseStockDto( + Guid WarehouseId, + string WarehouseName, + string WarehouseCode, + int Quantity); + +public sealed record AdminProductImageDto( + Guid Id, + string Url, + string? AltText, + int SortOrder, + bool IsPrimary); + +public sealed record AdminVariantDto( + Guid Id, + string Sku, + string ColorName, + int TotalStock, + int ReservedStock, + IReadOnlyList WarehouseStocks); + +public sealed record AdminProductListItemDto( + Guid Id, + string Name, + string Slug, + string Model, + string? Ean, + Guid? ProductGroupId, + string? ProductGroupName, + decimal BasePrice, + bool IsActive, + bool IsInStock, + int TotalStock, + int ReservedStock, + string? PrimaryImageUrl, + IReadOnlyList Variants); + +public sealed record AdminProductDetailDto( + Guid Id, + string Slug, + LocalizedTextDto Names, + LocalizedTextDto ShortDescriptions, + LocalizedTextDto Descriptions, + decimal BasePrice, + decimal LowestPrice30Days, + string ProductCode, + string Model, + string? Ean, + Guid CategoryId, + Guid? ProductGroupId, + bool IsBestseller, + bool IsNewArrival, + bool IsActive, + int DeliveryHours, + IReadOnlyList Attributes, + IReadOnlyList Images, + IReadOnlyList Variants); + diff --git a/src/backend/src/iLED.Application/Catalog/Dtos/ProductDtos.cs b/src/backend/src/iLED.Application/Catalog/Dtos/ProductDtos.cs new file mode 100644 index 0000000..e35cfe2 --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Dtos/ProductDtos.cs @@ -0,0 +1,70 @@ +namespace iLED.Application.Catalog.Dtos; + +public sealed record ProductListItemDto( + Guid Id, + string Name, + string Slug, + string ShortDescription, + decimal Price, + decimal? AverageRating, + int ReviewCount, + string? PrimaryImageUrl, + bool IsBestseller, + bool IsNewArrival, + bool IsInStock); + +public sealed record ProductVariantDto( + Guid Id, + string Sku, + string ColorName, + string ColorHex, + decimal Price, + bool IsDefault, + bool IsInStock, + int StockQuantity); + +public sealed record ProductImageDto( + Guid Id, + string Url, + string? AltText, + int SortOrder, + bool IsPrimary); + +public sealed record ProductAttributeDto( + string Name, + string Value); + +public sealed record ProductDetailDto( + Guid Id, + string Name, + string Slug, + string ShortDescription, + string Description, + decimal Price, + decimal LowestPrice30Days, + string ProductCode, + string Manufacturer, + bool IsBestseller, + bool IsInStock, + int DeliveryHours, + decimal? AverageRating, + int ReviewCount, + string CategoryName, + IReadOnlyList Variants, + IReadOnlyList Images, + IReadOnlyList Attributes); + +public sealed record CategoryDto( + Guid Id, + string Name, + string Slug); + +public sealed record ProductFilterOptionsDto( + decimal MinPrice, + decimal MaxPrice, + IReadOnlyList Colors, + IReadOnlyList FilterGroups); + +public sealed record FilterGroupDto( + string Name, + IReadOnlyList Values); diff --git a/src/backend/src/iLED.Application/Catalog/ProductLocalization.cs b/src/backend/src/iLED.Application/Catalog/ProductLocalization.cs new file mode 100644 index 0000000..5c944c6 --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/ProductLocalization.cs @@ -0,0 +1,83 @@ +using iLED.Application.Catalog.Dtos; +using iLED.Domain.Common; + +namespace iLED.Application.Catalog; + +public static class ProductLocalization +{ + public static LocalizedTextDto ToDto( + LocalizedStrings source, + string fallback = "") => + new( + string.IsNullOrWhiteSpace(source.Pl) ? fallback : source.Pl, + string.IsNullOrWhiteSpace(source.En) ? fallback : source.En, + string.IsNullOrWhiteSpace(source.De) ? fallback : source.De, + string.IsNullOrWhiteSpace(source.Cs) ? fallback : source.Cs); + + public static LocalizedTextDto EmptyDto() => + new(string.Empty, string.Empty, string.Empty, string.Empty); + + public static LocalizedStrings FromDto(LocalizedTextDto source) => + new() + { + Pl = source.Pl?.Trim() ?? string.Empty, + En = source.En?.Trim() ?? string.Empty, + De = source.De?.Trim() ?? string.Empty, + Cs = source.Cs?.Trim() ?? string.Empty, + }; + + public static LocalizedStrings FromOptional( + LocalizedTextDto? source, + string? legacyPl = null) + { + if (source is null) + { + return new LocalizedStrings + { + Pl = legacyPl?.Trim() ?? string.Empty, + }; + } + + var result = FromDto(source); + if (string.IsNullOrWhiteSpace(result.Pl) && + !string.IsNullOrWhiteSpace(legacyPl)) + { + result.Pl = legacyPl.Trim(); + } + + return result; + } + + public static void ApplyToProduct( + Domain.Catalog.Product product, + LocalizedTextDto names, + LocalizedTextDto shortDescriptions, + LocalizedTextDto descriptions) + { + product.Names = FromDto(names); + product.ShortDescriptions = FromDto(shortDescriptions); + product.Descriptions = FromDto(descriptions); + product.Name = names.Pl; + product.ShortDescription = shortDescriptions.Pl; + product.Description = descriptions.Pl; + } + + public static void ApplyToGroup( + Domain.Catalog.ProductGroup group, + LocalizedTextDto names) + { + group.Names = FromDto(names); + group.Name = names.Pl; + } + + public static void ApplyToAttribute( + Domain.Catalog.ProductAttribute attribute, + LocalizedTextDto names, + LocalizedTextDto values) + { + attribute.Names = FromDto(names); + attribute.Values = FromDto(values); + attribute.Name = names.Pl; + attribute.Value = values.Pl; + } +} diff --git a/src/backend/src/iLED.Application/Catalog/Queries/AdminCatalogQueries.cs b/src/backend/src/iLED.Application/Catalog/Queries/AdminCatalogQueries.cs new file mode 100644 index 0000000..073fd5c --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Queries/AdminCatalogQueries.cs @@ -0,0 +1,259 @@ +using iLED.Application.Catalog; +using iLED.Application.Catalog.Dtos; +using iLED.Application.Common.Interfaces; +using iLED.Application.Common.Models; +using iLED.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Catalog.Queries; + +public sealed record GetAdminProductsQuery( + int Page = 1, + int PageSize = 50, + string? Search = null, + bool? IsActive = null, + Guid? GroupId = null, + bool LowStock = false, + string SortBy = "name", + string SortDir = "asc") : IRequest>; + +public sealed class GetAdminProductsQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + private static readonly OrderStatus[] OpenOrderStatuses = + [ + OrderStatus.PendingPayment, + OrderStatus.Paid, + OrderStatus.Processing, + ]; + + public async Task> Handle( + GetAdminProductsQuery request, + CancellationToken cancellationToken) + { + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + + var query = db.Products.AsNoTracking().AsQueryable(); + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var term = request.Search.Trim().ToLowerInvariant(); + query = query.Where(p => + p.Name.ToLower().Contains(term) || + p.Slug.ToLower().Contains(term) || + p.Model.ToLower().Contains(term) || + (p.Ean != null && p.Ean.ToLower().Contains(term)) || + p.Variants.Any(v => v.Sku.ToLower().Contains(term))); + } + + if (request.IsActive.HasValue) + { + query = query.Where(p => p.IsActive == request.IsActive.Value); + } + + if (request.GroupId.HasValue) + { + query = query.Where(p => p.ProductGroupId == request.GroupId.Value); + } + + if (request.LowStock) + { + query = query.Where(p => + p.Variants.Any(v => + v.Stock.Quantity <= v.Stock.LowStockThreshold)); + } + + var sortBy = (request.SortBy ?? "name").Trim().ToLowerInvariant(); + var desc = string.Equals( + request.SortDir, + "desc", + StringComparison.OrdinalIgnoreCase); + + query = (sortBy, desc) switch + { + ("price", true) => query.OrderByDescending(p => p.BasePrice), + ("price", false) => query.OrderBy(p => p.BasePrice), + ("stock", true) => query.OrderByDescending(p => + p.Variants.Sum(v => v.Stock.Quantity)), + ("stock", false) => query.OrderBy(p => + p.Variants.Sum(v => v.Stock.Quantity)), + ("name", true) => query.OrderByDescending(p => p.Name), + _ => query.OrderBy(p => p.Name), + }; + + var total = await query.CountAsync(cancellationToken); + + var reservedByVariant = await db.Orders + .AsNoTracking() + .Where(o => OpenOrderStatuses.Contains(o.Status)) + .SelectMany(o => o.Lines) + .GroupBy(l => l.ProductVariantId) + .Select(g => new { VariantId = g.Key, Qty = g.Sum(l => l.Quantity) }) + .ToDictionaryAsync(x => x.VariantId, x => x.Qty, cancellationToken); + + var products = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(p => new + { + p.Id, + p.Name, + p.Slug, + p.Model, + p.Ean, + p.ProductGroupId, + GroupName = p.ProductGroup != null ? p.ProductGroup.Name : null, + p.BasePrice, + p.IsActive, + Variants = p.Variants.Select(v => new + { + v.Id, + v.Sku, + v.ColorName, + StockQty = v.Stock.Quantity, + WarehouseStocks = v.WarehouseStocks.Select(ws => new + { + ws.WarehouseId, + ws.Warehouse.Name, + ws.Warehouse.Code, + ws.Quantity, + }), + }), + PrimaryImageUrl = p.Images + .OrderByDescending(i => i.IsPrimary) + .ThenBy(i => i.SortOrder) + .Select(i => i.Url) + .FirstOrDefault(), + }) + .ToListAsync(cancellationToken); + + var items = products.Select(p => + { + var variants = p.Variants.Select(v => + { + var reserved = reservedByVariant.GetValueOrDefault(v.Id, 0); + return new AdminVariantDto( + v.Id, + v.Sku, + v.ColorName, + v.StockQty, + reserved, + v.WarehouseStocks + .OrderBy(ws => ws.Code) + .Select(ws => new WarehouseStockDto( + ws.WarehouseId, + ws.Name, + ws.Code, + ws.Quantity)) + .ToList()); + }).ToList(); + + var totalStock = variants.Sum(v => v.TotalStock); + var reservedStock = variants.Sum(v => v.ReservedStock); + + return new AdminProductListItemDto( + p.Id, + p.Name, + p.Slug, + p.Model, + p.Ean, + p.ProductGroupId, + p.GroupName, + p.BasePrice, + p.IsActive, + totalStock > 0, + totalStock, + reservedStock, + p.PrimaryImageUrl, + variants); + }).ToList(); + + return new PaginatedList + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = total, + }; + } +} + +public sealed record GetAdminProductQuery(Guid Id) : IRequest; + +public sealed class GetAdminProductQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetAdminProductQuery request, + CancellationToken cancellationToken) + { + var product = await db.Products + .AsNoTracking() + .Include(p => p.Attributes) + .Include(p => p.Images) + .Include(p => p.Variants) + .ThenInclude(v => v.Stock) + .Include(p => p.Variants) + .ThenInclude(v => v.WarehouseStocks) + .ThenInclude(ws => ws.Warehouse) + .FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Product not found."); + + return new AdminProductDetailDto( + product.Id, + product.Slug, + ProductLocalization.ToDto(product.Names, product.Name), + ProductLocalization.ToDto( + product.ShortDescriptions, + product.ShortDescription), + ProductLocalization.ToDto( + product.Descriptions, + product.Description), + product.BasePrice, + product.LowestPrice30Days, + product.ProductCode, + product.Model, + product.Ean, + product.CategoryId, + product.ProductGroupId, + product.IsBestseller, + product.IsNewArrival, + product.IsActive, + product.DeliveryHours, + product.Attributes + .Select(a => new AdminAttributeDto( + a.Id, + ProductLocalization.ToDto(a.Names, a.Name), + ProductLocalization.ToDto(a.Values, a.Value), + a.FilterGroup)) + .ToList(), + product.Images + .OrderByDescending(i => i.IsPrimary) + .ThenBy(i => i.SortOrder) + .Select(i => new AdminProductImageDto( + i.Id, + i.Url, + i.AltText, + i.SortOrder, + i.IsPrimary)) + .ToList(), + product.Variants + .Select(v => new AdminVariantDto( + v.Id, + v.Sku, + v.ColorName, + v.Stock.Quantity, + 0, + v.WarehouseStocks + .OrderBy(ws => ws.Warehouse.Code) + .Select(ws => new WarehouseStockDto( + ws.WarehouseId, + ws.Warehouse.Name, + ws.Warehouse.Code, + ws.Quantity)) + .ToList())) + .ToList()); + } +} diff --git a/src/backend/src/iLED.Application/Catalog/Queries/GetProductDetailQuery.cs b/src/backend/src/iLED.Application/Catalog/Queries/GetProductDetailQuery.cs new file mode 100644 index 0000000..359d9e7 --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Queries/GetProductDetailQuery.cs @@ -0,0 +1,104 @@ +using iLED.Application.Catalog.Dtos; +using iLED.Application.Common; +using iLED.Application.Common.Exceptions; +using iLED.Application.Common.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Catalog.Queries; + +public sealed record GetProductDetailQuery(string Slug, string? Locale = "pl") + : IRequest; + +public sealed class GetProductDetailQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetProductDetailQuery request, + CancellationToken cancellationToken) + { + var locale = LocaleHelper.Normalize(request.Locale); + var product = await db.Products + .AsNoTracking() + .Where(p => p.Slug == request.Slug && p.IsActive) + .Select(p => new + { + p.Id, + p.Name, + p.Names, + p.Slug, + p.ShortDescription, + p.ShortDescriptions, + p.Description, + p.Descriptions, + p.BasePrice, + p.LowestPrice30Days, + p.ProductCode, + p.Manufacturer, + p.IsBestseller, + p.DeliveryHours, + CategoryName = p.Category.Name, + Variants = p.Variants.Select(v => new ProductVariantDto( + v.Id, + v.Sku, + v.ColorName, + v.ColorHex, + p.BasePrice + v.PriceAdjustment, + v.IsDefault, + v.Stock.Quantity > 0, + v.Stock.Quantity)).ToList(), + Images = p.Images + .OrderBy(i => i.SortOrder) + .Select(i => new ProductImageDto( + i.Id, i.Url, i.AltText, i.SortOrder, i.IsPrimary)) + .ToList(), + Attributes = p.Attributes + .Select(a => new + { + a.Name, + a.Value, + a.Names, + a.Values, + }) + .ToList(), + AvgRating = p.Reviews + .Where(r => r.IsApproved) + .Select(r => (double?)r.Rating) + .Average(), + ReviewCount = p.Reviews.Count(r => r.IsApproved), + InStock = p.Variants.Any(v => v.Stock.Quantity > 0), + }) + .FirstOrDefaultAsync(cancellationToken); + + if (product is null) + { + throw new NotFoundException($"Product '{request.Slug}' not found."); + } + + return new ProductDetailDto( + product.Id, + product.Names.Resolve(locale, product.Name), + product.Slug, + product.ShortDescriptions.Resolve( + locale, + product.ShortDescription), + product.Descriptions.Resolve(locale, product.Description), + product.BasePrice, + product.LowestPrice30Days, + product.ProductCode, + product.Manufacturer, + product.IsBestseller, + product.InStock, + product.DeliveryHours, + product.AvgRating.HasValue ? (decimal)product.AvgRating.Value : null, + product.ReviewCount, + product.CategoryName, + product.Variants, + product.Images, + product.Attributes + .Select(a => new ProductAttributeDto( + a.Names.Resolve(locale, a.Name), + a.Values.Resolve(locale, a.Value))) + .ToList()); + } +} diff --git a/src/backend/src/iLED.Application/Catalog/Queries/GetProductFilterOptionsQuery.cs b/src/backend/src/iLED.Application/Catalog/Queries/GetProductFilterOptionsQuery.cs new file mode 100644 index 0000000..70365cb --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Queries/GetProductFilterOptionsQuery.cs @@ -0,0 +1,64 @@ +using iLED.Application.Catalog.Dtos; +using iLED.Application.Common; +using iLED.Application.Common.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Catalog.Queries; + +public sealed record GetProductFilterOptionsQuery(string? Locale = "pl") + : IRequest; + +public sealed class GetProductFilterOptionsQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetProductFilterOptionsQuery request, + CancellationToken cancellationToken) + { + var locale = LocaleHelper.Normalize(request.Locale); + var products = db.Products.AsNoTracking().Where(p => p.IsActive); + + var minPrice = await products.MinAsync( + p => p.BasePrice, cancellationToken); + var maxPrice = await products.MaxAsync( + p => p.BasePrice, cancellationToken); + + var colors = await db.ProductVariants + .AsNoTracking() + .Select(v => v.ColorName) + .Distinct() + .OrderBy(c => c) + .ToListAsync(cancellationToken); + + var attributeRows = await db.Products + .AsNoTracking() + .Where(p => p.IsActive) + .SelectMany(p => p.Attributes) + .Where(a => a.FilterGroup != null) + .Select(a => new + { + Group = a.FilterGroup!, + a.Value, + a.Values, + }) + .ToListAsync(cancellationToken); + + var filterGroups = attributeRows + .GroupBy(a => a.Group) + .Select(g => new FilterGroupDto( + g.Key, + g.Select(a => a.Values.Resolve(locale, a.Value)) + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Distinct() + .OrderBy(v => v) + .ToList())) + .ToList(); + + return new ProductFilterOptionsDto( + minPrice, + maxPrice, + colors, + filterGroups); + } +} diff --git a/src/backend/src/iLED.Application/Catalog/Queries/GetProductListQuery.cs b/src/backend/src/iLED.Application/Catalog/Queries/GetProductListQuery.cs new file mode 100644 index 0000000..7ddd5b9 --- /dev/null +++ b/src/backend/src/iLED.Application/Catalog/Queries/GetProductListQuery.cs @@ -0,0 +1,137 @@ +using iLED.Application.Catalog.Dtos; +using iLED.Application.Common; +using iLED.Application.Common.Interfaces; +using iLED.Application.Common.Models; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Catalog.Queries; + +public sealed record GetProductListQuery( + int Page = 1, + int PageSize = 12, + decimal? MinPrice = null, + decimal? MaxPrice = null, + string? Color = null, + string? Search = null, + bool? IsBestseller = null, + bool? IsNewArrival = null, + Guid? CategoryId = null, + string? Locale = "pl") : IRequest>; + +public sealed class GetProductListQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetProductListQuery request, + CancellationToken cancellationToken) + { + var locale = LocaleHelper.Normalize(request.Locale); + var query = db.Products + .AsNoTracking() + .Where(p => p.IsActive); + + if (request.MinPrice.HasValue) + { + query = query.Where(p => p.BasePrice >= request.MinPrice.Value); + } + + if (request.MaxPrice.HasValue) + { + query = query.Where(p => p.BasePrice <= request.MaxPrice.Value); + } + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var term = request.Search.ToLower(); + query = query.Where(p => + p.Name.ToLower().Contains(term) || + p.ShortDescription.ToLower().Contains(term) || + p.Names.Pl.ToLower().Contains(term) || + p.Names.En.ToLower().Contains(term) || + p.Names.De.ToLower().Contains(term) || + p.Names.Cs.ToLower().Contains(term) || + p.ShortDescriptions.Pl.ToLower().Contains(term) || + p.ShortDescriptions.En.ToLower().Contains(term) || + p.ShortDescriptions.De.ToLower().Contains(term) || + p.ShortDescriptions.Cs.ToLower().Contains(term)); + } + + if (request.IsBestseller == true) + { + query = query.Where(p => p.IsBestseller); + } + + if (request.IsNewArrival == true) + { + query = query.Where(p => p.IsNewArrival); + } + + if (request.CategoryId.HasValue) + { + query = query.Where(p => p.CategoryId == request.CategoryId.Value); + } + + if (!string.IsNullOrWhiteSpace(request.Color)) + { + var color = request.Color.ToLower(); + query = query.Where(p => p.Variants.Any(v => + v.ColorName.ToLower() == color)); + } + + var total = await query.CountAsync(cancellationToken); + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 48); + + var products = await query + .OrderByDescending(p => p.IsBestseller) + .ThenByDescending(p => p.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(p => new + { + p.Id, + p.Name, + p.Names, + p.Slug, + p.ShortDescription, + p.ShortDescriptions, + p.BasePrice, + p.IsBestseller, + p.IsNewArrival, + PrimaryImage = p.Images + .OrderByDescending(i => i.IsPrimary) + .ThenBy(i => i.SortOrder) + .Select(i => i.Url) + .FirstOrDefault(), + AvgRating = p.Reviews + .Where(r => r.IsApproved) + .Select(r => (double?)r.Rating) + .Average(), + ReviewCount = p.Reviews.Count(r => r.IsApproved), + InStock = p.Variants.Any(v => v.Stock.Quantity > 0), + }) + .ToListAsync(cancellationToken); + + var items = products.Select(p => new ProductListItemDto( + p.Id, + p.Names.Resolve(locale, p.Name), + p.Slug, + p.ShortDescriptions.Resolve(locale, p.ShortDescription), + p.BasePrice, + p.AvgRating.HasValue ? (decimal)p.AvgRating.Value : null, + p.ReviewCount, + p.PrimaryImage, + p.IsBestseller, + p.IsNewArrival, + p.InStock)).ToList(); + + return new PaginatedList + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = total, + }; + } +} diff --git a/src/backend/src/iLED.Application/Common/Behaviors/ValidationBehavior.cs b/src/backend/src/iLED.Application/Common/Behaviors/ValidationBehavior.cs new file mode 100644 index 0000000..c018fe2 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Behaviors/ValidationBehavior.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using MediatR; + +namespace iLED.Application.Common.Behaviors; + +public sealed class ValidationBehavior( + IEnumerable> validators) + : IPipelineBehavior + where TRequest : notnull +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + if (!validators.Any()) + { + return await next(); + } + + var context = new ValidationContext(request); + var failures = validators + .Select(v => v.Validate(context)) + .SelectMany(r => r.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count != 0) + { + throw new Exceptions.ValidationException( + string.Join("; ", failures.Select(f => f.ErrorMessage))); + } + + return await next(); + } +} diff --git a/src/backend/src/iLED.Application/Common/Exceptions/NotFoundException.cs b/src/backend/src/iLED.Application/Common/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..6f39219 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Exceptions/NotFoundException.cs @@ -0,0 +1,8 @@ +namespace iLED.Application.Common.Exceptions; + +public class NotFoundException : Exception +{ + public NotFoundException(string message) : base(message) + { + } +} diff --git a/src/backend/src/iLED.Application/Common/Exceptions/ValidationException.cs b/src/backend/src/iLED.Application/Common/Exceptions/ValidationException.cs new file mode 100644 index 0000000..575f4c9 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Exceptions/ValidationException.cs @@ -0,0 +1,21 @@ +namespace iLED.Application.Common.Exceptions; + +public class ValidationException : Exception +{ + public string? ErrorCode { get; } + + public decimal? MinOrderAmount { get; } + + public ValidationException(string message) : base(message) + { + } + + public ValidationException( + string message, + string errorCode, + decimal? minOrderAmount = null) : base(message) + { + ErrorCode = errorCode; + MinOrderAmount = minOrderAmount; + } +} diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IAppDbContext.cs b/src/backend/src/iLED.Application/Common/Interfaces/IAppDbContext.cs new file mode 100644 index 0000000..fc0065e --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/IAppDbContext.cs @@ -0,0 +1,48 @@ +namespace iLED.Application.Common.Interfaces; + +public interface IAppDbContext : IApplicationDbContext +{ + IQueryable Products { get; } + + IQueryable ProductAttributes { get; } + + IQueryable ProductImages { get; } + + IQueryable ProductGroups { get; } + + IQueryable Categories { get; } + + IQueryable ProductVariants { get; } + + IQueryable Warehouses { get; } + + IQueryable WarehouseStocks { get; } + + IQueryable Reviews { get; } + + IQueryable Carts { get; } + + IQueryable FavoriteItems { get; } + + IQueryable Orders { get; } + + IQueryable Payments { get; } + + IQueryable Shipments { get; } + + IQueryable DiscountCodes { get; } + + IQueryable DiscountRules { get; } + + IQueryable DiscountRedemptions { get; } + + IQueryable PromoBanners { get; } + + IQueryable NewsletterSubscribers { get; } + + IQueryable NewsletterCampaigns { get; } + + void Add(T entity) where T : class; + + void Remove(T entity) where T : class; +} diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IApplicationDbContext.cs b/src/backend/src/iLED.Application/Common/Interfaces/IApplicationDbContext.cs new file mode 100644 index 0000000..f2d1013 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/IApplicationDbContext.cs @@ -0,0 +1,6 @@ +namespace iLED.Application.Common.Interfaces; + +public interface IApplicationDbContext +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/backend/src/iLED.Application/Common/Interfaces/ICatalogDbContext.cs b/src/backend/src/iLED.Application/Common/Interfaces/ICatalogDbContext.cs new file mode 100644 index 0000000..d58491f --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/ICatalogDbContext.cs @@ -0,0 +1,14 @@ +namespace iLED.Application.Common.Interfaces; + +public interface ICatalogDbContext : IApplicationDbContext +{ + IQueryable Products { get; } + + IQueryable Categories { get; } + + IQueryable Reviews { get; } + + void Add(T entity) where T : class; + + void Remove(T entity) where T : class; +} diff --git a/src/backend/src/iLED.Application/Common/Interfaces/ICurrentUserService.cs b/src/backend/src/iLED.Application/Common/Interfaces/ICurrentUserService.cs new file mode 100644 index 0000000..76e23e1 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/ICurrentUserService.cs @@ -0,0 +1,15 @@ +namespace iLED.Application.Common.Interfaces; + +public interface ICurrentUserService +{ + string? UserId { get; } + + string? SessionId { get; } + + bool IsAuthenticated { get; } + + bool IsAdmin { get; } + + /// Storefront locale from query (pl/en/de/cs). + string Locale { get; } +} diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IEmailService.cs b/src/backend/src/iLED.Application/Common/Interfaces/IEmailService.cs new file mode 100644 index 0000000..0818cfd --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/IEmailService.cs @@ -0,0 +1,10 @@ +namespace iLED.Application.Common.Interfaces; + +public interface IEmailService +{ + Task SendAsync( + string to, + string subject, + string htmlBody, + CancellationToken cancellationToken = default); +} diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IJwtTokenService.cs b/src/backend/src/iLED.Application/Common/Interfaces/IJwtTokenService.cs new file mode 100644 index 0000000..476157d --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/IJwtTokenService.cs @@ -0,0 +1,11 @@ +namespace iLED.Application.Common.Interfaces; + +public interface IJwtTokenService +{ + string GenerateAccessToken(string userId, string email, IList roles); + + string GenerateRefreshToken(); + + (string userId, string email, IList roles)? ValidateAccessToken( + string token); +} diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IPaymentService.cs b/src/backend/src/iLED.Application/Common/Interfaces/IPaymentService.cs new file mode 100644 index 0000000..6f49800 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/IPaymentService.cs @@ -0,0 +1,51 @@ +namespace iLED.Application.Common.Interfaces; + +public interface IPaymentService +{ + Task InitiatePaymentAsync( + PaymentInitiationRequest request, + CancellationToken cancellationToken = default); + + Task HandleWebhookAsync( + string payload, + string signature, + CancellationToken cancellationToken = default); + + /// + /// Fetches current status from PayU (GET /orders/{payuOrderId}). + /// + Task GetPaymentStatusAsync( + string externalId, + CancellationToken cancellationToken = default); +} + +public sealed record PaymentInitiationRequest( + Guid OrderId, + string OrderNumber, + decimal Amount, + string CustomerEmail, + string CustomerFirstName, + string CustomerLastName, + string ContinueUrl, + string NotifyUrl); + +public sealed record PaymentInitiationResult( + bool Success, + string? ExternalId, + string? RedirectUrl, + string? ErrorMessage); + +public sealed record PaymentWebhookResult( + bool Success, + Guid? OrderId, + string? ExternalId, + bool IsCompleted, + string? ErrorMessage); + +public sealed record PaymentStatusResult( + bool Success, + string? ExternalId, + string? Status, + bool IsCompleted, + bool IsCanceled, + string? ErrorMessage); diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IProductImageStorage.cs b/src/backend/src/iLED.Application/Common/Interfaces/IProductImageStorage.cs new file mode 100644 index 0000000..1623113 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/IProductImageStorage.cs @@ -0,0 +1,9 @@ +namespace iLED.Application.Common.Interfaces; + +public interface IProductImageStorage +{ + Task SaveAsWebpAsync( + Stream input, + string originalFileName, + CancellationToken cancellationToken); +} diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IShippingService.cs b/src/backend/src/iLED.Application/Common/Interfaces/IShippingService.cs new file mode 100644 index 0000000..46abf77 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Interfaces/IShippingService.cs @@ -0,0 +1,84 @@ +using iLED.Domain.Enums; + +namespace iLED.Application.Common.Interfaces; + +public interface IShippingService +{ + Task> SearchParcelPointsAsync( + ShippingParcelPointSearch search, + CancellationToken cancellationToken = default); + + Task CreateShipmentLabelAsync( + ShipmentRequest request, + CancellationToken cancellationToken = default); + + ShippingWidgetConfigDto GetWidgetConfig(); +} + +public sealed record ShippingParcelPointSearch( + ShippingCarrier Carrier, + string Query, + double? Latitude = null, + double? Longitude = null); + +public sealed record ShippingWidgetConfigDto( + bool UseSandbox, + InPostGeowidgetConfigDto? InPost, + DhlWidgetConfigDto? Dhl, + DpdWidgetConfigDto? Dpd); + +public sealed record InPostGeowidgetConfigDto( + string PublicToken, + string Config, + string SdkBaseUrl); + +public sealed record DhlWidgetConfigDto( + bool Enabled, + string WidgetUrl); + +public sealed record DpdWidgetConfigDto( + bool Enabled, + string WidgetUrl); + +public sealed record ParcelPointDto( + string Id, + string Name, + string Address, + string City, + string PostalCode, + double Latitude, + double Longitude, + string? OpeningHours = null, + bool SupportsCod = false, + double? DistanceKm = null); + +public sealed record ShipmentRequest( + Guid OrderId, + string OrderNumber, + ShippingCarrier Carrier, + ShippingServiceType ServiceType, + string CustomerFirstName, + string CustomerLastName, + string CustomerEmail, + string CustomerPhone, + string ShippingStreet, + string ShippingCity, + string ShippingPostalCode, + string ShippingCountry, + string? ParcelPointId, + string? ParcelPointName, + string? ParcelPointAddress, + decimal CodAmount, + IReadOnlyList Lines); + +public sealed record ShipmentLineRequest( + string ProductName, + int Quantity, + decimal UnitPrice); + +public sealed record ShipmentLabelResult( + bool Success, + string? TrackingNumber, + string? ExternalShipmentId, + string? LabelRelativePath, + string? ErrorMessage); diff --git a/src/backend/src/iLED.Application/Common/LocaleHelper.cs b/src/backend/src/iLED.Application/Common/LocaleHelper.cs new file mode 100644 index 0000000..57d1d99 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/LocaleHelper.cs @@ -0,0 +1,12 @@ +using iLED.Domain.Common; + +namespace iLED.Application.Common; + +/// +/// Shared locale normalization for storefront queries. +/// +public static class LocaleHelper +{ + public static string Normalize(string? locale) => + LocalizedStrings.NormalizeLocale(locale); +} diff --git a/src/backend/src/iLED.Application/Common/Models/PaginatedList.cs b/src/backend/src/iLED.Application/Common/Models/PaginatedList.cs new file mode 100644 index 0000000..c8ac942 --- /dev/null +++ b/src/backend/src/iLED.Application/Common/Models/PaginatedList.cs @@ -0,0 +1,18 @@ +namespace iLED.Application.Common.Models; + +public sealed class PaginatedList +{ + public IReadOnlyList Items { get; init; } = []; + + public int Page { get; init; } + + public int PageSize { get; init; } + + public int TotalCount { get; init; } + + public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); + + public bool HasNextPage => Page < TotalPages; + + public bool HasPreviousPage => Page > 1; +} diff --git a/src/backend/src/iLED.Application/DependencyInjection.cs b/src/backend/src/iLED.Application/DependencyInjection.cs new file mode 100644 index 0000000..d85ecab --- /dev/null +++ b/src/backend/src/iLED.Application/DependencyInjection.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using FluentValidation; +using iLED.Application.Common.Behaviors; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace iLED.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplication( + this IServiceCollection services) + { + var assembly = Assembly.GetExecutingAssembly(); + + services.AddMediatR(cfg => + cfg.RegisterServicesFromAssembly(assembly)); + services.AddValidatorsFromAssembly(assembly); + services.AddTransient( + typeof(IPipelineBehavior<,>), + typeof(ValidationBehavior<,>)); + + return services; + } +} diff --git a/src/backend/src/iLED.Application/Identity/Commands/IdentityCommands.cs b/src/backend/src/iLED.Application/Identity/Commands/IdentityCommands.cs new file mode 100644 index 0000000..cdf823c --- /dev/null +++ b/src/backend/src/iLED.Application/Identity/Commands/IdentityCommands.cs @@ -0,0 +1,92 @@ +using iLED.Application.Common.Interfaces; +using MediatR; + +namespace iLED.Application.Identity.Commands; + +public sealed record RegisterCommand( + string Email, + string Password, + string FirstName, + string LastName) : IRequest; + +public sealed record LoginCommand( + string Email, + string Password) : IRequest; + +public sealed record RefreshTokenCommand(string RefreshToken) + : IRequest; + +public sealed record AuthResponse( + string AccessToken, + string RefreshToken, + DateTime ExpiresAt, + string UserId, + string Email, + string FirstName, + string LastName, + IReadOnlyList Roles); + +public sealed record GetCurrentUserQuery() : IRequest; + +public sealed record UserProfileDto( + string UserId, + string Email, + string FirstName, + string LastName, + IReadOnlyList Roles); + +public interface IIdentityService +{ + Task RegisterAsync( + RegisterCommand command, + CancellationToken cancellationToken); + + Task LoginAsync( + LoginCommand command, + CancellationToken cancellationToken); + + Task RefreshTokenAsync( + RefreshTokenCommand command, + CancellationToken cancellationToken); + + Task GetProfileAsync( + string? userId, + CancellationToken cancellationToken); +} + +public sealed class RegisterCommandHandler(IIdentityService identity) + : IRequestHandler +{ + public Task Handle( + RegisterCommand request, + CancellationToken cancellationToken) => + identity.RegisterAsync(request, cancellationToken); +} + +public sealed class LoginCommandHandler(IIdentityService identity) + : IRequestHandler +{ + public Task Handle( + LoginCommand request, + CancellationToken cancellationToken) => + identity.LoginAsync(request, cancellationToken); +} + +public sealed class RefreshTokenCommandHandler(IIdentityService identity) + : IRequestHandler +{ + public Task Handle( + RefreshTokenCommand request, + CancellationToken cancellationToken) => + identity.RefreshTokenAsync(request, cancellationToken); +} + +public sealed class GetCurrentUserQueryHandler( + ICurrentUserService currentUser, + IIdentityService identity) : IRequestHandler +{ + public Task Handle( + GetCurrentUserQuery request, + CancellationToken cancellationToken) => + identity.GetProfileAsync(currentUser.UserId, cancellationToken); +} diff --git a/src/backend/src/iLED.Application/Marketing/Commands/MarketingCommands.cs b/src/backend/src/iLED.Application/Marketing/Commands/MarketingCommands.cs new file mode 100644 index 0000000..07c7d76 --- /dev/null +++ b/src/backend/src/iLED.Application/Marketing/Commands/MarketingCommands.cs @@ -0,0 +1,469 @@ +using iLED.Application.Common.Interfaces; +using iLED.Application.Common.Models; +using iLED.Application.Identity.Commands; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Marketing.Commands; + +public sealed record SubscribeNewsletterCommand(string Email) : IRequest; + +public sealed class SubscribeNewsletterCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + SubscribeNewsletterCommand request, + CancellationToken cancellationToken) + { + var email = request.Email.Trim().ToLowerInvariant(); + var existing = await db.NewsletterSubscribers + .FirstOrDefaultAsync(s => s.Email == email, cancellationToken); + + if (existing is not null) + { + existing.IsActive = true; + existing.UnsubscribedAt = null; + } + else + { + db.Add(new Domain.Marketing.NewsletterSubscriber + { + Email = email, + }); + } + + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record GetPromoBannerQuery(string? Locale = "pl") + : IRequest; + +public sealed record PromoBannerDto(string Text, string? HighlightedCode); + +public sealed class GetPromoBannerQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetPromoBannerQuery request, + CancellationToken cancellationToken) + { + var locale = Common.LocaleHelper.Normalize(request.Locale); + var banner = await db.PromoBanners + .AsNoTracking() + .Where(b => b.IsActive) + .OrderByDescending(b => b.CreatedAt) + .Select(b => new { b.Texts, b.HighlightedCode }) + .FirstOrDefaultAsync(cancellationToken); + + if (banner is null) + { + return null; + } + + var text = banner.Texts.Resolve(locale); + if (string.IsNullOrWhiteSpace(text)) + { + return null; + } + + return new PromoBannerDto(text, banner.HighlightedCode); + } +} + +public sealed record GetAdminPromoBannerQuery() : IRequest; + +public sealed record AdminPromoBannerDto( + Catalog.Dtos.LocalizedTextDto Texts, + string? HighlightedCode, + bool IsActive); + +public sealed class GetAdminPromoBannerQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetAdminPromoBannerQuery request, + CancellationToken cancellationToken) + { + var banner = await db.PromoBanners + .AsNoTracking() + .OrderByDescending(b => b.CreatedAt) + .Select(b => new { b.Texts, b.HighlightedCode, b.IsActive }) + .FirstOrDefaultAsync(cancellationToken); + + if (banner is null) + { + return null; + } + + return new AdminPromoBannerDto( + Catalog.ProductLocalization.ToDto(banner.Texts), + banner.HighlightedCode, + banner.IsActive); + } +} + +public sealed record ValidateDiscountCodeQuery( + string Code, + decimal Subtotal = 0, + string? CustomerEmail = null) : IRequest; + +public sealed record DiscountValidationDto( + bool IsValid, + string? ErrorCode, + decimal? MinOrderAmount, + decimal? DiscountAmount, + bool FreeShipping); + +public sealed class ValidateDiscountCodeQueryHandler(IMediator mediator) + : IRequestHandler +{ + public Task Handle( + ValidateDiscountCodeQuery request, + CancellationToken cancellationToken) => + mediator.Send( + new ValidatePromotionCodeQuery( + request.Code, + request.Subtotal, + request.CustomerEmail), + cancellationToken); +} + +public sealed record CreateDiscountRuleCommand( + string Name, + Domain.Enums.DiscountType Type, + decimal Value, + decimal? MinimumOrderAmount, + DateTime? ValidFrom, + DateTime? ValidTo) : IRequest; + +public sealed class CreateDiscountRuleCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreateDiscountRuleCommand request, + CancellationToken cancellationToken) + { + var rule = new Domain.Marketing.DiscountRule + { + Name = request.Name, + Type = request.Type, + Value = request.Value, + MinimumOrderAmount = request.MinimumOrderAmount, + ValidFrom = request.ValidFrom, + ValidTo = request.ValidTo, + }; + db.Add(rule); + await db.SaveChangesAsync(cancellationToken); + return rule.Id; + } +} + +public sealed record CreateDiscountCodeCommand( + Guid DiscountRuleId, + string Code, + int? MaxUsages) : IRequest; + +public sealed class CreateDiscountCodeCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreateDiscountCodeCommand request, + CancellationToken cancellationToken) + { + var code = new Domain.Marketing.DiscountCode + { + DiscountRuleId = request.DiscountRuleId, + Code = request.Code.ToUpperInvariant(), + MaxUsages = request.MaxUsages, + }; + db.Add(code); + await db.SaveChangesAsync(cancellationToken); + return code.Id; + } +} + +public sealed record UpdatePromoBannerCommand( + Catalog.Dtos.LocalizedTextDto Texts, + string? HighlightedCode, + bool IsActive) : IRequest; + +public sealed class UpdatePromoBannerCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + UpdatePromoBannerCommand request, + CancellationToken cancellationToken) + { + var banner = await db.PromoBanners + .OrderByDescending(b => b.CreatedAt) + .FirstOrDefaultAsync(cancellationToken); + + var texts = Catalog.ProductLocalization.FromDto(request.Texts); + + if (banner is null) + { + db.Add(new Domain.Marketing.PromoBanner + { + Texts = texts, + HighlightedCode = request.HighlightedCode, + IsActive = request.IsActive, + }); + } + else + { + banner.Texts = texts; + banner.HighlightedCode = request.HighlightedCode; + banner.IsActive = request.IsActive; + banner.UpdatedAt = DateTime.UtcNow; + } + + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record CreateCampaignCommand( + string Subject, + string BodyHtml) : IRequest; + +public sealed class CreateCampaignCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreateCampaignCommand request, + CancellationToken cancellationToken) + { + var campaign = new Domain.Marketing.NewsletterCampaign + { + Subject = request.Subject, + BodyHtml = request.BodyHtml, + }; + db.Add(campaign); + await db.SaveChangesAsync(cancellationToken); + return campaign.Id; + } +} + +public sealed record SendCampaignCommand(Guid CampaignId) : IRequest; + +public sealed class SendCampaignCommandHandler( + IAppDbContext db, + IEmailService emailService) + : IRequestHandler +{ + public async Task Handle( + SendCampaignCommand request, + CancellationToken cancellationToken) + { + var campaign = await db.NewsletterCampaigns + .FirstOrDefaultAsync(c => c.Id == request.CampaignId, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Campaign not found."); + + var subscribers = await db.NewsletterSubscribers + .Where(s => s.IsActive) + .ToListAsync(cancellationToken); + + var sent = 0; + foreach (var subscriber in subscribers) + { + await emailService.SendAsync( + subscriber.Email, + campaign.Subject, + campaign.BodyHtml, + cancellationToken); + + db.Add(new Domain.Marketing.CampaignSend + { + CampaignId = campaign.Id, + SubscriberId = subscriber.Id, + SentAt = DateTime.UtcNow, + IsDelivered = true, + }); + sent++; + } + + campaign.Status = Domain.Enums.CampaignStatus.Sent; + campaign.SentAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + return sent; + } +} + +public sealed record GetSubscribersQuery( + int Page = 1, + int PageSize = 20, + string? Search = null) : IRequest>; + +public sealed record SubscriberDto( + Guid Id, + string Email, + bool IsActive, + DateTime CreatedAt); + +public sealed class GetSubscribersQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetSubscribersQuery request, + CancellationToken cancellationToken) + { + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + var query = db.NewsletterSubscribers.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var term = request.Search.Trim().ToLowerInvariant(); + query = query.Where(s => s.Email.ToLower().Contains(term)); + } + + var total = await query.CountAsync(cancellationToken); + var items = await query + .OrderByDescending(s => s.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(s => new SubscriberDto( + s.Id, s.Email, s.IsActive, s.CreatedAt)) + .ToListAsync(cancellationToken); + + return new PaginatedList + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = total, + }; + } +} + +public sealed record GetDiscountCodesQuery( + int Page = 1, + int PageSize = 20, + string? Search = null, + bool? IsActive = null) : IRequest>; + +public sealed record DiscountCodeAdminDto( + Guid Id, + string Code, + string RuleName, + int UsageCount, + int? MaxUsages, + bool IsActive); + +public sealed class GetDiscountCodesQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetDiscountCodesQuery request, + CancellationToken cancellationToken) + { + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + var query = db.DiscountCodes.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var term = request.Search.Trim().ToLowerInvariant(); + query = query.Where(c => + c.Code.ToLower().Contains(term) || + c.DiscountRule.Name.ToLower().Contains(term)); + } + + if (request.IsActive.HasValue) + { + query = query.Where(c => c.IsActive == request.IsActive.Value); + } + + var total = await query.CountAsync(cancellationToken); + var items = await query + .OrderByDescending(c => c.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(c => new DiscountCodeAdminDto( + c.Id, + c.Code, + c.DiscountRule.Name, + c.UsageCount, + c.MaxUsages, + c.IsActive)) + .ToListAsync(cancellationToken); + + return new PaginatedList + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = total, + }; + } +} + +public sealed record SetDiscountCodeActiveCommand( + Guid Id, + bool IsActive) : IRequest; + +public sealed class SetDiscountCodeActiveCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + SetDiscountCodeActiveCommand request, + CancellationToken cancellationToken) + { + var code = await db.DiscountCodes + .FirstOrDefaultAsync(c => c.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException( + "Discount code not found."); + + code.IsActive = request.IsActive; + code.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record GetCampaignsQuery( + int Page = 1, + int PageSize = 20, + string? Search = null) : IRequest>; + +public sealed record CampaignDto( + Guid Id, + string Subject, + Domain.Enums.CampaignStatus Status, + DateTime? SentAt, + DateTime CreatedAt); + +public sealed class GetCampaignsQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetCampaignsQuery request, + CancellationToken cancellationToken) + { + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + var query = db.NewsletterCampaigns.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var term = request.Search.Trim().ToLowerInvariant(); + query = query.Where(c => c.Subject.ToLower().Contains(term)); + } + + var total = await query.CountAsync(cancellationToken); + var items = await query + .OrderByDescending(c => c.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(c => new CampaignDto( + c.Id, c.Subject, c.Status, c.SentAt, c.CreatedAt)) + .ToListAsync(cancellationToken); + + return new PaginatedList + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = total, + }; + } +} diff --git a/src/backend/src/iLED.Application/Marketing/Commands/PromotionsCommands.cs b/src/backend/src/iLED.Application/Marketing/Commands/PromotionsCommands.cs new file mode 100644 index 0000000..046e9ea --- /dev/null +++ b/src/backend/src/iLED.Application/Marketing/Commands/PromotionsCommands.cs @@ -0,0 +1,521 @@ +using iLED.Application.Catalog; +using iLED.Application.Catalog.Dtos; +using iLED.Application.Common.Interfaces; +using iLED.Application.Common.Models; +using iLED.Application.Marketing; +using iLED.Domain.Enums; +using iLED.Domain.Marketing; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Marketing.Commands; + +public sealed record PromotionDto( + Guid Id, + string Name, + DiscountType Type, + decimal Value, + decimal? MinimumOrderAmount, + DateTime? ValidFrom, + DateTime? ValidTo, + bool IsActive, + int? MaxTotalRedemptions, + int? MaxRedemptionsPerCustomer, + bool ShowInBanner, + LocalizedTextDto BannerTexts, + string? BannerHighlightedCode, + bool IssueRewardOnPaidOrder, + decimal? RewardMinPaidOrderTotal, + string? RewardCodePrefix, + int? RewardValidDays, + int CodeCount, + int RedemptionCount); + +public sealed record PromotionCodeDto( + Guid Id, + string Code, + int UsageCount, + int? MaxUsages, + bool IsActive, + DiscountCodeSource Source, + string? IssuedToEmail, + DateTime? ValidUntil, + DateTime CreatedAt); + +public sealed record GetPromotionsQuery( + int Page = 1, + int PageSize = 20, + string? Search = null, + bool? IsActive = null, + DiscountType? Type = null) : IRequest>; + +public sealed class GetPromotionsQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetPromotionsQuery request, + CancellationToken cancellationToken) + { + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + var query = db.DiscountRules.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var term = request.Search.Trim().ToLowerInvariant(); + query = query.Where(r => + r.Name.ToLower().Contains(term) || + r.Codes.Any(c => c.Code.ToLower().Contains(term))); + } + + if (request.IsActive.HasValue) + { + query = query.Where(r => r.IsActive == request.IsActive.Value); + } + + if (request.Type.HasValue) + { + query = query.Where(r => r.Type == request.Type.Value); + } + + var total = await query.CountAsync(cancellationToken); + var rows = await query + .OrderByDescending(r => r.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(r => new + { + r.Id, + r.Name, + r.Type, + r.Value, + r.MinimumOrderAmount, + r.ValidFrom, + r.ValidTo, + r.IsActive, + r.MaxTotalRedemptions, + r.MaxRedemptionsPerCustomer, + r.ShowInBanner, + r.BannerTexts, + r.BannerHighlightedCode, + r.IssueRewardOnPaidOrder, + r.RewardMinPaidOrderTotal, + r.RewardCodePrefix, + r.RewardValidDays, + CodeCount = r.Codes.Count, + RedemptionCount = r.Redemptions.Count, + }) + .ToListAsync(cancellationToken); + + var items = rows.Select(r => new PromotionDto( + r.Id, + r.Name, + r.Type, + r.Value, + r.MinimumOrderAmount, + r.ValidFrom, + r.ValidTo, + r.IsActive, + r.MaxTotalRedemptions, + r.MaxRedemptionsPerCustomer, + r.ShowInBanner, + ProductLocalization.ToDto(r.BannerTexts), + r.BannerHighlightedCode, + r.IssueRewardOnPaidOrder, + r.RewardMinPaidOrderTotal, + r.RewardCodePrefix, + r.RewardValidDays, + r.CodeCount, + r.RedemptionCount)).ToList(); + + return new PaginatedList + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = total, + }; + } +} + +public sealed record GetPromotionQuery(Guid Id) : IRequest; + +public sealed class GetPromotionQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetPromotionQuery request, + CancellationToken cancellationToken) + { + var x = await db.DiscountRules + .AsNoTracking() + .Where(r => r.Id == request.Id) + .Select(r => new + { + r.Id, + r.Name, + r.Type, + r.Value, + r.MinimumOrderAmount, + r.ValidFrom, + r.ValidTo, + r.IsActive, + r.MaxTotalRedemptions, + r.MaxRedemptionsPerCustomer, + r.ShowInBanner, + r.BannerTexts, + r.BannerHighlightedCode, + r.IssueRewardOnPaidOrder, + r.RewardMinPaidOrderTotal, + r.RewardCodePrefix, + r.RewardValidDays, + CodeCount = r.Codes.Count, + RedemptionCount = r.Redemptions.Count, + }) + .FirstOrDefaultAsync(cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Promotion not found."); + + return new PromotionDto( + x.Id, + x.Name, + x.Type, + x.Value, + x.MinimumOrderAmount, + x.ValidFrom, + x.ValidTo, + x.IsActive, + x.MaxTotalRedemptions, + x.MaxRedemptionsPerCustomer, + x.ShowInBanner, + ProductLocalization.ToDto(x.BannerTexts), + x.BannerHighlightedCode, + x.IssueRewardOnPaidOrder, + x.RewardMinPaidOrderTotal, + x.RewardCodePrefix, + x.RewardValidDays, + x.CodeCount, + x.RedemptionCount); + } +} + +public sealed record GetPromotionCodesQuery( + Guid PromotionId, + int Page = 1, + int PageSize = 20) : IRequest>; + +public sealed class GetPromotionCodesQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetPromotionCodesQuery request, + CancellationToken cancellationToken) + { + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + var query = db.DiscountCodes + .AsNoTracking() + .Where(c => c.DiscountRuleId == request.PromotionId); + + var total = await query.CountAsync(cancellationToken); + var items = await query + .OrderByDescending(c => c.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(c => new PromotionCodeDto( + c.Id, + c.Code, + c.UsageCount, + c.MaxUsages, + c.IsActive, + c.Source, + c.IssuedToEmail, + c.ValidUntil, + c.CreatedAt)) + .ToListAsync(cancellationToken); + + return new PaginatedList + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = total, + }; + } +} + +public sealed record UpsertPromotionCommand( + Guid? Id, + string Name, + DiscountType Type, + decimal Value, + decimal? MinimumOrderAmount, + DateTime? ValidFrom, + DateTime? ValidTo, + bool IsActive, + int? MaxTotalRedemptions, + int? MaxRedemptionsPerCustomer, + bool ShowInBanner, + LocalizedTextDto? BannerTexts, + string? BannerHighlightedCode, + bool IssueRewardOnPaidOrder, + decimal? RewardMinPaidOrderTotal, + string? RewardCodePrefix, + int? RewardValidDays) : IRequest; + +public sealed class UpsertPromotionCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + UpsertPromotionCommand request, + CancellationToken cancellationToken) + { + DiscountRule rule; + if (request.Id.HasValue) + { + rule = await db.DiscountRules + .FirstOrDefaultAsync(r => r.Id == request.Id.Value, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException( + "Promotion not found."); + rule.UpdatedAt = DateTime.UtcNow; + } + else + { + rule = new DiscountRule(); + db.Add(rule); + } + + rule.Name = request.Name.Trim(); + rule.Type = request.Type; + rule.Value = request.Value; + rule.MinimumOrderAmount = request.MinimumOrderAmount; + rule.ValidFrom = request.ValidFrom; + rule.ValidTo = request.ValidTo; + rule.IsActive = request.IsActive; + rule.MaxTotalRedemptions = request.MaxTotalRedemptions; + rule.MaxRedemptionsPerCustomer = request.MaxRedemptionsPerCustomer; + rule.ShowInBanner = request.ShowInBanner; + rule.BannerTexts = ProductLocalization.FromOptional(request.BannerTexts); + rule.BannerHighlightedCode = request.BannerHighlightedCode? + .Trim() + .ToUpperInvariant(); + rule.IssueRewardOnPaidOrder = request.IssueRewardOnPaidOrder; + rule.RewardMinPaidOrderTotal = request.RewardMinPaidOrderTotal; + rule.RewardCodePrefix = string.IsNullOrWhiteSpace(request.RewardCodePrefix) + ? null + : request.RewardCodePrefix.Trim().ToUpperInvariant(); + rule.RewardValidDays = request.RewardValidDays; + + if (rule.ShowInBanner && + string.IsNullOrWhiteSpace(rule.BannerTexts.Pl)) + { + throw new Common.Exceptions.ValidationException( + "Banner PL text is required when show in banner is enabled."); + } + + await db.SaveChangesAsync(cancellationToken); + await SyncBannerAsync(db, cancellationToken); + return rule.Id; + } + + internal static async Task SyncBannerAsync( + IAppDbContext db, + CancellationToken cancellationToken) + { + var promo = await db.DiscountRules + .AsNoTracking() + .Where(r => r.IsActive && r.ShowInBanner) + .OrderByDescending(r => r.UpdatedAt ?? r.CreatedAt) + .Select(r => new + { + r.BannerTexts, + r.BannerHighlightedCode, + }) + .FirstOrDefaultAsync(cancellationToken); + + var banner = await db.PromoBanners + .OrderByDescending(b => b.CreatedAt) + .FirstOrDefaultAsync(cancellationToken); + + if (promo is null || + string.IsNullOrWhiteSpace(promo.BannerTexts.Pl)) + { + if (banner is not null) + { + banner.IsActive = false; + banner.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } + + return; + } + + if (banner is null) + { + db.Add(new PromoBanner + { + Texts = new Domain.Common.LocalizedStrings + { + Pl = promo.BannerTexts.Pl, + En = promo.BannerTexts.En, + De = promo.BannerTexts.De, + Cs = promo.BannerTexts.Cs, + }, + HighlightedCode = promo.BannerHighlightedCode, + IsActive = true, + }); + } + else + { + banner.Texts = new Domain.Common.LocalizedStrings + { + Pl = promo.BannerTexts.Pl, + En = promo.BannerTexts.En, + De = promo.BannerTexts.De, + Cs = promo.BannerTexts.Cs, + }; + banner.HighlightedCode = promo.BannerHighlightedCode; + banner.IsActive = true; + banner.UpdatedAt = DateTime.UtcNow; + } + + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record SetPromotionActiveCommand(Guid Id, bool IsActive) : IRequest; + +public sealed class SetPromotionActiveCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + SetPromotionActiveCommand request, + CancellationToken cancellationToken) + { + var rule = await db.DiscountRules + .FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Promotion not found."); + + rule.IsActive = request.IsActive; + rule.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + await UpsertPromotionCommandHandler.SyncBannerAsync(db, cancellationToken); + } +} + +public sealed record CreatePromotionCodeCommand( + Guid PromotionId, + string? Code, + int? MaxUsages) : IRequest; + +public sealed class CreatePromotionCodeCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreatePromotionCodeCommand request, + CancellationToken cancellationToken) + { + var exists = await db.DiscountRules + .AnyAsync(r => r.Id == request.PromotionId, cancellationToken); + if (!exists) + { + throw new Common.Exceptions.NotFoundException("Promotion not found."); + } + + var codeValue = string.IsNullOrWhiteSpace(request.Code) + ? $"PROMO{Random.Shared.Next(100000, 999999)}" + : request.Code.Trim().ToUpperInvariant(); + + if (await db.DiscountCodes.AnyAsync(c => c.Code == codeValue, cancellationToken)) + { + throw new Common.Exceptions.ValidationException( + "Kod już istnieje."); + } + + db.Add(new DiscountCode + { + DiscountRuleId = request.PromotionId, + Code = codeValue, + MaxUsages = request.MaxUsages, + Source = DiscountCodeSource.Manual, + }); + await db.SaveChangesAsync(cancellationToken); + return codeValue; + } +} + +public sealed record SetPromotionCodeActiveCommand( + Guid Id, + bool IsActive) : IRequest; + +public sealed class SetPromotionCodeActiveCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + SetPromotionCodeActiveCommand request, + CancellationToken cancellationToken) + { + var code = await db.DiscountCodes + .FirstOrDefaultAsync(c => c.Id == request.Id, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Code not found."); + + code.IsActive = request.IsActive; + code.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record ValidatePromotionCodeQuery( + string Code, + decimal Subtotal, + string? CustomerEmail) : IRequest; + +public sealed class ValidatePromotionCodeQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + ValidatePromotionCodeQuery request, + CancellationToken cancellationToken) + { + var code = await db.DiscountCodes + .AsNoTracking() + .Include(c => c.DiscountRule) + .FirstOrDefaultAsync( + c => c.Code == request.Code.Trim().ToUpperInvariant(), + cancellationToken); + + var email = request.CustomerEmail?.Trim().ToLowerInvariant() ?? ""; + var ruleId = code?.DiscountRuleId ?? Guid.Empty; + + var ruleCount = 0; + var customerCount = 0; + if (code is not null) + { + ruleCount = await db.DiscountRedemptions + .CountAsync(r => r.DiscountRuleId == ruleId, cancellationToken); + if (!string.IsNullOrEmpty(email)) + { + customerCount = await db.DiscountRedemptions + .CountAsync( + r => r.DiscountRuleId == ruleId && + r.CustomerEmail == email, + cancellationToken); + } + } + + var eval = PromotionEvaluation.Evaluate( + code, + request.Subtotal, + email, + null, + ruleCount, + customerCount, + DateTime.UtcNow); + + return new DiscountValidationDto( + eval.IsValid, + eval.ErrorCode, + eval.MinOrderAmount, + eval.IsValid ? eval.DiscountAmount : null, + eval.FreeShipping); + } +} diff --git a/src/backend/src/iLED.Application/Marketing/PromotionEvaluation.cs b/src/backend/src/iLED.Application/Marketing/PromotionEvaluation.cs new file mode 100644 index 0000000..cdcaade --- /dev/null +++ b/src/backend/src/iLED.Application/Marketing/PromotionEvaluation.cs @@ -0,0 +1,122 @@ +using iLED.Domain.Enums; +using iLED.Domain.Marketing; + +namespace iLED.Application.Marketing; + +public static class PromotionErrorCodes +{ + public const string Invalid = "Invalid"; + public const string Inactive = "Inactive"; + public const string NotYetActive = "NotYetActive"; + public const string Expired = "Expired"; + public const string MinOrder = "MinOrder"; + public const string GlobalCap = "GlobalCap"; + public const string CustomerCap = "CustomerCap"; + public const string WrongEmail = "WrongEmail"; +} + +public sealed record PromotionEvalResult( + bool IsValid, + string? ErrorCode, + decimal? MinOrderAmount, + decimal DiscountAmount, + bool FreeShipping, + DiscountCode? Code, + DiscountRule? Rule); + +/// +/// Shared promotion validation for cart preview and order create. +/// +public static class PromotionEvaluation +{ + public static PromotionEvalResult Evaluate( + DiscountCode? code, + decimal subtotal, + string customerEmail, + string? userId, + int ruleRedemptionCount, + int customerRedemptionCount, + DateTime utcNow) + { + if (code is null) + { + return Invalid(PromotionErrorCodes.Invalid); + } + + if (!code.CanBeUsed) + { + return Invalid(PromotionErrorCodes.Expired); + } + + var rule = code.DiscountRule; + if (rule is null || !rule.IsActive) + { + return Invalid(PromotionErrorCodes.Inactive); + } + + if (rule.ValidFrom.HasValue && utcNow < rule.ValidFrom.Value) + { + return Invalid(PromotionErrorCodes.NotYetActive); + } + + if (rule.ValidTo.HasValue && utcNow > rule.ValidTo.Value) + { + return Invalid(PromotionErrorCodes.Expired); + } + + if (rule.MinimumOrderAmount.HasValue && + subtotal < rule.MinimumOrderAmount.Value) + { + return Invalid( + PromotionErrorCodes.MinOrder, + rule.MinimumOrderAmount.Value); + } + + if (rule.MaxTotalRedemptions.HasValue && + ruleRedemptionCount >= rule.MaxTotalRedemptions.Value) + { + return Invalid(PromotionErrorCodes.GlobalCap); + } + + if (rule.MaxRedemptionsPerCustomer.HasValue && + !string.IsNullOrWhiteSpace(customerEmail) && + customerRedemptionCount >= rule.MaxRedemptionsPerCustomer.Value) + { + return Invalid(PromotionErrorCodes.CustomerCap); + } + + if (code.Source == DiscountCodeSource.Reward && + !string.IsNullOrWhiteSpace(code.IssuedToEmail) && + !string.Equals( + code.IssuedToEmail.Trim(), + customerEmail.Trim(), + StringComparison.OrdinalIgnoreCase)) + { + return Invalid(PromotionErrorCodes.WrongEmail); + } + + var discountAmount = rule.Type switch + { + DiscountType.Percentage => + Math.Round(subtotal * rule.Value / 100m, 2), + DiscountType.FixedAmount => + Math.Min(rule.Value, subtotal), + _ => 0m, + }; + var freeShipping = rule.Type == DiscountType.FreeShipping; + + return new PromotionEvalResult( + true, + null, + null, + discountAmount, + freeShipping, + code, + rule); + } + + private static PromotionEvalResult Invalid( + string errorCode, + decimal? minOrderAmount = null) => + new(false, errorCode, minOrderAmount, 0m, false, null, null); +} diff --git a/src/backend/src/iLED.Application/Orders/Commands/AdminOrderQueries.cs b/src/backend/src/iLED.Application/Orders/Commands/AdminOrderQueries.cs new file mode 100644 index 0000000..d3d9d2a --- /dev/null +++ b/src/backend/src/iLED.Application/Orders/Commands/AdminOrderQueries.cs @@ -0,0 +1,586 @@ +using iLED.Application.Common.Interfaces; +using iLED.Application.Shipping.Commands; +using iLED.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Orders.Commands; + +public sealed record GetAdminOrdersQuery( + int Page = 1, + int PageSize = 20, + OrderStatus? Status = null, + PaymentStatus? PaymentStatus = null, + string? Search = null, + string? Month = null, + string SortBy = "date", + string SortDir = "desc") : IRequest; + +public sealed record AdminOrdersResult( + IReadOnlyList Items, + int TotalCount, + int Page, + int PageSize); + +public sealed record AdminOrderDto( + Guid Id, + string OrderNumber, + string CustomerEmail, + string CustomerFirstName, + string CustomerLastName, + OrderStatus Status, + PaymentStatus? PaymentStatus, + string? PaymentProvider, + decimal Total, + DateTime CreatedAt, + int ItemCount); + +public sealed class GetAdminOrdersQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetAdminOrdersQuery request, + CancellationToken cancellationToken) + { + var query = db.Orders.AsNoTracking(); + + if (request.Status.HasValue) + { + query = query.Where(o => o.Status == request.Status.Value); + } + + if (request.PaymentStatus.HasValue) + { + query = query.Where( + o => o.Payment != null && + o.Payment.Status == request.PaymentStatus.Value); + } + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var term = request.Search.Trim().ToLower(); + query = query.Where(o => + o.OrderNumber.ToLower().Contains(term) || + o.CustomerEmail.ToLower().Contains(term) || + o.CustomerFirstName.ToLower().Contains(term) || + o.CustomerLastName.ToLower().Contains(term)); + } + + if (TryParseMonth(request.Month, out var monthStart, out var monthEnd)) + { + query = query.Where( + o => o.CreatedAt >= monthStart && o.CreatedAt < monthEnd); + } + + var total = await query.CountAsync(cancellationToken); + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + + var descending = !string.Equals( + request.SortDir, + "asc", + StringComparison.OrdinalIgnoreCase); + + query = request.SortBy?.ToLowerInvariant() switch + { + "total" => descending + ? query.OrderByDescending(o => o.Total) + : query.OrderBy(o => o.Total), + "number" => descending + ? query.OrderByDescending(o => o.OrderNumber) + : query.OrderBy(o => o.OrderNumber), + _ => descending + ? query.OrderByDescending(o => o.CreatedAt) + : query.OrderBy(o => o.CreatedAt), + }; + + var items = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(o => new AdminOrderDto( + o.Id, + o.OrderNumber, + o.CustomerEmail, + o.CustomerFirstName, + o.CustomerLastName, + o.Status, + o.Payment != null ? o.Payment.Status : (PaymentStatus?)null, + o.Payment != null ? o.Payment.Provider : null, + o.Total, + o.CreatedAt, + o.Lines.Count)) + .ToListAsync(cancellationToken); + + return new AdminOrdersResult(items, total, page, pageSize); + } + + private static bool TryParseMonth( + string? value, + out DateTime start, + out DateTime end) + { + start = default; + end = default; + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + if (!DateTime.TryParseExact( + $"{value.Trim()}-01", + "yyyy-MM-dd", + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.AssumeUniversal | + System.Globalization.DateTimeStyles.AdjustToUniversal, + out start)) + { + return false; + } + + end = start.AddMonths(1); + return true; + } +} + +public sealed record GetAdminOrderDetailQuery(Guid Id) + : IRequest; + +public sealed record AdminOrderLineDto( + string ProductName, + string VariantColor, + string Sku, + decimal UnitPrice, + int Quantity, + decimal LineTotal); + +public sealed record AdminOrderDetailLineDto( + Guid Id, + Guid ProductVariantId, + string ProductName, + string VariantColor, + string Sku, + decimal UnitPrice, + int Quantity, + decimal LineTotal, + bool IsCollected, + Guid? PickedFromWarehouseId, + string? PickedFromWarehouseName, + string? PickedFromWarehouseCode, + DateTime? CollectedAt, + IReadOnlyList WarehouseStock); + +public sealed record AdminOrderDetailDto( + Guid Id, + string OrderNumber, + OrderStatus Status, + PaymentStatus? PaymentStatus, + string? PaymentProvider, + string? PaymentExternalId, + string CustomerEmail, + string CustomerPhone, + string CustomerFirstName, + string CustomerLastName, + ShippingCarrier ShippingCarrier, + ShippingServiceType ShippingServiceType, + string? ParcelPointId, + string? ParcelPointName, + string? ParcelPointAddress, + string ShippingStreet, + string ShippingCity, + string ShippingPostalCode, + string ShippingCountry, + PaymentMethod PaymentMethod, + string? CustomerNotes, + decimal Subtotal, + decimal DeliveryCost, + decimal DiscountAmount, + decimal VatAmount, + decimal Total, + DateTime CreatedAt, + ShipmentDto? Shipment, + IReadOnlyList Lines); + +public sealed class GetAdminOrderDetailQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetAdminOrderDetailQuery request, + CancellationToken cancellationToken) + { + var order = await db.Orders + .AsNoTracking() + .Where(o => o.Id == request.Id) + .Select(o => new + { + o.Id, + o.OrderNumber, + o.Status, + PaymentStatus = o.Payment != null + ? o.Payment.Status + : (PaymentStatus?)null, + PaymentProvider = o.Payment != null + ? o.Payment.Provider + : null, + PaymentExternalId = o.Payment != null + ? o.Payment.ExternalId + : null, + o.CustomerEmail, + o.CustomerPhone, + o.CustomerFirstName, + o.CustomerLastName, + o.ShippingCarrier, + o.ShippingServiceType, + o.ParcelPointId, + o.ParcelPointName, + o.ParcelPointAddress, + o.ShippingStreet, + o.ShippingCity, + o.ShippingPostalCode, + o.ShippingCountry, + o.PaymentMethod, + o.CustomerNotes, + o.Subtotal, + o.DeliveryCost, + o.DiscountAmount, + o.VatAmount, + o.Total, + o.CreatedAt, + Shipment = o.Shipment == null + ? null + : new ShipmentDto( + o.Shipment.Id, + o.Shipment.OrderId, + o.Shipment.Carrier, + o.Shipment.ServiceType, + o.Shipment.Status, + o.Shipment.TrackingNumber, + o.Shipment.ExternalShipmentId, + o.Shipment.LabelRelativePath, + o.Shipment.ParcelPointName, + o.Shipment.ParcelPointAddress, + o.Shipment.ErrorMessage), + Lines = o.Lines.Select(l => new + { + l.Id, + l.ProductVariantId, + l.ProductName, + l.VariantColor, + l.Sku, + l.UnitPrice, + l.Quantity, + l.CollectedAt, + l.PickedFromWarehouseId, + PickedFromWarehouseName = l.PickedFromWarehouse != null + ? l.PickedFromWarehouse.Name + : null, + PickedFromWarehouseCode = l.PickedFromWarehouse != null + ? l.PickedFromWarehouse.Code + : null, + }).ToList(), + }) + .FirstOrDefaultAsync(cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Order not found."); + + var variantIds = order.Lines + .Select(l => l.ProductVariantId) + .Distinct() + .ToList(); + + var warehouseStocks = await db.ProductVariants + .AsNoTracking() + .Where(v => variantIds.Contains(v.Id)) + .Select(v => new + { + v.Id, + Stocks = v.WarehouseStocks + .OrderBy(ws => ws.Warehouse.SortOrder) + .Select(ws => new PickListWarehouseQtyDto( + ws.WarehouseId, + ws.Warehouse.Name, + ws.Warehouse.Code, + ws.Quantity)) + .ToList(), + }) + .ToListAsync(cancellationToken); + + var stockLookup = warehouseStocks.ToDictionary(v => v.Id, v => v.Stocks); + + var lines = order.Lines + .Select(l => new AdminOrderDetailLineDto( + l.Id, + l.ProductVariantId, + l.ProductName, + l.VariantColor, + l.Sku, + l.UnitPrice, + l.Quantity, + l.UnitPrice * l.Quantity, + l.CollectedAt.HasValue, + l.PickedFromWarehouseId, + l.PickedFromWarehouseName, + l.PickedFromWarehouseCode, + l.CollectedAt, + stockLookup.GetValueOrDefault(l.ProductVariantId) ?? [])) + .ToList(); + + return new AdminOrderDetailDto( + order.Id, + order.OrderNumber, + order.Status, + order.PaymentStatus, + order.PaymentProvider, + order.PaymentExternalId, + order.CustomerEmail, + order.CustomerPhone, + order.CustomerFirstName, + order.CustomerLastName, + order.ShippingCarrier, + order.ShippingServiceType, + order.ParcelPointId, + order.ParcelPointName, + order.ParcelPointAddress, + order.ShippingStreet, + order.ShippingCity, + order.ShippingPostalCode, + order.ShippingCountry, + order.PaymentMethod, + order.CustomerNotes, + order.Subtotal, + order.DeliveryCost, + order.DiscountAmount, + order.VatAmount, + order.Total, + order.CreatedAt, + order.Shipment, + lines); + } +} + +public sealed record GetPickListQuery( + OrderStatus? Status = null, + bool IncludeCollected = false, + Guid? OrderId = null) : IRequest; + +public sealed record PickListResult( + IReadOnlyList Items, + int OrderCount, + int PendingCount, + int CollectedCount); + +public sealed record PickListLineDto( + Guid OrderLineId, + Guid OrderId, + string OrderNumber, + Guid ProductVariantId, + string ProductName, + string VariantColor, + string Sku, + int Quantity, + bool IsCollected, + Guid? PickedFromWarehouseId, + string? PickedFromWarehouseName, + string? PickedFromWarehouseCode, + DateTime? CollectedAt, + IReadOnlyList WarehouseStock); + +public sealed record PickListWarehouseQtyDto( + Guid WarehouseId, + string WarehouseName, + string WarehouseCode, + int Quantity); + +public sealed class GetPickListQueryHandler(IAppDbContext db) + : IRequestHandler +{ + private static readonly OrderStatus[] DefaultStatuses = + [ + OrderStatus.Paid, + OrderStatus.Processing, + ]; + + public async Task Handle( + GetPickListQuery request, + CancellationToken cancellationToken) + { + var statuses = request.Status.HasValue + ? [request.Status.Value] + : DefaultStatuses; + + var lines = await db.Orders + .AsNoTracking() + .Where(o => statuses.Contains(o.Status)) + .Where(o => !request.OrderId.HasValue || o.Id == request.OrderId.Value) + .SelectMany(o => o.Lines.Select(l => new + { + LineId = l.Id, + OrderId = o.Id, + o.OrderNumber, + l.ProductVariantId, + l.ProductName, + l.VariantColor, + l.Sku, + l.Quantity, + l.CollectedAt, + l.PickedFromWarehouseId, + PickedFromWarehouseName = l.PickedFromWarehouse != null + ? l.PickedFromWarehouse.Name + : null, + PickedFromWarehouseCode = l.PickedFromWarehouse != null + ? l.PickedFromWarehouse.Code + : null, + })) + .ToListAsync(cancellationToken); + + if (!request.IncludeCollected) + { + lines = lines.Where(l => l.CollectedAt == null).ToList(); + } + + var orderCount = lines + .Select(l => l.OrderNumber) + .Distinct() + .Count(); + + var pendingCount = lines.Count(l => l.CollectedAt == null); + var collectedCount = lines.Count(l => l.CollectedAt != null); + + var variantIds = lines + .Select(l => l.ProductVariantId) + .Distinct() + .ToList(); + + var warehouseStocks = await db.ProductVariants + .AsNoTracking() + .Where(v => variantIds.Contains(v.Id)) + .Select(v => new + { + v.Id, + Stocks = v.WarehouseStocks + .OrderBy(ws => ws.Warehouse.SortOrder) + .Select(ws => new PickListWarehouseQtyDto( + ws.WarehouseId, + ws.Warehouse.Name, + ws.Warehouse.Code, + ws.Quantity)) + .ToList(), + }) + .ToListAsync(cancellationToken); + + var stockLookup = warehouseStocks.ToDictionary(v => v.Id, v => v.Stocks); + + var items = lines + .OrderBy(l => l.CollectedAt.HasValue) + .ThenBy(l => l.OrderNumber) + .ThenBy(l => l.ProductName) + .Select(l => new PickListLineDto( + l.LineId, + l.OrderId, + l.OrderNumber, + l.ProductVariantId, + l.ProductName, + l.VariantColor, + l.Sku, + l.Quantity, + l.CollectedAt.HasValue, + l.PickedFromWarehouseId, + l.PickedFromWarehouseName, + l.PickedFromWarehouseCode, + l.CollectedAt, + stockLookup.GetValueOrDefault(l.ProductVariantId) ?? [])) + .ToList(); + + return new PickListResult(items, orderCount, pendingCount, collectedCount); + } +} + +public sealed record GetOrderInvoiceQuery(Guid OrderId) : IRequest; + +public sealed record OrderInvoiceDto( + string InvoiceNumber, + DateTime IssueDate, + string SellerName, + string SellerAddress, + string SellerTaxId, + string BuyerName, + string BuyerEmail, + string BuyerAddress, + string OrderNumber, + IReadOnlyList Lines, + decimal Subtotal, + decimal DeliveryCost, + decimal DiscountAmount, + decimal VatAmount, + decimal Total, + PaymentStatus? PaymentStatus, + OrderStatus OrderStatus); + +public sealed class GetOrderInvoiceQueryHandler(IAppDbContext db) + : IRequestHandler +{ + private const string SellerName = "iLED Sp. z o.o."; + private const string SellerAddress = "ul. Przykładowa 1, 00-001 Warszawa"; + private const string SellerTaxId = "PL1234567890"; + + public async Task Handle( + GetOrderInvoiceQuery request, + CancellationToken cancellationToken) + { + var order = await db.Orders + .AsNoTracking() + .Where(o => o.Id == request.OrderId) + .Select(o => new + { + o.OrderNumber, + o.Status, + o.CustomerEmail, + o.CustomerFirstName, + o.CustomerLastName, + o.ShippingStreet, + o.ShippingCity, + o.ShippingPostalCode, + o.ShippingCountry, + o.Subtotal, + o.DeliveryCost, + o.DiscountAmount, + o.VatAmount, + o.Total, + o.CreatedAt, + PaymentStatus = o.Payment != null + ? o.Payment.Status + : (PaymentStatus?)null, + Lines = o.Lines.Select(l => new AdminOrderLineDto( + l.ProductName, + l.VariantColor, + l.Sku, + l.UnitPrice, + l.Quantity, + l.LineTotal)).ToList(), + }) + .FirstOrDefaultAsync(cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Order not found."); + + var buyerAddress = + $"{order.ShippingStreet}, {order.ShippingPostalCode} " + + $"{order.ShippingCity}, {order.ShippingCountry}"; + + return new OrderInvoiceDto( + $"FV/{order.OrderNumber}", + order.CreatedAt, + SellerName, + SellerAddress, + SellerTaxId, + $"{order.CustomerFirstName} {order.CustomerLastName}".Trim(), + order.CustomerEmail, + buyerAddress, + order.OrderNumber, + order.Lines, + order.Subtotal, + order.DeliveryCost, + order.DiscountAmount, + order.VatAmount, + order.Total, + order.PaymentStatus, + order.Status); + } +} diff --git a/src/backend/src/iLED.Application/Orders/Commands/CollectOrderCommand.cs b/src/backend/src/iLED.Application/Orders/Commands/CollectOrderCommand.cs new file mode 100644 index 0000000..61b7d56 --- /dev/null +++ b/src/backend/src/iLED.Application/Orders/Commands/CollectOrderCommand.cs @@ -0,0 +1,51 @@ +using iLED.Application.Common.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Orders.Commands; + +public sealed record CollectOrderLineWarehouseDto( + Guid OrderLineId, + Guid WarehouseId); + +public sealed record CollectOrderCommand( + Guid OrderId, + IReadOnlyList Lines) : IRequest; + +public sealed class CollectOrderCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CollectOrderCommand request, + CancellationToken cancellationToken) + { + if (request.Lines.Count == 0) + { + throw new Common.Exceptions.ValidationException( + "Wybierz pozycje do kompletacji."); + } + + var order = await db.Orders + .Include(o => o.Lines) + .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Order not found."); + + OrderCollectionHelper.EnsurePickable(order); + + foreach (var entry in request.Lines) + { + var line = order.Lines.FirstOrDefault(l => l.Id == entry.OrderLineId) + ?? throw new Common.Exceptions.ValidationException( + "Pozycja nie należy do tego zamówienia."); + + await OrderCollectionHelper.CollectLineAsync( + db, + order, + line, + entry.WarehouseId, + cancellationToken); + } + + await db.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/backend/src/iLED.Application/Orders/Commands/CollectOrderLineCommand.cs b/src/backend/src/iLED.Application/Orders/Commands/CollectOrderLineCommand.cs new file mode 100644 index 0000000..7b19ce7 --- /dev/null +++ b/src/backend/src/iLED.Application/Orders/Commands/CollectOrderLineCommand.cs @@ -0,0 +1,36 @@ +using iLED.Application.Common.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Orders.Commands; + +public sealed record CollectOrderLineCommand( + Guid OrderLineId, + Guid WarehouseId) : IRequest; + +public sealed class CollectOrderLineCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CollectOrderLineCommand request, + CancellationToken cancellationToken) + { + var order = await db.Orders + .Include(o => o.Lines) + .FirstOrDefaultAsync( + o => o.Lines.Any(l => l.Id == request.OrderLineId), + cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Order line not found."); + + var line = order.Lines.First(l => l.Id == request.OrderLineId); + + await OrderCollectionHelper.CollectLineAsync( + db, + order, + line, + request.WarehouseId, + cancellationToken); + + await db.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/backend/src/iLED.Application/Orders/Commands/OrderCollectionHelper.cs b/src/backend/src/iLED.Application/Orders/Commands/OrderCollectionHelper.cs new file mode 100644 index 0000000..98b3e29 --- /dev/null +++ b/src/backend/src/iLED.Application/Orders/Commands/OrderCollectionHelper.cs @@ -0,0 +1,89 @@ +using iLED.Application.Common.Interfaces; +using iLED.Domain.Catalog; +using iLED.Domain.Enums; +using iLED.Domain.Orders; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Orders.Commands; + +internal static class OrderCollectionHelper +{ + internal static readonly OrderStatus[] PickableStatuses = + [ + OrderStatus.Paid, + OrderStatus.Processing, + ]; + + internal static void EnsurePickable(Order order) + { + if (!PickableStatuses.Contains(order.Status)) + { + throw new Common.Exceptions.ValidationException( + "Pozycję można kompletować tylko dla opłaconych zamówień."); + } + } + + internal static async Task CollectLineAsync( + IAppDbContext db, + Order order, + OrderLine line, + Guid warehouseId, + CancellationToken cancellationToken) + { + EnsurePickable(order); + + if (line.CollectedAt.HasValue) + { + throw new Common.Exceptions.ValidationException( + "Pozycja została już skompletowana."); + } + + var warehouseExists = await db.Warehouses + .AnyAsync(w => w.Id == warehouseId, cancellationToken); + + if (!warehouseExists) + { + throw new Common.Exceptions.NotFoundException("Warehouse not found."); + } + + var variant = await db.ProductVariants + .Include(v => v.Stock) + .Include(v => v.WarehouseStocks) + .FirstOrDefaultAsync( + v => v.Id == line.ProductVariantId, + cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Variant not found."); + + var warehouseStock = variant.WarehouseStocks + .FirstOrDefault(ws => ws.WarehouseId == warehouseId); + + if (warehouseStock is null || warehouseStock.Quantity < line.Quantity) + { + throw new Common.Exceptions.ValidationException( + "Niewystarczający stan w wybranym magazynie."); + } + + DeductStock(variant, warehouseStock, line.Quantity); + + line.PickedFromWarehouseId = warehouseId; + line.CollectedAt = DateTime.UtcNow; + line.UpdatedAt = DateTime.UtcNow; + + if (order.Status == OrderStatus.Paid) + { + order.Status = OrderStatus.Processing; + order.UpdatedAt = DateTime.UtcNow; + } + } + + private static void DeductStock( + ProductVariant variant, + WarehouseStock warehouseStock, + int quantity) + { + warehouseStock.Quantity -= quantity; + warehouseStock.UpdatedAt = DateTime.UtcNow; + variant.Stock.Quantity = variant.WarehouseStocks.Sum(ws => ws.Quantity); + variant.Stock.UpdatedAt = DateTime.UtcNow; + } +} diff --git a/src/backend/src/iLED.Application/Orders/Commands/OrderCommands.cs b/src/backend/src/iLED.Application/Orders/Commands/OrderCommands.cs new file mode 100644 index 0000000..3090b27 --- /dev/null +++ b/src/backend/src/iLED.Application/Orders/Commands/OrderCommands.cs @@ -0,0 +1,212 @@ +using iLED.Application.Common.Interfaces; +using iLED.Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Orders.Commands; + +public sealed record CreateOrderCommand( + string CustomerEmail, + string CustomerPhone, + string CustomerFirstName, + string CustomerLastName, + string ShippingStreet, + string ShippingCity, + string ShippingPostalCode, + string ShippingCountry, + string? BillingStreet, + string? BillingCity, + string? BillingPostalCode, + string? BillingCountry, + ShippingCarrier ShippingCarrier, + ShippingServiceType ShippingServiceType, + string? ParcelPointId, + string? ParcelPointName, + string? ParcelPointAddress, + PaymentMethod PaymentMethod, + string? CustomerNotes, + string? DiscountCode, + string? Locale = null) : IRequest; + +public sealed record OrderDto( + Guid Id, + string OrderNumber, + OrderStatus Status, + decimal Subtotal, + decimal DeliveryCost, + decimal DiscountAmount, + decimal VatAmount, + decimal Total, + string CustomerEmail, + string CustomerPhone, + string CustomerFirstName, + string CustomerLastName, + ShippingCarrier ShippingCarrier, + ShippingServiceType ShippingServiceType, + string? ParcelPointId, + string? ParcelPointName, + string? ParcelPointAddress, + PaymentMethod PaymentMethod, + IReadOnlyList Lines, + string? PaymentRedirectUrl); + +public sealed record OrderLineDto( + string ProductName, + string VariantColor, + decimal UnitPrice, + int Quantity, + decimal LineTotal); + +public interface IOrderService +{ + Task CreateOrderAsync( + CreateOrderCommand command, + CancellationToken cancellationToken); + + Task GetOrderAsync( + Guid orderId, + CancellationToken cancellationToken); + + Task> GetUserOrdersAsync( + CancellationToken cancellationToken); + + Task UpdateOrderStatusAsync( + Guid orderId, + OrderStatus status, + CancellationToken cancellationToken); +} + +public sealed record OrderSummaryDto( + Guid Id, + string OrderNumber, + OrderStatus Status, + decimal Total, + DateTime CreatedAt, + int ItemCount); + +public sealed class CreateOrderCommandHandler(IOrderService orders) + : IRequestHandler +{ + public Task Handle( + CreateOrderCommand request, + CancellationToken cancellationToken) => + orders.CreateOrderAsync(request, cancellationToken); +} + +public sealed record GetOrderQuery(Guid OrderId) : IRequest; + +public sealed class GetOrderQueryHandler(IOrderService orders) + : IRequestHandler +{ + public Task Handle( + GetOrderQuery request, + CancellationToken cancellationToken) => + orders.GetOrderAsync(request.OrderId, cancellationToken); +} + +public sealed record GetUserOrdersQuery() : IRequest>; + +public sealed class GetUserOrdersQueryHandler(IOrderService orders) + : IRequestHandler> +{ + public Task> Handle( + GetUserOrdersQuery request, + CancellationToken cancellationToken) => + orders.GetUserOrdersAsync(cancellationToken); +} + +public sealed record UpdateOrderStatusCommand( + Guid OrderId, + OrderStatus Status) : IRequest; + +public sealed class UpdateOrderStatusCommandHandler(IOrderService orders) + : IRequestHandler +{ + public Task Handle( + UpdateOrderStatusCommand request, + CancellationToken cancellationToken) => + orders.UpdateOrderStatusAsync( + request.OrderId, + request.Status, + cancellationToken); +} + +public sealed record GetDashboardStatsQuery() : IRequest; + +public sealed record DashboardStatsDto( + int OrdersToday, + decimal RevenueToday, + int LowStockCount, + int PendingOrders); + +public sealed class GetDashboardStatsQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetDashboardStatsQuery request, + CancellationToken cancellationToken) + { + var today = DateTime.UtcNow.Date; + + var ordersToday = await db.Orders + .CountAsync(o => o.CreatedAt >= today, cancellationToken); + + var revenueToday = await db.Orders + .Where(o => o.CreatedAt >= today && + o.Status != OrderStatus.Cancelled) + .SumAsync(o => o.Total, cancellationToken); + + var pendingOrders = await db.Orders + .CountAsync(o => o.Status == OrderStatus.PendingPayment || + o.Status == OrderStatus.Paid, + cancellationToken); + + var lowStock = await db.ProductVariants + .CountAsync(v => v.Stock.Quantity <= v.Stock.LowStockThreshold, + cancellationToken); + + return new DashboardStatsDto( + ordersToday, + revenueToday, + lowStock, + pendingOrders); + } +} + +public sealed record GetDashboardOrdersChartQuery(int Days = 14) + : IRequest>; + +public sealed record OrdersChartPointDto(string Date, int Count); + +public sealed class GetDashboardOrdersChartQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetDashboardOrdersChartQuery request, + CancellationToken cancellationToken) + { + var days = Math.Clamp(request.Days, 7, 30); + var startDate = DateTime.UtcNow.Date.AddDays(-(days - 1)); + + var counts = await db.Orders + .AsNoTracking() + .Where(o => o.CreatedAt >= startDate) + .GroupBy(o => o.CreatedAt.Date) + .Select(g => new { Date = g.Key, Count = g.Count() }) + .ToListAsync(cancellationToken); + + var lookup = counts.ToDictionary(x => x.Date, x => x.Count); + var points = new List(); + + for (var i = 0; i < days; i++) + { + var date = startDate.AddDays(i); + lookup.TryGetValue(date, out var count); + points.Add(new OrdersChartPointDto( + date.ToString("yyyy-MM-dd"), + count)); + } + + return points; + } +} diff --git a/src/backend/src/iLED.Application/Payments/Commands/PaymentCommands.cs b/src/backend/src/iLED.Application/Payments/Commands/PaymentCommands.cs new file mode 100644 index 0000000..0a96168 --- /dev/null +++ b/src/backend/src/iLED.Application/Payments/Commands/PaymentCommands.cs @@ -0,0 +1,276 @@ +using iLED.Application.Common.Interfaces; +using iLED.Application.Orders.Commands; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace iLED.Application.Payments.Commands; + +public sealed record InitiatePaymentCommand(Guid OrderId) : IRequest; + +public sealed record PaymentResultDto( + bool Success, + string? RedirectUrl, + string? ErrorMessage); + +public sealed class InitiatePaymentCommandHandler( + IOrderService orderService, + IPaymentService paymentService, + IAppDbContext db, + IConfiguration configuration) + : IRequestHandler +{ + public async Task Handle( + InitiatePaymentCommand request, + CancellationToken cancellationToken) + { + var order = await orderService.GetOrderAsync( + request.OrderId, cancellationToken); + + var shopBase = configuration["Seo:ShopBaseUrl"] ?? "http://localhost:4200"; + var notifyUrl = configuration["PayU:NotifyUrl"] + ?? "http://localhost:5000/api/v1/payments/webhook"; + var continueBase = configuration["PayU:ContinueUrl"] ?? $"{shopBase}/order"; + + var paymentRequest = new PaymentInitiationRequest( + order.Id, + order.OrderNumber, + order.Total, + order.CustomerEmail, + order.CustomerFirstName, + order.CustomerLastName, + $"{continueBase.TrimEnd('/')}/{order.Id}", + notifyUrl); + + var result = await paymentService.InitiatePaymentAsync( + paymentRequest, cancellationToken); + + if (!result.Success) + { + return new PaymentResultDto(false, null, result.ErrorMessage); + } + + var payment = new Domain.Payments.Payment + { + OrderId = order.Id, + ExternalId = result.ExternalId ?? string.Empty, + Amount = order.Total, + RedirectUrl = result.RedirectUrl, + }; + db.Add(payment); + await db.SaveChangesAsync(cancellationToken); + + return new PaymentResultDto(true, result.RedirectUrl, null); + } +} + +public sealed record CompleteSandboxPaymentCommand(Guid OrderId) : IRequest; + +public sealed class CompleteSandboxPaymentCommandHandler( + IOrderService orderService, + IAppDbContext db, + IConfiguration configuration) + : IRequestHandler +{ + public async Task Handle( + CompleteSandboxPaymentCommand request, + CancellationToken cancellationToken) + { + if (!configuration.GetValue("PayU:UseSandbox", true)) + { + return new PaymentResultDto( + false, + null, + "Sandbox payments are disabled."); + } + + var order = await orderService.GetOrderAsync( + request.OrderId, + cancellationToken); + + await orderService.UpdateOrderStatusAsync( + request.OrderId, + Domain.Enums.OrderStatus.Paid, + cancellationToken); + + var payment = await db.Payments + .FirstOrDefaultAsync(p => p.OrderId == request.OrderId, cancellationToken); + + if (payment is not null) + { + payment.Status = Domain.Enums.PaymentStatus.Completed; + payment.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } + + return new PaymentResultDto( + true, + $"/order/{order.Id}?payment=sandbox", + null); + } +} + +public sealed record SyncPaymentStatusCommand(Guid OrderId) + : IRequest; + +public sealed record SyncPaymentResultDto( + bool Success, + string? PayUStatus, + string? OrderStatus, + string? ErrorMessage); + +public sealed class SyncPaymentStatusCommandHandler( + IOrderService orderService, + IPaymentService paymentService, + IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + SyncPaymentStatusCommand request, + CancellationToken cancellationToken) + { + var order = await orderService.GetOrderAsync( + request.OrderId, + cancellationToken); + + var payment = await db.Payments + .FirstOrDefaultAsync(p => p.OrderId == request.OrderId, cancellationToken); + + if (payment is null || string.IsNullOrWhiteSpace(payment.ExternalId)) + { + return new SyncPaymentResultDto( + false, + null, + order.Status.ToString(), + "Payment not found."); + } + + if (order.Status is Domain.Enums.OrderStatus.Paid + or Domain.Enums.OrderStatus.Processing + or Domain.Enums.OrderStatus.Shipped + or Domain.Enums.OrderStatus.Delivered) + { + return new SyncPaymentResultDto( + true, + payment.Status.ToString(), + order.Status.ToString(), + null); + } + + var status = await paymentService.GetPaymentStatusAsync( + payment.ExternalId, + cancellationToken); + + if (!status.Success) + { + return new SyncPaymentResultDto( + false, + status.Status, + order.Status.ToString(), + status.ErrorMessage); + } + + if (!string.IsNullOrWhiteSpace(status.ExternalId)) + { + payment.ExternalId = status.ExternalId; + } + + if (status.IsCompleted) + { + payment.Status = Domain.Enums.PaymentStatus.Completed; + payment.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + await orderService.UpdateOrderStatusAsync( + request.OrderId, + Domain.Enums.OrderStatus.Paid, + cancellationToken); + + return new SyncPaymentResultDto( + true, + status.Status, + Domain.Enums.OrderStatus.Paid.ToString(), + null); + } + + if (status.IsCanceled) + { + payment.Status = Domain.Enums.PaymentStatus.Failed; + payment.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + + return new SyncPaymentResultDto( + true, + status.Status, + order.Status.ToString(), + null); + } + + await db.SaveChangesAsync(cancellationToken); + return new SyncPaymentResultDto( + true, + status.Status, + order.Status.ToString(), + null); + } +} + +public sealed record ProcessPaymentWebhookCommand( + string Payload, + string Signature) : IRequest; + +public sealed class ProcessPaymentWebhookCommandHandler( + IPaymentService paymentService, + IOrderService orderService, + IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + ProcessPaymentWebhookCommand request, + CancellationToken cancellationToken) + { + var result = await paymentService.HandleWebhookAsync( + request.Payload, + request.Signature, + cancellationToken); + + if (!result.Success || !result.OrderId.HasValue) + { + return; + } + + var payment = await db.Payments + .FirstOrDefaultAsync( + p => p.OrderId == result.OrderId.Value, + cancellationToken); + + if (result.IsCompleted) + { + await orderService.UpdateOrderStatusAsync( + result.OrderId.Value, + Domain.Enums.OrderStatus.Paid, + cancellationToken); + + if (payment is not null) + { + payment.Status = Domain.Enums.PaymentStatus.Completed; + payment.UpdatedAt = DateTime.UtcNow; + if (!string.IsNullOrWhiteSpace(result.ExternalId)) + { + payment.ExternalId = result.ExternalId; + } + + await db.SaveChangesAsync(cancellationToken); + } + + return; + } + + // CANCELED / failed notifications + if (payment is not null && + !string.IsNullOrWhiteSpace(result.ExternalId)) + { + payment.ExternalId = result.ExternalId; + await db.SaveChangesAsync(cancellationToken); + } + } +} diff --git a/src/backend/src/iLED.Application/Reviews/Commands/ReviewCommands.cs b/src/backend/src/iLED.Application/Reviews/Commands/ReviewCommands.cs new file mode 100644 index 0000000..d436893 --- /dev/null +++ b/src/backend/src/iLED.Application/Reviews/Commands/ReviewCommands.cs @@ -0,0 +1,227 @@ +using iLED.Application.Common.Interfaces; +using iLED.Application.Common.Models; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Reviews.Commands; + +public sealed record ReviewDto( + Guid Id, + string AuthorName, + int Rating, + string Content, + DateTime CreatedAt); + +public sealed record ProductReviewsDto( + decimal AverageRating, + int TotalCount, + IReadOnlyDictionary RatingDistribution, + IReadOnlyList Reviews); + +public sealed record GetProductReviewsQuery(string ProductSlug) + : IRequest; + +public sealed class GetProductReviewsQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetProductReviewsQuery request, + CancellationToken cancellationToken) + { + var product = await db.Products + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Slug == request.ProductSlug, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Product not found."); + + var reviews = await db.Reviews + .AsNoTracking() + .Where(r => r.ProductId == product.Id && r.IsApproved) + .OrderByDescending(r => r.CreatedAt) + .Select(r => new ReviewDto( + r.Id, r.AuthorName, r.Rating, r.Content, r.CreatedAt)) + .ToListAsync(cancellationToken); + + var distribution = reviews + .GroupBy(r => r.Rating) + .ToDictionary(g => g.Key, g => g.Count()); + + for (var i = 1; i <= 5; i++) + { + distribution.TryAdd(i, 0); + } + + var avg = reviews.Count > 0 + ? (decimal)reviews.Average(r => r.Rating) + : 0; + + return new ProductReviewsDto(avg, reviews.Count, distribution, reviews); + } +} + +public sealed record CreateReviewCommand( + string ProductSlug, + string AuthorName, + string AuthorEmail, + int Rating, + string Content) : IRequest; + +public sealed class CreateReviewCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + CreateReviewCommand request, + CancellationToken cancellationToken) + { + var product = await db.Products + .FirstOrDefaultAsync(p => p.Slug == request.ProductSlug, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Product not found."); + + var review = new Domain.Catalog.Review + { + ProductId = product.Id, + AuthorName = request.AuthorName, + AuthorEmail = request.AuthorEmail, + Rating = Math.Clamp(request.Rating, 1, 5), + Content = request.Content, + IsApproved = false, + }; + db.Add(review); + await db.SaveChangesAsync(cancellationToken); + return review.Id; + } +} + +public sealed record ApproveReviewCommand(Guid ReviewId, bool IsApproved) : IRequest; + +public sealed class ApproveReviewCommandHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + ApproveReviewCommand request, + CancellationToken cancellationToken) + { + var review = await db.Reviews + .FirstOrDefaultAsync(r => r.Id == request.ReviewId, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Review not found."); + + review.IsApproved = request.IsApproved; + review.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } +} + +public sealed record GetPendingReviewsQuery() + : IRequest>; + +public sealed record PendingReviewDto( + Guid Id, + string ProductName, + string AuthorName, + int Rating, + string Content, + DateTime CreatedAt); + +public sealed class GetPendingReviewsQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetPendingReviewsQuery request, + CancellationToken cancellationToken) + { + return await db.Reviews + .AsNoTracking() + .Where(r => !r.IsApproved && r.UpdatedAt == null) + .OrderByDescending(r => r.CreatedAt) + .Select(r => new PendingReviewDto( + r.Id, + r.Product.Name, + r.AuthorName, + r.Rating, + r.Content, + r.CreatedAt)) + .ToListAsync(cancellationToken); + } +} + +public sealed record GetAdminReviewsQuery( + int Page = 1, + int PageSize = 20, + string? Status = "pending", + string? Search = null) : IRequest>; + +public sealed record AdminReviewDto( + Guid Id, + string ProductName, + string AuthorName, + int Rating, + string Content, + string Status, + DateTime CreatedAt); + +public sealed class GetAdminReviewsQueryHandler(IAppDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + GetAdminReviewsQuery request, + CancellationToken cancellationToken) + { + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + var status = (request.Status ?? "pending").Trim().ToLowerInvariant(); + var query = db.Reviews.AsNoTracking(); + + query = status switch + { + "approved" => query.Where(r => r.IsApproved), + "rejected" => query.Where(r => !r.IsApproved && r.UpdatedAt != null), + "all" => query, + _ => query.Where(r => !r.IsApproved && r.UpdatedAt == null), + }; + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var term = request.Search.Trim().ToLowerInvariant(); + query = query.Where(r => + r.Product.Name.ToLower().Contains(term) || + r.AuthorName.ToLower().Contains(term) || + r.Content.ToLower().Contains(term)); + } + + var total = await query.CountAsync(cancellationToken); + var rows = await query + .OrderByDescending(r => r.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(r => new + { + r.Id, + ProductName = r.Product.Name, + r.AuthorName, + r.Rating, + r.Content, + r.IsApproved, + r.UpdatedAt, + r.CreatedAt, + }) + .ToListAsync(cancellationToken); + + var items = rows.Select(r => new AdminReviewDto( + r.Id, + r.ProductName, + r.AuthorName, + r.Rating, + r.Content, + r.IsApproved + ? "approved" + : r.UpdatedAt is null ? "pending" : "rejected", + r.CreatedAt)).ToList(); + + return new PaginatedList + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = total, + }; + } +} diff --git a/src/backend/src/iLED.Application/Shipping/Commands/ShippingCommands.cs b/src/backend/src/iLED.Application/Shipping/Commands/ShippingCommands.cs new file mode 100644 index 0000000..d7f8fca --- /dev/null +++ b/src/backend/src/iLED.Application/Shipping/Commands/ShippingCommands.cs @@ -0,0 +1,189 @@ +using iLED.Application.Common.Interfaces; +using iLED.Domain.Enums; +using iLED.Domain.Orders; +using iLED.Domain.Shipping; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Application.Shipping.Commands; + +public sealed record SearchParcelPointsQuery( + ShippingCarrier Carrier, + string Query, + double? Latitude = null, + double? Longitude = null) : IRequest>; + +public sealed class SearchParcelPointsQueryHandler(IShippingService shipping) + : IRequestHandler> +{ + public Task> Handle( + SearchParcelPointsQuery request, + CancellationToken cancellationToken) => + shipping.SearchParcelPointsAsync( + new ShippingParcelPointSearch( + request.Carrier, + request.Query, + request.Latitude, + request.Longitude), + cancellationToken); +} + +public sealed record GetShippingWidgetConfigQuery() + : IRequest; + +public sealed class GetShippingWidgetConfigQueryHandler(IShippingService shipping) + : IRequestHandler +{ + public Task Handle( + GetShippingWidgetConfigQuery request, + CancellationToken cancellationToken) => + Task.FromResult(shipping.GetWidgetConfig()); +} + +public sealed record CreateShipmentLabelCommand(Guid OrderId) + : IRequest; + +public sealed record ShipmentDto( + Guid Id, + Guid OrderId, + ShippingCarrier Carrier, + ShippingServiceType ServiceType, + ShipmentStatus Status, + string? TrackingNumber, + string? ExternalShipmentId, + string? LabelRelativePath, + string? ParcelPointName, + string? ParcelPointAddress, + string? ErrorMessage); + +public sealed class CreateShipmentLabelCommandHandler( + IAppDbContext db, + IShippingService shipping) + : IRequestHandler +{ + public async Task Handle( + CreateShipmentLabelCommand request, + CancellationToken cancellationToken) + { + var order = await db.Orders + .Include(o => o.Lines) + .Include(o => o.Shipment) + .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken) + ?? throw new Common.Exceptions.NotFoundException("Order not found."); + + if (order.Status is not (OrderStatus.Paid or OrderStatus.Processing)) + { + throw new Common.Exceptions.ValidationException( + "Label can only be generated for paid or processing orders."); + } + + if (order.Shipment?.Status == ShipmentStatus.LabelGenerated) + { + return MapShipment(order.Shipment); + } + + var codAmount = order.PaymentMethod == PaymentMethod.CashOnDelivery + ? order.Total + : 0m; + + var labelRequest = new ShipmentRequest( + order.Id, + order.OrderNumber, + order.ShippingCarrier, + order.ShippingServiceType, + order.CustomerFirstName, + order.CustomerLastName, + order.CustomerEmail, + order.CustomerPhone, + order.ShippingStreet, + order.ShippingCity, + order.ShippingPostalCode, + order.ShippingCountry, + order.ParcelPointId, + order.ParcelPointName, + order.ParcelPointAddress, + codAmount, + order.Lines.Select(l => new ShipmentLineRequest( + l.ProductName, + l.Quantity, + l.UnitPrice)).ToList()); + + var result = await shipping.CreateShipmentLabelAsync( + labelRequest, + cancellationToken); + + if (!result.Success) + { + throw new Common.Exceptions.ValidationException( + result.ErrorMessage ?? "Label generation failed."); + } + + var shipment = order.Shipment ?? new Shipment { OrderId = order.Id }; + shipment.Carrier = order.ShippingCarrier; + shipment.ServiceType = order.ShippingServiceType; + shipment.Status = ShipmentStatus.LabelGenerated; + shipment.TrackingNumber = result.TrackingNumber; + shipment.ExternalShipmentId = result.ExternalShipmentId; + shipment.LabelRelativePath = result.LabelRelativePath; + shipment.ParcelPointId = order.ParcelPointId; + shipment.ParcelPointName = order.ParcelPointName; + shipment.ParcelPointAddress = order.ParcelPointAddress; + shipment.ErrorMessage = null; + shipment.UpdatedAt = DateTime.UtcNow; + + if (order.Shipment is null) + { + db.Add(shipment); + } + + if (order.Status == OrderStatus.Processing) + { + order.Status = OrderStatus.Shipped; + order.UpdatedAt = DateTime.UtcNow; + } + + await db.SaveChangesAsync(cancellationToken); + return MapShipment(shipment); + } + + private static ShipmentDto MapShipment(Shipment s) => new( + s.Id, + s.OrderId, + s.Carrier, + s.ServiceType, + s.Status, + s.TrackingNumber, + s.ExternalShipmentId, + s.LabelRelativePath, + s.ParcelPointName, + s.ParcelPointAddress, + s.ErrorMessage); +} + +public sealed record GetOrderShipmentQuery(Guid OrderId) : IRequest; + +public sealed class GetOrderShipmentQueryHandler(IAppDbContext db) + : IRequestHandler +{ + public async Task Handle( + GetOrderShipmentQuery request, + CancellationToken cancellationToken) + { + var shipment = await db.Shipments + .AsNoTracking() + .FirstOrDefaultAsync(s => s.OrderId == request.OrderId, cancellationToken); + + return shipment is null ? null : new ShipmentDto( + shipment.Id, + shipment.OrderId, + shipment.Carrier, + shipment.ServiceType, + shipment.Status, + shipment.TrackingNumber, + shipment.ExternalShipmentId, + shipment.LabelRelativePath, + shipment.ParcelPointName, + shipment.ParcelPointAddress, + shipment.ErrorMessage); + } +} diff --git a/src/backend/src/iLED.Application/iLED.Application.csproj b/src/backend/src/iLED.Application/iLED.Application.csproj new file mode 100644 index 0000000..7cc6fd3 --- /dev/null +++ b/src/backend/src/iLED.Application/iLED.Application.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/src/backend/src/iLED.Domain/Cart/Cart.cs b/src/backend/src/iLED.Domain/Cart/Cart.cs new file mode 100644 index 0000000..cb6bfc0 --- /dev/null +++ b/src/backend/src/iLED.Domain/Cart/Cart.cs @@ -0,0 +1,12 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Cart; + +public class Cart : BaseEntity +{ + public string? UserId { get; set; } + + public string? SessionId { get; set; } + + public ICollection Items { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Cart/CartItem.cs b/src/backend/src/iLED.Domain/Cart/CartItem.cs new file mode 100644 index 0000000..98c1141 --- /dev/null +++ b/src/backend/src/iLED.Domain/Cart/CartItem.cs @@ -0,0 +1,16 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Cart; + +public class CartItem : BaseEntity +{ + public Guid CartId { get; set; } + + public Cart Cart { get; set; } = null!; + + public Guid ProductId { get; set; } + + public Guid ProductVariantId { get; set; } + + public int Quantity { get; set; } = 1; +} diff --git a/src/backend/src/iLED.Domain/Cart/FavoriteItem.cs b/src/backend/src/iLED.Domain/Cart/FavoriteItem.cs new file mode 100644 index 0000000..88cf937 --- /dev/null +++ b/src/backend/src/iLED.Domain/Cart/FavoriteItem.cs @@ -0,0 +1,12 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Cart; + +public class FavoriteItem : BaseEntity +{ + public string UserId { get; set; } = string.Empty; + + public Guid ProductId { get; set; } + + public Catalog.Product Product { get; set; } = null!; +} diff --git a/src/backend/src/iLED.Domain/Catalog/Category.cs b/src/backend/src/iLED.Domain/Catalog/Category.cs new file mode 100644 index 0000000..95e58fe --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/Category.cs @@ -0,0 +1,20 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class Category : AuditableEntity +{ + public string Name { get; set; } = string.Empty; + + public string Slug { get; set; } = string.Empty; + + public string? Description { get; set; } + + public Guid? ParentCategoryId { get; set; } + + public Category? ParentCategory { get; set; } + + public ICollection SubCategories { get; set; } = []; + + public ICollection Products { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Catalog/Product.cs b/src/backend/src/iLED.Domain/Catalog/Product.cs new file mode 100644 index 0000000..8581bc7 --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/Product.cs @@ -0,0 +1,56 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class Product : AuditableEntity +{ + public string Name { get; set; } = string.Empty; + + public string Slug { get; set; } = string.Empty; + + public LocalizedStrings Names { get; set; } = new(); + + public string ShortDescription { get; set; } = string.Empty; + + public LocalizedStrings ShortDescriptions { get; set; } = new(); + + public string Description { get; set; } = string.Empty; + + public LocalizedStrings Descriptions { get; set; } = new(); + + public decimal BasePrice { get; set; } + + public decimal LowestPrice30Days { get; set; } + + public string ProductCode { get; set; } = string.Empty; + + public string Model { get; set; } = string.Empty; + + public string? Ean { get; set; } + + public string Manufacturer { get; set; } = "iLED"; + + public Guid? ProductGroupId { get; set; } + + public ProductGroup? ProductGroup { get; set; } + + public bool IsBestseller { get; set; } + + public bool IsNewArrival { get; set; } + + public bool IsActive { get; set; } = true; + + public int DeliveryHours { get; set; } = 24; + + public Guid CategoryId { get; set; } + + public Category Category { get; set; } = null!; + + public ICollection Variants { get; set; } = []; + + public ICollection Images { get; set; } = []; + + public ICollection Attributes { get; set; } = []; + + public ICollection Reviews { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Catalog/ProductAttribute.cs b/src/backend/src/iLED.Domain/Catalog/ProductAttribute.cs new file mode 100644 index 0000000..e861462 --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/ProductAttribute.cs @@ -0,0 +1,20 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class ProductAttribute : BaseEntity +{ + public Guid ProductId { get; set; } + + public Product Product { get; set; } = null!; + + public string Name { get; set; } = string.Empty; + + public LocalizedStrings Names { get; set; } = new(); + + public string Value { get; set; } = string.Empty; + + public LocalizedStrings Values { get; set; } = new(); + + public string? FilterGroup { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Catalog/ProductGroup.cs b/src/backend/src/iLED.Domain/Catalog/ProductGroup.cs new file mode 100644 index 0000000..2c47924 --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/ProductGroup.cs @@ -0,0 +1,16 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class ProductGroup : AuditableEntity +{ + public string Name { get; set; } = string.Empty; + + public LocalizedStrings Names { get; set; } = new(); + + public string Code { get; set; } = string.Empty; + + public int SortOrder { get; set; } + + public ICollection Products { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Catalog/ProductImage.cs b/src/backend/src/iLED.Domain/Catalog/ProductImage.cs new file mode 100644 index 0000000..c6bbffb --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/ProductImage.cs @@ -0,0 +1,18 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class ProductImage : BaseEntity +{ + public Guid ProductId { get; set; } + + public Product Product { get; set; } = null!; + + public string Url { get; set; } = string.Empty; + + public string? AltText { get; set; } + + public int SortOrder { get; set; } + + public bool IsPrimary { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Catalog/ProductVariant.cs b/src/backend/src/iLED.Domain/Catalog/ProductVariant.cs new file mode 100644 index 0000000..a5d8c6e --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/ProductVariant.cs @@ -0,0 +1,24 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class ProductVariant : BaseEntity +{ + public Guid ProductId { get; set; } + + public Product Product { get; set; } = null!; + + public string Sku { get; set; } = string.Empty; + + public string ColorName { get; set; } = string.Empty; + + public string ColorHex { get; set; } = string.Empty; + + public decimal PriceAdjustment { get; set; } + + public bool IsDefault { get; set; } + + public Stock Stock { get; set; } = null!; + + public ICollection WarehouseStocks { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Catalog/Review.cs b/src/backend/src/iLED.Domain/Catalog/Review.cs new file mode 100644 index 0000000..f0c2725 --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/Review.cs @@ -0,0 +1,22 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class Review : BaseEntity +{ + public Guid ProductId { get; set; } + + public Product Product { get; set; } = null!; + + public string? UserId { get; set; } + + public string AuthorName { get; set; } = string.Empty; + + public string AuthorEmail { get; set; } = string.Empty; + + public int Rating { get; set; } + + public string Content { get; set; } = string.Empty; + + public bool IsApproved { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Catalog/Stock.cs b/src/backend/src/iLED.Domain/Catalog/Stock.cs new file mode 100644 index 0000000..6527a9e --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/Stock.cs @@ -0,0 +1,16 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class Stock : BaseEntity +{ + public Guid ProductVariantId { get; set; } + + public ProductVariant ProductVariant { get; set; } = null!; + + public int Quantity { get; set; } + + public int LowStockThreshold { get; set; } = 5; + + public bool IsInStock => Quantity > 0; +} diff --git a/src/backend/src/iLED.Domain/Catalog/Warehouse.cs b/src/backend/src/iLED.Domain/Catalog/Warehouse.cs new file mode 100644 index 0000000..a4da1c7 --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/Warehouse.cs @@ -0,0 +1,14 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class Warehouse : BaseEntity +{ + public string Name { get; set; } = string.Empty; + + public string Code { get; set; } = string.Empty; + + public int SortOrder { get; set; } + + public ICollection Stocks { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Catalog/WarehouseStock.cs b/src/backend/src/iLED.Domain/Catalog/WarehouseStock.cs new file mode 100644 index 0000000..0718307 --- /dev/null +++ b/src/backend/src/iLED.Domain/Catalog/WarehouseStock.cs @@ -0,0 +1,16 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Catalog; + +public class WarehouseStock : BaseEntity +{ + public Guid ProductVariantId { get; set; } + + public ProductVariant ProductVariant { get; set; } = null!; + + public Guid WarehouseId { get; set; } + + public Warehouse Warehouse { get; set; } = null!; + + public int Quantity { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Common/AuditableEntity.cs b/src/backend/src/iLED.Domain/Common/AuditableEntity.cs new file mode 100644 index 0000000..d825877 --- /dev/null +++ b/src/backend/src/iLED.Domain/Common/AuditableEntity.cs @@ -0,0 +1,8 @@ +namespace iLED.Domain.Common; + +public abstract class AuditableEntity : BaseEntity +{ + public string? CreatedBy { get; set; } + + public string? UpdatedBy { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Common/BaseEntity.cs b/src/backend/src/iLED.Domain/Common/BaseEntity.cs new file mode 100644 index 0000000..4147d8e --- /dev/null +++ b/src/backend/src/iLED.Domain/Common/BaseEntity.cs @@ -0,0 +1,10 @@ +namespace iLED.Domain.Common; + +public abstract class BaseEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime? UpdatedAt { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Common/LocalizedStrings.cs b/src/backend/src/iLED.Domain/Common/LocalizedStrings.cs new file mode 100644 index 0000000..1b29f21 --- /dev/null +++ b/src/backend/src/iLED.Domain/Common/LocalizedStrings.cs @@ -0,0 +1,55 @@ +namespace iLED.Domain.Common; + +public class LocalizedStrings +{ + public string Pl { get; set; } = string.Empty; + + public string En { get; set; } = string.Empty; + + public string De { get; set; } = string.Empty; + + public string Cs { get; set; } = string.Empty; + + /// + /// Locale text with fallback: requested → Pl → . + /// + public string Resolve(string? locale, string? legacy = null) + { + var primary = NormalizeLocale(locale) switch + { + "en" => En, + "de" => De, + "cs" => Cs, + _ => Pl, + }; + if (!string.IsNullOrWhiteSpace(primary)) + { + return primary; + } + + if (!string.IsNullOrWhiteSpace(Pl)) + { + return Pl; + } + + return legacy ?? string.Empty; + } + + public string Get(string locale) => Resolve(locale); + + public static string NormalizeLocale(string? locale) + { + if (string.IsNullOrWhiteSpace(locale)) + { + return "pl"; + } + + var code = locale.Trim().ToLowerInvariant(); + return code switch + { + "en" or "de" or "cs" or "pl" => code, + "cz" => "cs", + _ => "pl", + }; + } +} diff --git a/src/backend/src/iLED.Domain/Enums/CampaignStatus.cs b/src/backend/src/iLED.Domain/Enums/CampaignStatus.cs new file mode 100644 index 0000000..2d2bee7 --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/CampaignStatus.cs @@ -0,0 +1,9 @@ +namespace iLED.Domain.Enums; + +public enum CampaignStatus +{ + Draft = 0, + Scheduled = 1, + Sent = 2, + Cancelled = 3, +} diff --git a/src/backend/src/iLED.Domain/Enums/DiscountCodeSource.cs b/src/backend/src/iLED.Domain/Enums/DiscountCodeSource.cs new file mode 100644 index 0000000..c0773b4 --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/DiscountCodeSource.cs @@ -0,0 +1,7 @@ +namespace iLED.Domain.Enums; + +public enum DiscountCodeSource +{ + Manual = 0, + Reward = 1, +} diff --git a/src/backend/src/iLED.Domain/Enums/DiscountType.cs b/src/backend/src/iLED.Domain/Enums/DiscountType.cs new file mode 100644 index 0000000..1abdbd0 --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/DiscountType.cs @@ -0,0 +1,8 @@ +namespace iLED.Domain.Enums; + +public enum DiscountType +{ + Percentage = 0, + FixedAmount = 1, + FreeShipping = 2, +} diff --git a/src/backend/src/iLED.Domain/Enums/OrderStatus.cs b/src/backend/src/iLED.Domain/Enums/OrderStatus.cs new file mode 100644 index 0000000..2dcc97a --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/OrderStatus.cs @@ -0,0 +1,12 @@ +namespace iLED.Domain.Enums; + +public enum OrderStatus +{ + PendingPayment = 0, + Paid = 1, + Processing = 2, + Shipped = 3, + Delivered = 4, + Cancelled = 5, + Refunded = 6, +} diff --git a/src/backend/src/iLED.Domain/Enums/PaymentMethod.cs b/src/backend/src/iLED.Domain/Enums/PaymentMethod.cs new file mode 100644 index 0000000..25276bd --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/PaymentMethod.cs @@ -0,0 +1,8 @@ +namespace iLED.Domain.Enums; + +public enum PaymentMethod +{ + PayU = 0, + BankTransfer = 1, + CashOnDelivery = 2, +} diff --git a/src/backend/src/iLED.Domain/Enums/PaymentStatus.cs b/src/backend/src/iLED.Domain/Enums/PaymentStatus.cs new file mode 100644 index 0000000..81d271f --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/PaymentStatus.cs @@ -0,0 +1,9 @@ +namespace iLED.Domain.Enums; + +public enum PaymentStatus +{ + Pending = 0, + Completed = 1, + Failed = 2, + Refunded = 3, +} diff --git a/src/backend/src/iLED.Domain/Enums/ShipmentStatus.cs b/src/backend/src/iLED.Domain/Enums/ShipmentStatus.cs new file mode 100644 index 0000000..dfd41a1 --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/ShipmentStatus.cs @@ -0,0 +1,10 @@ +namespace iLED.Domain.Enums; + +public enum ShipmentStatus +{ + Pending = 0, + LabelGenerated = 1, + Dispatched = 2, + Delivered = 3, + Failed = 4, +} diff --git a/src/backend/src/iLED.Domain/Enums/ShippingCarrier.cs b/src/backend/src/iLED.Domain/Enums/ShippingCarrier.cs new file mode 100644 index 0000000..c444d59 --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/ShippingCarrier.cs @@ -0,0 +1,8 @@ +namespace iLED.Domain.Enums; + +public enum ShippingCarrier +{ + InPost = 0, + Dhl = 1, + Dpd = 2, +} diff --git a/src/backend/src/iLED.Domain/Enums/ShippingServiceType.cs b/src/backend/src/iLED.Domain/Enums/ShippingServiceType.cs new file mode 100644 index 0000000..0952bb3 --- /dev/null +++ b/src/backend/src/iLED.Domain/Enums/ShippingServiceType.cs @@ -0,0 +1,7 @@ +namespace iLED.Domain.Enums; + +public enum ShippingServiceType +{ + ParcelLocker = 0, + Courier = 1, +} diff --git a/src/backend/src/iLED.Domain/Identity/ApplicationUser.cs b/src/backend/src/iLED.Domain/Identity/ApplicationUser.cs new file mode 100644 index 0000000..d0100fe --- /dev/null +++ b/src/backend/src/iLED.Domain/Identity/ApplicationUser.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Identity; + +namespace iLED.Domain.Identity; + +public class ApplicationUser : IdentityUser +{ + public string FirstName { get; set; } = string.Empty; + + public string LastName { get; set; } = string.Empty; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public ICollection RefreshTokens { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Identity/RefreshToken.cs b/src/backend/src/iLED.Domain/Identity/RefreshToken.cs new file mode 100644 index 0000000..2048ad2 --- /dev/null +++ b/src/backend/src/iLED.Domain/Identity/RefreshToken.cs @@ -0,0 +1,22 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Identity; + +public class RefreshToken : BaseEntity +{ + public string UserId { get; set; } = string.Empty; + + public ApplicationUser User { get; set; } = null!; + + public string Token { get; set; } = string.Empty; + + public DateTime ExpiresAt { get; set; } + + public DateTime? RevokedAt { get; set; } + + public bool IsExpired => DateTime.UtcNow >= ExpiresAt; + + public bool IsRevoked => RevokedAt.HasValue; + + public bool IsActive => !IsRevoked && !IsExpired; +} diff --git a/src/backend/src/iLED.Domain/Marketing/CampaignSend.cs b/src/backend/src/iLED.Domain/Marketing/CampaignSend.cs new file mode 100644 index 0000000..6238211 --- /dev/null +++ b/src/backend/src/iLED.Domain/Marketing/CampaignSend.cs @@ -0,0 +1,18 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Marketing; + +public class CampaignSend : BaseEntity +{ + public Guid CampaignId { get; set; } + + public NewsletterCampaign Campaign { get; set; } = null!; + + public Guid SubscriberId { get; set; } + + public NewsletterSubscriber Subscriber { get; set; } = null!; + + public DateTime? SentAt { get; set; } + + public bool IsDelivered { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Marketing/DiscountCode.cs b/src/backend/src/iLED.Domain/Marketing/DiscountCode.cs new file mode 100644 index 0000000..e2ac75b --- /dev/null +++ b/src/backend/src/iLED.Domain/Marketing/DiscountCode.cs @@ -0,0 +1,32 @@ +using iLED.Domain.Common; +using iLED.Domain.Enums; + +namespace iLED.Domain.Marketing; + +public class DiscountCode : AuditableEntity +{ + public string Code { get; set; } = string.Empty; + + public Guid DiscountRuleId { get; set; } + + public DiscountRule DiscountRule { get; set; } = null!; + + public int? MaxUsages { get; set; } + + public int UsageCount { get; set; } + + public bool IsActive { get; set; } = true; + + public DiscountCodeSource Source { get; set; } = DiscountCodeSource.Manual; + + public string? IssuedToEmail { get; set; } + + public Guid? IssuedForOrderId { get; set; } + + public DateTime? ValidUntil { get; set; } + + public bool CanBeUsed => + IsActive && + (!MaxUsages.HasValue || UsageCount < MaxUsages.Value) && + (!ValidUntil.HasValue || ValidUntil.Value >= DateTime.UtcNow); +} diff --git a/src/backend/src/iLED.Domain/Marketing/DiscountRedemption.cs b/src/backend/src/iLED.Domain/Marketing/DiscountRedemption.cs new file mode 100644 index 0000000..6f6f184 --- /dev/null +++ b/src/backend/src/iLED.Domain/Marketing/DiscountRedemption.cs @@ -0,0 +1,22 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Marketing; + +public class DiscountRedemption : BaseEntity +{ + public Guid OrderId { get; set; } + + public Guid DiscountCodeId { get; set; } + + public DiscountCode DiscountCode { get; set; } = null!; + + public Guid DiscountRuleId { get; set; } + + public DiscountRule DiscountRule { get; set; } = null!; + + public string CustomerEmail { get; set; } = string.Empty; + + public string? UserId { get; set; } + + public decimal DiscountAmount { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Marketing/DiscountRule.cs b/src/backend/src/iLED.Domain/Marketing/DiscountRule.cs new file mode 100644 index 0000000..2d53f23 --- /dev/null +++ b/src/backend/src/iLED.Domain/Marketing/DiscountRule.cs @@ -0,0 +1,49 @@ +using iLED.Domain.Common; +using iLED.Domain.Enums; + +namespace iLED.Domain.Marketing; + +/// +/// Promotion campaign (admin "Promotions"). Table name kept as DiscountRules. +/// +public class DiscountRule : AuditableEntity +{ + public string Name { get; set; } = string.Empty; + + public DiscountType Type { get; set; } + + public decimal Value { get; set; } + + public decimal? MinimumOrderAmount { get; set; } + + public DateTime? ValidFrom { get; set; } + + public DateTime? ValidTo { get; set; } + + public bool IsActive { get; set; } = true; + + /// Global redemption cap across all codes (null = unlimited). + public int? MaxTotalRedemptions { get; set; } + + /// Per customer email/user (null = unlimited, 1 = one per customer). + public int? MaxRedemptionsPerCustomer { get; set; } + + public bool ShowInBanner { get; set; } + + public LocalizedStrings BannerTexts { get; set; } = new(); + + public string? BannerHighlightedCode { get; set; } + + public bool IssueRewardOnPaidOrder { get; set; } + + /// Single paid order total threshold to issue a reward code. + public decimal? RewardMinPaidOrderTotal { get; set; } + + public string? RewardCodePrefix { get; set; } + + public int? RewardValidDays { get; set; } + + public ICollection Codes { get; set; } = []; + + public ICollection Redemptions { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Marketing/NewsletterCampaign.cs b/src/backend/src/iLED.Domain/Marketing/NewsletterCampaign.cs new file mode 100644 index 0000000..e4d3d5d --- /dev/null +++ b/src/backend/src/iLED.Domain/Marketing/NewsletterCampaign.cs @@ -0,0 +1,19 @@ +using iLED.Domain.Common; +using iLED.Domain.Enums; + +namespace iLED.Domain.Marketing; + +public class NewsletterCampaign : AuditableEntity +{ + public string Subject { get; set; } = string.Empty; + + public string BodyHtml { get; set; } = string.Empty; + + public CampaignStatus Status { get; set; } = CampaignStatus.Draft; + + public DateTime? ScheduledAt { get; set; } + + public DateTime? SentAt { get; set; } + + public ICollection Sends { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Marketing/NewsletterSubscriber.cs b/src/backend/src/iLED.Domain/Marketing/NewsletterSubscriber.cs new file mode 100644 index 0000000..e575500 --- /dev/null +++ b/src/backend/src/iLED.Domain/Marketing/NewsletterSubscriber.cs @@ -0,0 +1,14 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Marketing; + +public class NewsletterSubscriber : BaseEntity +{ + public string Email { get; set; } = string.Empty; + + public bool IsActive { get; set; } = true; + + public DateTime? UnsubscribedAt { get; set; } + + public ICollection CampaignSends { get; set; } = []; +} diff --git a/src/backend/src/iLED.Domain/Marketing/PromoBanner.cs b/src/backend/src/iLED.Domain/Marketing/PromoBanner.cs new file mode 100644 index 0000000..def5a30 --- /dev/null +++ b/src/backend/src/iLED.Domain/Marketing/PromoBanner.cs @@ -0,0 +1,12 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Marketing; + +public class PromoBanner : AuditableEntity +{ + public LocalizedStrings Texts { get; set; } = new(); + + public string? HighlightedCode { get; set; } + + public bool IsActive { get; set; } = true; +} diff --git a/src/backend/src/iLED.Domain/Orders/Order.cs b/src/backend/src/iLED.Domain/Orders/Order.cs new file mode 100644 index 0000000..ccbaf08 --- /dev/null +++ b/src/backend/src/iLED.Domain/Orders/Order.cs @@ -0,0 +1,76 @@ +using iLED.Domain.Common; + +namespace iLED.Domain.Orders; + +public class Order : AuditableEntity +{ + public string OrderNumber { get; set; } = string.Empty; + + public string? UserId { get; set; } + + public string CustomerEmail { get; set; } = string.Empty; + + /// Shop UI locale at order time (pl/en/de/cs). + public string Locale { get; set; } = "pl"; + + public string CustomerPhone { get; set; } = string.Empty; + + public string CustomerFirstName { get; set; } = string.Empty; + + public string CustomerLastName { get; set; } = string.Empty; + + public string ShippingStreet { get; set; } = string.Empty; + + public string ShippingCity { get; set; } = string.Empty; + + public string ShippingPostalCode { get; set; } = string.Empty; + + public string ShippingCountry { get; set; } = "PL"; + + public string? BillingStreet { get; set; } + + public string? BillingCity { get; set; } + + public string? BillingPostalCode { get; set; } + + public string? BillingCountry { get; set; } + + public Enums.ShippingCarrier ShippingCarrier { get; set; } = + Enums.ShippingCarrier.InPost; + + public Enums.ShippingServiceType ShippingServiceType { get; set; } = + Enums.ShippingServiceType.ParcelLocker; + + public string? ParcelPointId { get; set; } + + public string? ParcelPointName { get; set; } + + public string? ParcelPointAddress { get; set; } + + public Enums.PaymentMethod PaymentMethod { get; set; } = + Enums.PaymentMethod.PayU; + + public string? CustomerNotes { get; set; } + + public Enums.OrderStatus Status { get; set; } = Enums.OrderStatus.PendingPayment; + + public decimal Subtotal { get; set; } + + public decimal DeliveryCost { get; set; } + + public decimal DiscountAmount { get; set; } + + public decimal VatAmount { get; set; } + + public decimal Total { get; set; } + + public Guid? DiscountCodeId { get; set; } + + public Marketing.DiscountCode? DiscountCode { get; set; } + + public ICollection Lines { get; set; } = []; + + public Payments.Payment? Payment { get; set; } + + public Shipping.Shipment? Shipment { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Orders/OrderLine.cs b/src/backend/src/iLED.Domain/Orders/OrderLine.cs new file mode 100644 index 0000000..98df6d3 --- /dev/null +++ b/src/backend/src/iLED.Domain/Orders/OrderLine.cs @@ -0,0 +1,35 @@ +using iLED.Domain.Catalog; +using iLED.Domain.Common; + +namespace iLED.Domain.Orders; + +public class OrderLine : BaseEntity +{ + public Guid OrderId { get; set; } + + public Order Order { get; set; } = null!; + + public Guid ProductId { get; set; } + + public Guid ProductVariantId { get; set; } + + public string ProductName { get; set; } = string.Empty; + + public string VariantColor { get; set; } = string.Empty; + + public string Sku { get; set; } = string.Empty; + + public string? ImageUrl { get; set; } + + public decimal UnitPrice { get; set; } + + public int Quantity { get; set; } + + public Guid? PickedFromWarehouseId { get; set; } + + public Warehouse? PickedFromWarehouse { get; set; } + + public DateTime? CollectedAt { get; set; } + + public decimal LineTotal => UnitPrice * Quantity; +} diff --git a/src/backend/src/iLED.Domain/Payments/Payment.cs b/src/backend/src/iLED.Domain/Payments/Payment.cs new file mode 100644 index 0000000..6da156a --- /dev/null +++ b/src/backend/src/iLED.Domain/Payments/Payment.cs @@ -0,0 +1,25 @@ +using iLED.Domain.Common; +using iLED.Domain.Enums; + +namespace iLED.Domain.Payments; + +public class Payment : BaseEntity +{ + public Guid OrderId { get; set; } + + public Orders.Order Order { get; set; } = null!; + + public string ExternalId { get; set; } = string.Empty; + + public string Provider { get; set; } = "PayU"; + + public PaymentStatus Status { get; set; } = PaymentStatus.Pending; + + public decimal Amount { get; set; } + + public string Currency { get; set; } = "PLN"; + + public string? RedirectUrl { get; set; } + + public string? RawResponse { get; set; } +} diff --git a/src/backend/src/iLED.Domain/Shipping/Shipment.cs b/src/backend/src/iLED.Domain/Shipping/Shipment.cs new file mode 100644 index 0000000..065c3e4 --- /dev/null +++ b/src/backend/src/iLED.Domain/Shipping/Shipment.cs @@ -0,0 +1,31 @@ +using iLED.Domain.Common; +using iLED.Domain.Enums; + +namespace iLED.Domain.Shipping; + +public class Shipment : AuditableEntity +{ + public Guid OrderId { get; set; } + + public Orders.Order Order { get; set; } = null!; + + public ShippingCarrier Carrier { get; set; } + + public ShippingServiceType ServiceType { get; set; } + + public ShipmentStatus Status { get; set; } = ShipmentStatus.Pending; + + public string? TrackingNumber { get; set; } + + public string? ExternalShipmentId { get; set; } + + public string? LabelRelativePath { get; set; } + + public string? ParcelPointId { get; set; } + + public string? ParcelPointName { get; set; } + + public string? ParcelPointAddress { get; set; } + + public string? ErrorMessage { get; set; } +} diff --git a/src/backend/src/iLED.Domain/iLED.Domain.csproj b/src/backend/src/iLED.Domain/iLED.Domain.csproj new file mode 100644 index 0000000..5ec1567 --- /dev/null +++ b/src/backend/src/iLED.Domain/iLED.Domain.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/src/backend/src/iLED.Infrastructure/DependencyInjection.cs b/src/backend/src/iLED.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..96a4de8 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/DependencyInjection.cs @@ -0,0 +1,79 @@ +using iLED.Application.Cart.Commands; +using iLED.Application.Common.Interfaces; +using iLED.Application.Identity.Commands; +using iLED.Application.Orders.Commands; +using iLED.Domain.Identity; +using iLED.Infrastructure.Identity; +using iLED.Infrastructure.Persistence; +using iLED.Infrastructure.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace iLED.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + IConfiguration configuration) + { + services.AddDbContext(options => + options.UseNpgsql( + configuration.GetConnectionString("DefaultConnection"))); + + services.AddScoped(sp => + sp.GetRequiredService()); + + services.AddIdentity(options => + { + options.Password.RequireDigit = true; + options.Password.RequiredLength = 8; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + options.User.RequireUniqueEmail = true; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + services.AddHttpContextAccessor(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddHttpClient(nameof(PayUPaymentService)) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + // PayU returns 302 + Location / redirectUri — do not follow. + AllowAutoRedirect = false, + }); + services.AddHttpClient(nameof(ShippingCarrierClients)); + services.AddHttpClient(nameof(DpdPudoClient), client => + { + client.Timeout = TimeSpan.FromSeconds(60); + }); + services.AddScoped(); + services.AddHostedService(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.Configure( + configuration.GetSection(ShippingLabelStorageOptions.SectionName)); + services.AddSingleton(); + services.AddScoped(); + services.Configure( + configuration.GetSection(ProductImageStorageOptions.SectionName)); + services.AddScoped(); + + services.AddStackExchangeRedisCache(options => + { + options.Configuration = configuration.GetConnectionString("Redis"); + }); + + return services; + } +} diff --git a/src/backend/src/iLED.Infrastructure/Identity/IdentityServices.cs b/src/backend/src/iLED.Infrastructure/Identity/IdentityServices.cs new file mode 100644 index 0000000..a717db7 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Identity/IdentityServices.cs @@ -0,0 +1,159 @@ +using iLED.Application.Common; +using iLED.Application.Common.Interfaces; +using iLED.Application.Identity.Commands; +using iLED.Domain.Identity; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Persistence = iLED.Infrastructure.Persistence; + +namespace iLED.Infrastructure.Identity; + +public sealed class CurrentUserService(IHttpContextAccessor httpContextAccessor) + : ICurrentUserService +{ + public string? UserId => + httpContextAccessor.HttpContext?.User + .FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + public string? SessionId => + httpContextAccessor.HttpContext?.Request.Headers["X-Session-Id"] + .FirstOrDefault() ?? + httpContextAccessor.HttpContext?.Request.Cookies["iled_session"]; + + public bool IsAuthenticated => + httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false; + + public bool IsAdmin => + httpContextAccessor.HttpContext?.User + .IsInRole("Admin") ?? false; + + public string Locale + { + get + { + var ctx = httpContextAccessor.HttpContext; + var fromQuery = ctx?.Request.Query["locale"].FirstOrDefault(); + var fromHeader = ctx?.Request.Headers["X-Locale"].FirstOrDefault(); + return LocaleHelper.Normalize(fromQuery ?? fromHeader); + } + } +} + +public sealed class IdentityService( + UserManager userManager, + Persistence.ApplicationDbContext db, + IJwtTokenService jwtTokenService) : IIdentityService +{ + private const int RefreshTokenDays = 7; + + public async Task RegisterAsync( + RegisterCommand command, + CancellationToken cancellationToken) + { + var user = new ApplicationUser + { + UserName = command.Email, + Email = command.Email, + FirstName = command.FirstName, + LastName = command.LastName, + }; + + var result = await userManager.CreateAsync(user, command.Password); + if (!result.Succeeded) + { + throw new Application.Common.Exceptions.ValidationException( + string.Join("; ", result.Errors.Select(e => e.Description))); + } + + await userManager.AddToRoleAsync(user, "Customer"); + return await CreateAuthResponseAsync(user, cancellationToken); + } + + public async Task LoginAsync( + LoginCommand command, + CancellationToken cancellationToken) + { + var user = await userManager.FindByEmailAsync(command.Email); + if (user is null || + !await userManager.CheckPasswordAsync(user, command.Password)) + { + throw new Application.Common.Exceptions.ValidationException( + "Nieprawidłowy email lub hasło."); + } + + return await CreateAuthResponseAsync(user, cancellationToken); + } + + public async Task RefreshTokenAsync( + RefreshTokenCommand command, + CancellationToken cancellationToken) + { + var stored = await db.RefreshTokens + .Include(t => t.User) + .FirstOrDefaultAsync(t => t.Token == command.RefreshToken, cancellationToken); + + if (stored is null || !stored.IsActive) + { + throw new Application.Common.Exceptions.ValidationException( + "Nieprawidłowy token odświeżania."); + } + + stored.RevokedAt = DateTime.UtcNow; + return await CreateAuthResponseAsync(stored.User, cancellationToken); + } + + public async Task GetProfileAsync( + string? userId, + CancellationToken cancellationToken) + { + if (userId is null) + { + return null; + } + + var user = await userManager.FindByIdAsync(userId); + if (user is null) + { + return null; + } + + var roles = await userManager.GetRolesAsync(user); + return new UserProfileDto( + user.Id, + user.Email ?? string.Empty, + user.FirstName, + user.LastName, + roles.ToList()); + } + + private async Task CreateAuthResponseAsync( + ApplicationUser user, + CancellationToken cancellationToken) + { + var roles = await userManager.GetRolesAsync(user); + var accessToken = jwtTokenService.GenerateAccessToken( + user.Id, + user.Email ?? string.Empty, + roles); + var refreshToken = jwtTokenService.GenerateRefreshToken(); + + db.RefreshTokens.Add(new RefreshToken + { + UserId = user.Id, + Token = refreshToken, + ExpiresAt = DateTime.UtcNow.AddDays(RefreshTokenDays), + }); + await db.SaveChangesAsync(cancellationToken); + + return new AuthResponse( + accessToken, + refreshToken, + DateTime.UtcNow.AddMinutes(60), + user.Id, + user.Email ?? string.Empty, + user.FirstName, + user.LastName, + roles.ToList()); + } +} diff --git a/src/backend/src/iLED.Infrastructure/Identity/JwtTokenService.cs b/src/backend/src/iLED.Infrastructure/Identity/JwtTokenService.cs new file mode 100644 index 0000000..4f46576 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Identity/JwtTokenService.cs @@ -0,0 +1,96 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using iLED.Application.Common.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace iLED.Infrastructure.Identity; + +public sealed class JwtTokenService(IConfiguration configuration) : IJwtTokenService +{ + public string GenerateAccessToken( + string userId, + string email, + IList roles) + { + var claims = new List + { + new(ClaimTypes.NameIdentifier, userId), + new(ClaimTypes.Email, email), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + foreach (var role in roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + var key = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(GetSecret())); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var expiry = DateTime.UtcNow.AddMinutes(GetExpiryMinutes()); + + var token = new JwtSecurityToken( + issuer: configuration["Jwt:Issuer"], + audience: configuration["Jwt:Audience"], + claims: claims, + expires: expiry, + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public string GenerateRefreshToken() + { + var bytes = new byte[64]; + RandomNumberGenerator.Fill(bytes); + return Convert.ToBase64String(bytes); + } + + public (string userId, string email, IList roles)? ValidateAccessToken( + string token) + { + var handler = new JwtSecurityTokenHandler(); + var parameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = configuration["Jwt:Issuer"], + ValidAudience = configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(GetSecret())), + }; + + try + { + var principal = handler.ValidateToken(token, parameters, out _); + var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier); + var email = principal.FindFirstValue(ClaimTypes.Email); + var roles = principal.FindAll(ClaimTypes.Role) + .Select(c => c.Value) + .ToList(); + + if (userId is null || email is null) + { + return null; + } + + return (userId, email, roles); + } + catch + { + return null; + } + } + + private string GetSecret() => + configuration["Jwt:Secret"] ?? + "iLED-Super-Secret-Key-Change-In-Production-2026!"; + + private int GetExpiryMinutes() => + int.TryParse(configuration["Jwt:ExpiryMinutes"], out var m) ? m : 60; +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260701190650_InitialCreate.Designer.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260701190650_InitialCreate.Designer.cs new file mode 100644 index 0000000..0e6405d --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260701190650_InitialCreate.Designer.cs @@ -0,0 +1,1329 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260701190650_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260701190650_InitialCreate.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260701190650_InitialCreate.cs new file mode 100644 index 0000000..f91e19a --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260701190650_InitialCreate.cs @@ -0,0 +1,886 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Carts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "text", nullable: true), + SessionId = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Carts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Slug = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: true), + ParentCategoryId = 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + table.ForeignKey( + name: "FK_Categories_Categories_ParentCategoryId", + column: x => x.ParentCategoryId, + principalTable: "Categories", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "DiscountRules", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Value = table.Column(type: "numeric", nullable: false), + MinimumOrderAmount = table.Column(type: "numeric", nullable: true), + ValidFrom = table.Column(type: "timestamp with time zone", nullable: true), + ValidTo = 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DiscountRules", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "NewsletterCampaigns", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Subject = table.Column(type: "text", nullable: false), + BodyHtml = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false), + ScheduledAt = table.Column(type: "timestamp with time zone", nullable: true), + SentAt = 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_NewsletterCampaigns", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "NewsletterSubscribers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "text", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + UnsubscribedAt = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_NewsletterSubscribers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PromoBanners", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Text = table.Column(type: "text", nullable: false), + HighlightedCode = table.Column(type: "text", 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PromoBanners", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RefreshTokens", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "text", nullable: false), + Token = table.Column(type: "text", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + RevokedAt = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshTokens", x => x.Id); + table.ForeignKey( + name: "FK_RefreshTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CartItems", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CartId = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + ProductVariantId = table.Column(type: "uuid", nullable: false), + Quantity = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_CartItems", x => x.Id); + table.ForeignKey( + name: "FK_CartItems_Carts_CartId", + column: x => x.CartId, + principalTable: "Carts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Slug = table.Column(type: "text", nullable: false), + ShortDescription = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + BasePrice = table.Column(type: "numeric", nullable: false), + LowestPrice30Days = table.Column(type: "numeric", nullable: false), + ProductCode = table.Column(type: "text", nullable: false), + Manufacturer = table.Column(type: "text", nullable: false), + IsBestseller = table.Column(type: "boolean", nullable: false), + IsNewArrival = table.Column(type: "boolean", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + DeliveryHours = table.Column(type: "integer", nullable: false), + CategoryId = 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + table.ForeignKey( + name: "FK_Products_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DiscountCodes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "text", nullable: false), + DiscountRuleId = table.Column(type: "uuid", nullable: false), + MaxUsages = table.Column(type: "integer", nullable: true), + UsageCount = 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DiscountCodes", x => x.Id); + table.ForeignKey( + name: "FK_DiscountCodes_DiscountRules_DiscountRuleId", + column: x => x.DiscountRuleId, + principalTable: "DiscountRules", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CampaignSends", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CampaignId = table.Column(type: "uuid", nullable: false), + SubscriberId = table.Column(type: "uuid", nullable: false), + SentAt = table.Column(type: "timestamp with time zone", nullable: true), + IsDelivered = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_CampaignSends", x => x.Id); + table.ForeignKey( + name: "FK_CampaignSends_NewsletterCampaigns_CampaignId", + column: x => x.CampaignId, + principalTable: "NewsletterCampaigns", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CampaignSends_NewsletterSubscribers_SubscriberId", + column: x => x.SubscriberId, + principalTable: "NewsletterSubscribers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FavoriteItems", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "text", nullable: false), + ProductId = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_FavoriteItems", x => x.Id); + table.ForeignKey( + name: "FK_FavoriteItems_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProductAttributes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: false), + FilterGroup = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductAttributes", x => x.Id); + table.ForeignKey( + name: "FK_ProductAttributes_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProductImages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + Url = table.Column(type: "text", nullable: false), + AltText = table.Column(type: "text", nullable: true), + SortOrder = table.Column(type: "integer", nullable: false), + IsPrimary = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductImages", x => x.Id); + table.ForeignKey( + name: "FK_ProductImages_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProductVariants", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + Sku = table.Column(type: "text", nullable: false), + ColorName = table.Column(type: "text", nullable: false), + ColorHex = table.Column(type: "text", nullable: false), + PriceAdjustment = table.Column(type: "numeric", nullable: false), + IsDefault = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductVariants", x => x.Id); + table.ForeignKey( + name: "FK_ProductVariants_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Reviews", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "text", nullable: true), + AuthorName = table.Column(type: "text", nullable: false), + AuthorEmail = table.Column(type: "text", nullable: false), + Rating = table.Column(type: "integer", nullable: false), + Content = table.Column(type: "text", nullable: false), + IsApproved = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Reviews", x => x.Id); + table.ForeignKey( + name: "FK_Reviews_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderNumber = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "text", nullable: true), + CustomerEmail = table.Column(type: "text", nullable: false), + CustomerFirstName = table.Column(type: "text", nullable: false), + CustomerLastName = table.Column(type: "text", nullable: false), + ShippingStreet = table.Column(type: "text", nullable: false), + ShippingCity = table.Column(type: "text", nullable: false), + ShippingPostalCode = table.Column(type: "text", nullable: false), + ShippingCountry = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Subtotal = table.Column(type: "numeric", nullable: false), + DeliveryCost = table.Column(type: "numeric", nullable: false), + DiscountAmount = table.Column(type: "numeric", nullable: false), + VatAmount = table.Column(type: "numeric", nullable: false), + Total = table.Column(type: "numeric", nullable: false), + DiscountCodeId = 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_DiscountCodes_DiscountCodeId", + column: x => x.DiscountCodeId, + principalTable: "DiscountCodes", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Stocks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductVariantId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + LowStockThreshold = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Stocks", x => x.Id); + table.ForeignKey( + name: "FK_Stocks_ProductVariants_ProductVariantId", + column: x => x.ProductVariantId, + principalTable: "ProductVariants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrderLines", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + ProductVariantId = table.Column(type: "uuid", nullable: false), + ProductName = table.Column(type: "text", nullable: false), + VariantColor = table.Column(type: "text", nullable: false), + Sku = table.Column(type: "text", nullable: false), + ImageUrl = table.Column(type: "text", nullable: true), + UnitPrice = table.Column(type: "numeric", nullable: false), + Quantity = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderLines", x => x.Id); + table.ForeignKey( + name: "FK_OrderLines_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Payments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + ExternalId = table.Column(type: "text", nullable: false), + Provider = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Amount = table.Column(type: "numeric", nullable: false), + Currency = table.Column(type: "text", nullable: false), + RedirectUrl = table.Column(type: "text", nullable: true), + RawResponse = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Payments", x => x.Id); + table.ForeignKey( + name: "FK_Payments_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CampaignSends_CampaignId", + table: "CampaignSends", + column: "CampaignId"); + + migrationBuilder.CreateIndex( + name: "IX_CampaignSends_SubscriberId", + table: "CampaignSends", + column: "SubscriberId"); + + migrationBuilder.CreateIndex( + name: "IX_CartItems_CartId", + table: "CartItems", + column: "CartId"); + + migrationBuilder.CreateIndex( + name: "IX_Categories_ParentCategoryId", + table: "Categories", + column: "ParentCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Categories_Slug", + table: "Categories", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DiscountCodes_Code", + table: "DiscountCodes", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DiscountCodes_DiscountRuleId", + table: "DiscountCodes", + column: "DiscountRuleId"); + + migrationBuilder.CreateIndex( + name: "IX_FavoriteItems_ProductId", + table: "FavoriteItems", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_NewsletterSubscribers_Email", + table: "NewsletterSubscribers", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrderLines_OrderId", + table: "OrderLines", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_DiscountCodeId", + table: "Orders", + column: "DiscountCodeId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_OrderNumber", + table: "Orders", + column: "OrderNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Payments_OrderId", + table: "Payments", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProductAttributes_ProductId", + table: "ProductAttributes", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductImages_ProductId", + table: "ProductImages", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Products_CategoryId", + table: "Products", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Products_Slug", + table: "Products", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariants_ProductId", + table: "ProductVariants", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductVariants_Sku", + table: "ProductVariants", + column: "Sku", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RefreshTokens_UserId", + table: "RefreshTokens", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Reviews_ProductId", + table: "Reviews", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Stocks_ProductVariantId", + table: "Stocks", + column: "ProductVariantId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "CampaignSends"); + + migrationBuilder.DropTable( + name: "CartItems"); + + migrationBuilder.DropTable( + name: "FavoriteItems"); + + migrationBuilder.DropTable( + name: "OrderLines"); + + migrationBuilder.DropTable( + name: "Payments"); + + migrationBuilder.DropTable( + name: "ProductAttributes"); + + migrationBuilder.DropTable( + name: "ProductImages"); + + migrationBuilder.DropTable( + name: "PromoBanners"); + + migrationBuilder.DropTable( + name: "RefreshTokens"); + + migrationBuilder.DropTable( + name: "Reviews"); + + migrationBuilder.DropTable( + name: "Stocks"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "NewsletterCampaigns"); + + migrationBuilder.DropTable( + name: "NewsletterSubscribers"); + + migrationBuilder.DropTable( + name: "Carts"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "ProductVariants"); + + migrationBuilder.DropTable( + name: "DiscountCodes"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "DiscountRules"); + + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702102032_AddWarehouses.Designer.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702102032_AddWarehouses.Designer.cs new file mode 100644 index 0000000..b8a0456 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702102032_AddWarehouses.Designer.cs @@ -0,0 +1,1417 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260702102032_AddWarehouses")] + partial class AddWarehouses + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702102032_AddWarehouses.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702102032_AddWarehouses.cs new file mode 100644 index 0000000..22bc971 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702102032_AddWarehouses.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + /// + public partial class AddWarehouses : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Warehouses", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Code = table.Column(type: "text", nullable: false), + SortOrder = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Warehouses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "WarehouseStocks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductVariantId = table.Column(type: "uuid", nullable: false), + WarehouseId = table.Column(type: "uuid", nullable: false), + Quantity = 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) + }, + constraints: table => + { + table.PrimaryKey("PK_WarehouseStocks", x => x.Id); + table.ForeignKey( + name: "FK_WarehouseStocks_ProductVariants_ProductVariantId", + column: x => x.ProductVariantId, + principalTable: "ProductVariants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_WarehouseStocks_Warehouses_WarehouseId", + column: x => x.WarehouseId, + principalTable: "Warehouses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Warehouses_Code", + table: "Warehouses", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseStocks_ProductVariantId_WarehouseId", + table: "WarehouseStocks", + columns: new[] { "ProductVariantId", "WarehouseId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WarehouseStocks_WarehouseId", + table: "WarehouseStocks", + column: "WarehouseId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "WarehouseStocks"); + + migrationBuilder.DropTable( + name: "Warehouses"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702103839_ProductI18nAndGroups.Designer.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702103839_ProductI18nAndGroups.Designer.cs new file mode 100644 index 0000000..bc64bc1 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702103839_ProductI18nAndGroups.Designer.cs @@ -0,0 +1,1571 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260702103839_ProductI18nAndGroups")] + partial class ProductI18nAndGroups + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702103839_ProductI18nAndGroups.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702103839_ProductI18nAndGroups.cs new file mode 100644 index 0000000..b58ad22 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702103839_ProductI18nAndGroups.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + /// + public partial class ProductI18nAndGroups : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Descriptions", + table: "Products", + type: "jsonb", + nullable: false, + defaultValue: "{}"); + + migrationBuilder.AddColumn( + name: "Ean", + table: "Products", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Model", + table: "Products", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Names", + table: "Products", + type: "jsonb", + nullable: false, + defaultValue: "{}"); + + migrationBuilder.AddColumn( + name: "ProductGroupId", + table: "Products", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "ShortDescriptions", + table: "Products", + type: "jsonb", + nullable: false, + defaultValue: "{}"); + + migrationBuilder.CreateTable( + name: "ProductGroups", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Code = table.Column(type: "text", nullable: false), + SortOrder = 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductGroups", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Products_ProductGroupId", + table: "Products", + column: "ProductGroupId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductGroups_Code", + table: "ProductGroups", + column: "Code", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Products_ProductGroups_ProductGroupId", + table: "Products", + column: "ProductGroupId", + principalTable: "ProductGroups", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Products_ProductGroups_ProductGroupId", + table: "Products"); + + migrationBuilder.DropTable( + name: "ProductGroups"); + + migrationBuilder.DropIndex( + name: "IX_Products_ProductGroupId", + table: "Products"); + + migrationBuilder.DropColumn( + name: "Descriptions", + table: "Products"); + + migrationBuilder.DropColumn( + name: "Ean", + table: "Products"); + + migrationBuilder.DropColumn( + name: "Model", + table: "Products"); + + migrationBuilder.DropColumn( + name: "Names", + table: "Products"); + + migrationBuilder.DropColumn( + name: "ProductGroupId", + table: "Products"); + + migrationBuilder.DropColumn( + name: "ShortDescriptions", + table: "Products"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702104605_AttributeI18n.Designer.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702104605_AttributeI18n.Designer.cs new file mode 100644 index 0000000..3a12b27 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702104605_AttributeI18n.Designer.cs @@ -0,0 +1,1633 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260702104605_AttributeI18n")] + partial class AttributeI18n + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702104605_AttributeI18n.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702104605_AttributeI18n.cs new file mode 100644 index 0000000..869f59f --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702104605_AttributeI18n.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + /// + public partial class AttributeI18n : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Names", + table: "ProductAttributes", + type: "jsonb", + nullable: false, + defaultValue: "{}"); + + migrationBuilder.AddColumn( + name: "Values", + table: "ProductAttributes", + type: "jsonb", + nullable: false, + defaultValue: "{}"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Names", + table: "ProductAttributes"); + + migrationBuilder.DropColumn( + name: "Values", + table: "ProductAttributes"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702105752_GroupI18n.Designer.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702105752_GroupI18n.Designer.cs new file mode 100644 index 0000000..6d4cc8a --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702105752_GroupI18n.Designer.cs @@ -0,0 +1,1667 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260702105752_GroupI18n")] + partial class GroupI18n + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductGroupId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductGroupId"); + + b1.ToTable("ProductGroups"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductGroupId"); + }); + + b.Navigation("Names") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702105752_GroupI18n.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702105752_GroupI18n.cs new file mode 100644 index 0000000..7abf4d3 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702105752_GroupI18n.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + /// + public partial class GroupI18n : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Names", + table: "ProductGroups", + type: "jsonb", + nullable: false, + defaultValue: "{}"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Names", + table: "ProductGroups"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702114414_OrderLineCollection.Designer.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702114414_OrderLineCollection.Designer.cs new file mode 100644 index 0000000..45ddd5b --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702114414_OrderLineCollection.Designer.cs @@ -0,0 +1,1682 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260702114414_OrderLineCollection")] + partial class OrderLineCollection + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CollectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedFromWarehouseId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PickedFromWarehouseId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductGroupId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductGroupId"); + + b1.ToTable("ProductGroups"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductGroupId"); + }); + + b.Navigation("Names") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "PickedFromWarehouse") + .WithMany() + .HasForeignKey("PickedFromWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Order"); + + b.Navigation("PickedFromWarehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702114414_OrderLineCollection.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702114414_OrderLineCollection.cs new file mode 100644 index 0000000..ddcd4f7 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702114414_OrderLineCollection.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + /// + public partial class OrderLineCollection : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CollectedAt", + table: "OrderLines", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "PickedFromWarehouseId", + table: "OrderLines", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_OrderLines_PickedFromWarehouseId", + table: "OrderLines", + column: "PickedFromWarehouseId"); + + migrationBuilder.AddForeignKey( + name: "FK_OrderLines_Warehouses_PickedFromWarehouseId", + table: "OrderLines", + column: "PickedFromWarehouseId", + principalTable: "Warehouses", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrderLines_Warehouses_PickedFromWarehouseId", + table: "OrderLines"); + + migrationBuilder.DropIndex( + name: "IX_OrderLines_PickedFromWarehouseId", + table: "OrderLines"); + + migrationBuilder.DropColumn( + name: "CollectedAt", + table: "OrderLines"); + + migrationBuilder.DropColumn( + name: "PickedFromWarehouseId", + table: "OrderLines"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702124128_OrderCheckoutFields.Designer.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702124128_OrderCheckoutFields.Designer.cs new file mode 100644 index 0000000..494c0b7 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702124128_OrderCheckoutFields.Designer.cs @@ -0,0 +1,1707 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260702124128_OrderCheckoutFields")] + partial class OrderCheckoutFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingCity") + .HasColumnType("text"); + + b.Property("BillingCountry") + .HasColumnType("text"); + + b.Property("BillingPostalCode") + .HasColumnType("text"); + + b.Property("BillingStreet") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerNotes") + .HasColumnType("text"); + + b.Property("CustomerPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DeliveryMethod") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentMethod") + .HasColumnType("integer"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CollectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedFromWarehouseId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PickedFromWarehouseId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductGroupId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductGroupId"); + + b1.ToTable("ProductGroups"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductGroupId"); + }); + + b.Navigation("Names") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "PickedFromWarehouse") + .WithMany() + .HasForeignKey("PickedFromWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Order"); + + b.Navigation("PickedFromWarehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702124128_OrderCheckoutFields.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702124128_OrderCheckoutFields.cs new file mode 100644 index 0000000..7b5b395 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702124128_OrderCheckoutFields.cs @@ -0,0 +1,101 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + /// + public partial class OrderCheckoutFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BillingCity", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "BillingCountry", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "BillingPostalCode", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "BillingStreet", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "CustomerNotes", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "CustomerPhone", + table: "Orders", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DeliveryMethod", + table: "Orders", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "PaymentMethod", + table: "Orders", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BillingCity", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "BillingCountry", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "BillingPostalCode", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "BillingStreet", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "CustomerNotes", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "CustomerPhone", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "DeliveryMethod", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "PaymentMethod", + table: "Orders"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702125552_CarrierShipping.Designer.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702125552_CarrierShipping.Designer.cs new file mode 100644 index 0000000..bf0e61d --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702125552_CarrierShipping.Designer.cs @@ -0,0 +1,1791 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260702125552_CarrierShipping")] + partial class CarrierShipping + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingCity") + .HasColumnType("text"); + + b.Property("BillingCountry") + .HasColumnType("text"); + + b.Property("BillingPostalCode") + .HasColumnType("text"); + + b.Property("BillingStreet") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerNotes") + .HasColumnType("text"); + + b.Property("CustomerPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("PaymentMethod") + .HasColumnType("integer"); + + b.Property("ShippingCarrier") + .HasColumnType("integer"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingServiceType") + .HasColumnType("integer"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CollectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedFromWarehouseId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PickedFromWarehouseId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Carrier") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("ExternalShipmentId") + .HasColumnType("text"); + + b.Property("LabelRelativePath") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("ServiceType") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductGroupId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductGroupId"); + + b1.ToTable("ProductGroups"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductGroupId"); + }); + + b.Navigation("Names") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "PickedFromWarehouse") + .WithMany() + .HasForeignKey("PickedFromWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Order"); + + b.Navigation("PickedFromWarehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Shipment") + .HasForeignKey("iLED.Domain.Shipping.Shipment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + + b.Navigation("Shipment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/20260702125552_CarrierShipping.cs b/src/backend/src/iLED.Infrastructure/Migrations/20260702125552_CarrierShipping.cs new file mode 100644 index 0000000..1f52d7c --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/20260702125552_CarrierShipping.cs @@ -0,0 +1,111 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + /// + public partial class CarrierShipping : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "DeliveryMethod", + table: "Orders", + newName: "ShippingServiceType"); + + migrationBuilder.AddColumn( + name: "ParcelPointAddress", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "ParcelPointId", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "ParcelPointName", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "ShippingCarrier", + table: "Orders", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "Shipments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Carrier = table.Column(type: "integer", nullable: false), + ServiceType = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + TrackingNumber = table.Column(type: "text", nullable: true), + ExternalShipmentId = table.Column(type: "text", nullable: true), + LabelRelativePath = table.Column(type: "text", nullable: true), + ParcelPointId = table.Column(type: "text", nullable: true), + ParcelPointName = table.Column(type: "text", nullable: true), + ParcelPointAddress = table.Column(type: "text", nullable: true), + ErrorMessage = 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), + CreatedBy = table.Column(type: "text", nullable: true), + UpdatedBy = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Shipments", x => x.Id); + table.ForeignKey( + name: "FK_Shipments_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Shipments_OrderId", + table: "Shipments", + column: "OrderId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Shipments"); + + migrationBuilder.DropColumn( + name: "ParcelPointAddress", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "ParcelPointId", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "ParcelPointName", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "ShippingCarrier", + table: "Orders"); + + migrationBuilder.RenameColumn( + name: "ShippingServiceType", + table: "Orders", + newName: "DeliveryMethod"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/backend/src/iLED.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..2b0ca3f --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,1953 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IssuedForOrderId") + .HasColumnType("uuid"); + + b.Property("IssuedToEmail") + .HasColumnType("text"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("DiscountRuleId", "CustomerEmail"); + + b.ToTable("DiscountRedemptions"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannerHighlightedCode") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IssueRewardOnPaidOrder") + .HasColumnType("boolean"); + + b.Property("MaxRedemptionsPerCustomer") + .HasColumnType("integer"); + + b.Property("MaxTotalRedemptions") + .HasColumnType("integer"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RewardCodePrefix") + .HasColumnType("text"); + + b.Property("RewardMinPaidOrderTotal") + .HasColumnType("numeric"); + + b.Property("RewardValidDays") + .HasColumnType("integer"); + + b.Property("ShowInBanner") + .HasColumnType("boolean"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingCity") + .HasColumnType("text"); + + b.Property("BillingCountry") + .HasColumnType("text"); + + b.Property("BillingPostalCode") + .HasColumnType("text"); + + b.Property("BillingStreet") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerNotes") + .HasColumnType("text"); + + b.Property("CustomerPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("PaymentMethod") + .HasColumnType("integer"); + + b.Property("ShippingCarrier") + .HasColumnType("integer"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingServiceType") + .HasColumnType("integer"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CollectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedFromWarehouseId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PickedFromWarehouseId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Carrier") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("ExternalShipmentId") + .HasColumnType("text"); + + b.Property("LabelRelativePath") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("ServiceType") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductGroupId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductGroupId"); + + b1.ToTable("ProductGroups"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductGroupId"); + }); + + b.Navigation("Names") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRedemption", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Redemptions") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DiscountCode"); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "BannerTexts", b1 => + { + b1.Property("DiscountRuleId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("DiscountRuleId"); + + b1.ToTable("DiscountRules"); + + b1 + .ToJson("BannerTexts") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("DiscountRuleId"); + }); + + b.Navigation("BannerTexts") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Texts", b1 => + { + b1.Property("PromoBannerId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("PromoBannerId"); + + b1.ToTable("PromoBanners"); + + b1 + .ToJson("Texts") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("PromoBannerId"); + }); + + b.Navigation("Texts") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "PickedFromWarehouse") + .WithMany() + .HasForeignKey("PickedFromWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Order"); + + b.Navigation("PickedFromWarehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Shipment") + .HasForeignKey("iLED.Domain.Shipping.Shipment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + + b.Navigation("Redemptions"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + + b.Navigation("Shipment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Persistence/ApplicationDbContext.cs b/src/backend/src/iLED.Infrastructure/Persistence/ApplicationDbContext.cs new file mode 100644 index 0000000..661f99f --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Persistence/ApplicationDbContext.cs @@ -0,0 +1,261 @@ +using iLED.Domain.Cart; +using iLED.Domain.Catalog; +using iLED.Domain.Identity; +using iLED.Domain.Marketing; +using iLED.Domain.Orders; +using iLED.Domain.Payments; +using iLED.Domain.Shipping; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace iLED.Infrastructure.Persistence; + +public class ApplicationDbContext(DbContextOptions options) + : IdentityDbContext(options), + Application.Common.Interfaces.IAppDbContext +{ + public DbSet Products => Set(); + + public DbSet ProductGroups => Set(); + + public DbSet Categories => Set(); + + public DbSet ProductVariants => Set(); + + public DbSet Stocks => Set(); + + public DbSet Warehouses => Set(); + + public DbSet WarehouseStocks => Set(); + + public DbSet ProductImages => Set(); + + public DbSet ProductAttributes => Set(); + + public DbSet Reviews => Set(); + + public DbSet Carts => Set(); + + public DbSet CartItems => Set(); + + public DbSet FavoriteItems => Set(); + + public DbSet Orders => Set(); + + public DbSet OrderLines => Set(); + + public DbSet Payments => Set(); + + public DbSet Shipments => Set(); + + public DbSet DiscountRules => Set(); + + public DbSet DiscountCodes => Set(); + + public DbSet DiscountRedemptions => Set(); + + public DbSet PromoBanners => Set(); + + public DbSet NewsletterSubscribers => + Set(); + + public DbSet NewsletterCampaigns => + Set(); + + public DbSet CampaignSends => Set(); + + public DbSet RefreshTokens => Set(); + + IQueryable Application.Common.Interfaces.IAppDbContext.Products => + Products; + + IQueryable + Application.Common.Interfaces.IAppDbContext.ProductAttributes => + ProductAttributes; + + IQueryable + Application.Common.Interfaces.IAppDbContext.ProductImages => + ProductImages; + + IQueryable + Application.Common.Interfaces.IAppDbContext.ProductGroups => + ProductGroups; + + IQueryable Application.Common.Interfaces.IAppDbContext.Categories => + Categories; + + IQueryable + Application.Common.Interfaces.IAppDbContext.ProductVariants => + ProductVariants; + + IQueryable + Application.Common.Interfaces.IAppDbContext.Warehouses => Warehouses; + + IQueryable + Application.Common.Interfaces.IAppDbContext.WarehouseStocks => + WarehouseStocks; + + IQueryable Application.Common.Interfaces.IAppDbContext.Reviews => + Reviews; + + IQueryable Application.Common.Interfaces.IAppDbContext.Carts => Carts; + + IQueryable + Application.Common.Interfaces.IAppDbContext.FavoriteItems => + FavoriteItems; + + IQueryable Application.Common.Interfaces.IAppDbContext.Orders => + Orders; + + IQueryable Application.Common.Interfaces.IAppDbContext.Payments => + Payments; + + IQueryable Application.Common.Interfaces.IAppDbContext.Shipments => + Shipments; + + IQueryable + Application.Common.Interfaces.IAppDbContext.DiscountCodes => + DiscountCodes; + + IQueryable + Application.Common.Interfaces.IAppDbContext.DiscountRules => + DiscountRules; + + IQueryable + Application.Common.Interfaces.IAppDbContext.DiscountRedemptions => + DiscountRedemptions; + + IQueryable + Application.Common.Interfaces.IAppDbContext.PromoBanners => + PromoBanners; + + IQueryable Application.Common.Interfaces + .IAppDbContext.NewsletterSubscribers => NewsletterSubscribers; + + IQueryable Application.Common.Interfaces + .IAppDbContext.NewsletterCampaigns => NewsletterCampaigns; + + public new void Add(T entity) where T : class => Set().Add(entity); + + public new void Remove(T entity) where T : class => Set().Remove(entity); + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly); + + builder.Entity() + .HasIndex(p => p.Slug) + .IsUnique(); + + builder.Entity() + .OwnsOne(p => p.Names, b => b.ToJson()); + + builder.Entity() + .OwnsOne(p => p.ShortDescriptions, b => b.ToJson()); + + builder.Entity() + .OwnsOne(p => p.Descriptions, b => b.ToJson()); + + builder.Entity() + .HasOne(p => p.ProductGroup) + .WithMany(g => g.Products) + .HasForeignKey(p => p.ProductGroupId) + .OnDelete(DeleteBehavior.SetNull); + + builder.Entity() + .HasIndex(g => g.Code) + .IsUnique(); + + builder.Entity() + .OwnsOne(g => g.Names, b => b.ToJson()); + + builder.Entity() + .OwnsOne(a => a.Names, b => b.ToJson()); + + builder.Entity() + .OwnsOne(a => a.Values, b => b.ToJson()); + + builder.Entity() + .HasIndex(c => c.Slug) + .IsUnique(); + + builder.Entity() + .HasIndex(v => v.Sku) + .IsUnique(); + + builder.Entity() + .OwnsOne(r => r.BannerTexts, b => b.ToJson()); + + builder.Entity() + .OwnsOne(b => b.Texts, o => o.ToJson()); + + builder.Entity() + .HasIndex(c => c.Code) + .IsUnique(); + + builder.Entity() + .HasIndex(r => new { r.DiscountRuleId, r.CustomerEmail }); + + builder.Entity() + .HasOne(r => r.DiscountCode) + .WithMany() + .HasForeignKey(r => r.DiscountCodeId) + .OnDelete(DeleteBehavior.Restrict); + + builder.Entity() + .HasOne(r => r.DiscountRule) + .WithMany(rule => rule.Redemptions) + .HasForeignKey(r => r.DiscountRuleId) + .OnDelete(DeleteBehavior.Restrict); + + builder.Entity() + .HasIndex(s => s.Email) + .IsUnique(); + + builder.Entity() + .HasOne(s => s.ProductVariant) + .WithOne(v => v.Stock) + .HasForeignKey(s => s.ProductVariantId); + + builder.Entity() + .HasIndex(ws => new { ws.ProductVariantId, ws.WarehouseId }) + .IsUnique(); + + builder.Entity() + .HasOne(ws => ws.ProductVariant) + .WithMany(v => v.WarehouseStocks) + .HasForeignKey(ws => ws.ProductVariantId) + .OnDelete(DeleteBehavior.Cascade); + + builder.Entity() + .HasOne(ws => ws.Warehouse) + .WithMany(w => w.Stocks) + .HasForeignKey(ws => ws.WarehouseId) + .OnDelete(DeleteBehavior.Cascade); + + builder.Entity() + .HasIndex(w => w.Code) + .IsUnique(); + + builder.Entity() + .HasIndex(o => o.OrderNumber) + .IsUnique(); + + builder.Entity() + .HasOne(l => l.PickedFromWarehouse) + .WithMany() + .HasForeignKey(l => l.PickedFromWarehouseId) + .OnDelete(DeleteBehavior.SetNull); + + builder.Entity() + .HasIndex(s => s.OrderId) + .IsUnique(); + + builder.Entity() + .HasOne(s => s.Order) + .WithOne(o => o.Shipment) + .HasForeignKey(s => s.OrderId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009182217_PromotionsRework.Designer.cs b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009182217_PromotionsRework.Designer.cs new file mode 100644 index 0000000..7ff8aa1 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009182217_PromotionsRework.Designer.cs @@ -0,0 +1,1891 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20261009182217_PromotionsRework")] + partial class PromotionsRework + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IssuedForOrderId") + .HasColumnType("uuid"); + + b.Property("IssuedToEmail") + .HasColumnType("text"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("DiscountRuleId", "CustomerEmail"); + + b.ToTable("DiscountRedemptions"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannerHighlightedCode") + .HasColumnType("text"); + + b.Property("BannerText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IssueRewardOnPaidOrder") + .HasColumnType("boolean"); + + b.Property("MaxRedemptionsPerCustomer") + .HasColumnType("integer"); + + b.Property("MaxTotalRedemptions") + .HasColumnType("integer"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RewardCodePrefix") + .HasColumnType("text"); + + b.Property("RewardMinPaidOrderTotal") + .HasColumnType("numeric"); + + b.Property("RewardValidDays") + .HasColumnType("integer"); + + b.Property("ShowInBanner") + .HasColumnType("boolean"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingCity") + .HasColumnType("text"); + + b.Property("BillingCountry") + .HasColumnType("text"); + + b.Property("BillingPostalCode") + .HasColumnType("text"); + + b.Property("BillingStreet") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerNotes") + .HasColumnType("text"); + + b.Property("CustomerPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("PaymentMethod") + .HasColumnType("integer"); + + b.Property("ShippingCarrier") + .HasColumnType("integer"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingServiceType") + .HasColumnType("integer"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CollectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedFromWarehouseId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PickedFromWarehouseId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Carrier") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("ExternalShipmentId") + .HasColumnType("text"); + + b.Property("LabelRelativePath") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("ServiceType") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductGroupId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductGroupId"); + + b1.ToTable("ProductGroups"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductGroupId"); + }); + + b.Navigation("Names") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRedemption", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Redemptions") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DiscountCode"); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "PickedFromWarehouse") + .WithMany() + .HasForeignKey("PickedFromWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Order"); + + b.Navigation("PickedFromWarehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Shipment") + .HasForeignKey("iLED.Domain.Shipping.Shipment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + + b.Navigation("Redemptions"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + + b.Navigation("Shipment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009182217_PromotionsRework.cs b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009182217_PromotionsRework.cs new file mode 100644 index 0000000..344060e --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009182217_PromotionsRework.cs @@ -0,0 +1,196 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Persistence.Migrations +{ + /// + public partial class PromotionsRework : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BannerHighlightedCode", + table: "DiscountRules", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "BannerText", + table: "DiscountRules", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "IssueRewardOnPaidOrder", + table: "DiscountRules", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MaxRedemptionsPerCustomer", + table: "DiscountRules", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "MaxTotalRedemptions", + table: "DiscountRules", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "RewardCodePrefix", + table: "DiscountRules", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "RewardMinPaidOrderTotal", + table: "DiscountRules", + type: "numeric", + nullable: true); + + migrationBuilder.AddColumn( + name: "RewardValidDays", + table: "DiscountRules", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "ShowInBanner", + table: "DiscountRules", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IssuedForOrderId", + table: "DiscountCodes", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "IssuedToEmail", + table: "DiscountCodes", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Source", + table: "DiscountCodes", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ValidUntil", + table: "DiscountCodes", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.CreateTable( + name: "DiscountRedemptions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + DiscountCodeId = table.Column(type: "uuid", nullable: false), + DiscountRuleId = table.Column(type: "uuid", nullable: false), + CustomerEmail = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "text", nullable: true), + DiscountAmount = table.Column(type: "numeric", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DiscountRedemptions", x => x.Id); + table.ForeignKey( + name: "FK_DiscountRedemptions_DiscountCodes_DiscountCodeId", + column: x => x.DiscountCodeId, + principalTable: "DiscountCodes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_DiscountRedemptions_DiscountRules_DiscountRuleId", + column: x => x.DiscountRuleId, + principalTable: "DiscountRules", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_DiscountRedemptions_DiscountCodeId", + table: "DiscountRedemptions", + column: "DiscountCodeId"); + + migrationBuilder.CreateIndex( + name: "IX_DiscountRedemptions_DiscountRuleId_CustomerEmail", + table: "DiscountRedemptions", + columns: new[] { "DiscountRuleId", "CustomerEmail" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DiscountRedemptions"); + + migrationBuilder.DropColumn( + name: "BannerHighlightedCode", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "BannerText", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "IssueRewardOnPaidOrder", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "MaxRedemptionsPerCustomer", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "MaxTotalRedemptions", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "RewardCodePrefix", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "RewardMinPaidOrderTotal", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "RewardValidDays", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "ShowInBanner", + table: "DiscountRules"); + + migrationBuilder.DropColumn( + name: "IssuedForOrderId", + table: "DiscountCodes"); + + migrationBuilder.DropColumn( + name: "IssuedToEmail", + table: "DiscountCodes"); + + migrationBuilder.DropColumn( + name: "Source", + table: "DiscountCodes"); + + migrationBuilder.DropColumn( + name: "ValidUntil", + table: "DiscountCodes"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.Designer.cs b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.Designer.cs new file mode 100644 index 0000000..55c466b --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.Designer.cs @@ -0,0 +1,1956 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using iLED.Infrastructure.Persistence; + +#nullable disable + +namespace iLED.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20261009184551_ContentI18n")] + partial class ContentI18n + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("FavoriteItems"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCategoryId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentCategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DeliveryHours") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ean") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBestseller") + .HasColumnType("boolean"); + + b.Property("IsNewArrival") + .HasColumnType("boolean"); + + b.Property("LowestPrice30Days") + .HasColumnType("numeric"); + + b.Property("Manufacturer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterGroup") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductAttributes"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ProductGroups"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AltText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductImages"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ColorHex") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("PriceAdjustment") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.ToTable("ProductVariants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IssuedForOrderId") + .HasColumnType("uuid"); + + b.Property("IssuedToEmail") + .HasColumnType("text"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("DiscountRuleId", "CustomerEmail"); + + b.ToTable("DiscountRedemptions"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannerHighlightedCode") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IssueRewardOnPaidOrder") + .HasColumnType("boolean"); + + b.Property("MaxRedemptionsPerCustomer") + .HasColumnType("integer"); + + b.Property("MaxTotalRedemptions") + .HasColumnType("integer"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RewardCodePrefix") + .HasColumnType("text"); + + b.Property("RewardMinPaidOrderTotal") + .HasColumnType("numeric"); + + b.Property("RewardValidDays") + .HasColumnType("integer"); + + b.Property("ShowInBanner") + .HasColumnType("boolean"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingCity") + .HasColumnType("text"); + + b.Property("BillingCountry") + .HasColumnType("text"); + + b.Property("BillingPostalCode") + .HasColumnType("text"); + + b.Property("BillingStreet") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerNotes") + .HasColumnType("text"); + + b.Property("CustomerPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("PaymentMethod") + .HasColumnType("integer"); + + b.Property("ShippingCarrier") + .HasColumnType("integer"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingServiceType") + .HasColumnType("integer"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CollectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedFromWarehouseId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PickedFromWarehouseId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Carrier") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("ExternalShipmentId") + .HasColumnType("text"); + + b.Property("LabelRelativePath") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("ServiceType") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductGroupId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductGroupId"); + + b1.ToTable("ProductGroups"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductGroupId"); + }); + + b.Navigation("Names") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRedemption", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Redemptions") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DiscountCode"); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "BannerTexts", b1 => + { + b1.Property("DiscountRuleId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("DiscountRuleId"); + + b1.ToTable("DiscountRules"); + + b1 + .ToJson("BannerTexts") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("DiscountRuleId"); + }); + + b.Navigation("BannerTexts") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Texts", b1 => + { + b1.Property("PromoBannerId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("PromoBannerId"); + + b1.ToTable("PromoBanners"); + + b1 + .ToJson("Texts") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("PromoBannerId"); + }); + + b.Navigation("Texts") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "PickedFromWarehouse") + .WithMany() + .HasForeignKey("PickedFromWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Order"); + + b.Navigation("PickedFromWarehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Shipment") + .HasForeignKey("iLED.Domain.Shipping.Shipment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + + b.Navigation("Redemptions"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + + b.Navigation("Shipment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.cs b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.cs new file mode 100644 index 0000000..ea103d7 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261009184551_ContentI18n.cs @@ -0,0 +1,105 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Persistence.Migrations +{ + /// + public partial class ContentI18n : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BannerTexts", + table: "DiscountRules", + type: "jsonb", + nullable: false, + defaultValueSql: "'{}'::jsonb"); + + migrationBuilder.Sql( + """ + UPDATE "DiscountRules" + SET "BannerTexts" = jsonb_build_object( + 'Pl', COALESCE("BannerText", ''), + 'En', COALESCE("BannerText", ''), + 'De', COALESCE("BannerText", ''), + 'Cs', COALESCE("BannerText", '')) + WHERE "BannerText" IS NOT NULL AND "BannerText" <> ''; + """); + + migrationBuilder.DropColumn( + name: "BannerText", + table: "DiscountRules"); + + migrationBuilder.AddColumn( + name: "Texts", + table: "PromoBanners", + type: "jsonb", + nullable: false, + defaultValueSql: "'{}'::jsonb"); + + migrationBuilder.Sql( + """ + UPDATE "PromoBanners" + SET "Texts" = jsonb_build_object( + 'Pl', COALESCE("Text", ''), + 'En', COALESCE("Text", ''), + 'De', COALESCE("Text", ''), + 'Cs', COALESCE("Text", '')); + """); + + migrationBuilder.DropColumn( + name: "Text", + table: "PromoBanners"); + + migrationBuilder.AddColumn( + name: "Locale", + table: "Orders", + type: "text", + nullable: false, + defaultValue: "pl"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BannerText", + table: "DiscountRules", + type: "text", + nullable: true); + + migrationBuilder.Sql( + """ + UPDATE "DiscountRules" + SET "BannerText" = NULLIF("BannerTexts"->>'Pl', ''); + """); + + migrationBuilder.DropColumn( + name: "BannerTexts", + table: "DiscountRules"); + + migrationBuilder.AddColumn( + name: "Text", + table: "PromoBanners", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.Sql( + """ + UPDATE "PromoBanners" + SET "Text" = COALESCE("Texts"->>'Pl', ''); + """); + + migrationBuilder.DropColumn( + name: "Texts", + table: "PromoBanners"); + + migrationBuilder.DropColumn( + name: "Locale", + table: "Orders"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Persistence/SeedData.cs b/src/backend/src/iLED.Infrastructure/Persistence/SeedData.cs new file mode 100644 index 0000000..cdf6b6c --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Persistence/SeedData.cs @@ -0,0 +1,804 @@ +using iLED.Domain.Catalog; +using iLED.Domain.Common; +using iLED.Domain.Identity; +using iLED.Domain.Marketing; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace iLED.Infrastructure.Persistence; + +public static class SeedData +{ + public static async Task InitializeAsync(IServiceProvider services) + { + using var scope = services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider + .GetRequiredService>(); + var roleManager = scope.ServiceProvider + .GetRequiredService>(); + var logger = scope.ServiceProvider + .GetRequiredService>(); + + await db.Database.MigrateAsync(); + await SeedRolesAsync(roleManager); + await SeedUsersAsync(userManager); + await SeedWarehousesAsync(db); + await SeedProductGroupsAsync(db); + await BackfillProductLocalizationAsync(db); + await BackfillProductImageUrlsAsync(db); + await SeedCatalogAsync(db); + await BackfillProductGroupsAsync(db); + await SeedReviewsAsync(db); + await SeedMarketingAsync(db); + logger.LogInformation("Database seeded successfully."); + } + + private static async Task BackfillProductImageUrlsAsync(ApplicationDbContext db) + { + var images = await db.ProductImages + .Where(i => i.Url.EndsWith(".jpg")) + .ToListAsync(); + if (images.Count == 0) + { + return; + } + + foreach (var image in images) + { + image.Url = image.Url[..^4] + ".svg"; + } + + await db.SaveChangesAsync(); + } + + private static async Task SeedRolesAsync(RoleManager roleManager) + { + foreach (var role in new[] { "Admin", "Customer" }) + { + if (!await roleManager.RoleExistsAsync(role)) + { + await roleManager.CreateAsync(new IdentityRole(role)); + } + } + } + + private static async Task SeedUsersAsync(UserManager userManager) + { + if (await userManager.FindByEmailAsync("admin@iled.pl") is null) + { + var admin = new ApplicationUser + { + UserName = "admin@iled.pl", + Email = "admin@iled.pl", + FirstName = "Admin", + LastName = "iLED", + EmailConfirmed = true, + }; + await userManager.CreateAsync(admin, "Admin123!"); + await userManager.AddToRoleAsync(admin, "Admin"); + await userManager.AddToRoleAsync(admin, "Customer"); + } + } + + private static async Task SeedProductGroupsAsync(ApplicationDbContext db) + { + if (await db.ProductGroups.AnyAsync()) + { + return; + } + + db.ProductGroups.AddRange( + CreateGroup( + "Lampy wiszące", + "PENDANT", + 1, + "Pendant lamps", + "Pendelleuchten", + "Závěsná svítidla"), + CreateGroup( + "Lampy podłogowe", + "FLOOR", + 2, + "Floor lamps", + "Stehleuchten", + "Stojací lampy"), + CreateGroup( + "Kinkiety", + "WALL", + 3, + "Wall lamps", + "Wandleuchten", + "Nástěnná svítidla")); + await db.SaveChangesAsync(); + } + + private static ProductGroup CreateGroup( + string namePl, + string code, + int sortOrder, + string? nameEn = null, + string? nameDe = null, + string? nameCs = null) => + new() + { + Name = namePl, + Names = new LocalizedStrings + { + Pl = namePl, + En = nameEn ?? namePl, + De = nameDe ?? nameEn ?? namePl, + Cs = nameCs ?? nameEn ?? namePl, + }, + Code = code, + SortOrder = sortOrder, + }; + + private static void EnsureGroupLocale( + ProductGroup group, + string pl, + string en, + string de, + string cs, + ref bool changed) + { + if (string.IsNullOrWhiteSpace(group.Names.Pl) || + string.IsNullOrWhiteSpace(group.Names.En) || + string.IsNullOrWhiteSpace(group.Names.De) || + string.IsNullOrWhiteSpace(group.Names.Cs)) + { + group.Name = pl; + group.Names = new LocalizedStrings + { + Pl = pl, + En = en, + De = de, + Cs = cs, + }; + changed = true; + } + } + + private static async Task SeedWarehousesAsync(ApplicationDbContext db) + { + if (!await db.Warehouses.AnyAsync()) + { + db.Warehouses.AddRange( + new Warehouse + { + Name = "Magazyn główny", + Code = "MAIN", + SortOrder = 1, + }, + new Warehouse + { + Name = "Magazyn II", + Code = "SECOND", + SortOrder = 2, + }); + await db.SaveChangesAsync(); + } + + var main = await db.Warehouses.FirstAsync(w => w.Code == "MAIN"); + var second = await db.Warehouses.FirstAsync(w => w.Code == "SECOND"); + + var variants = await db.ProductVariants + .Include(v => v.Stock) + .Include(v => v.WarehouseStocks) + .ToListAsync(); + + foreach (var variant in variants) + { + if (variant.WarehouseStocks.Count > 0) + { + continue; + } + + var total = variant.Stock.Quantity; + var mainQty = (int)Math.Ceiling(total * 0.6m); + var secondQty = total - mainQty; + + db.WarehouseStocks.AddRange( + new WarehouseStock + { + ProductVariantId = variant.Id, + WarehouseId = main.Id, + Quantity = mainQty, + }, + new WarehouseStock + { + ProductVariantId = variant.Id, + WarehouseId = second.Id, + Quantity = secondQty, + }); + } + + await db.SaveChangesAsync(); + } + + private static async Task BackfillProductLocalizationAsync(ApplicationDbContext db) + { + var products = await db.Products + .Include(p => p.Attributes) + .ToListAsync(); + var changed = false; + + foreach (var product in products) + { + if (string.IsNullOrWhiteSpace(product.Names.Pl) && + !string.IsNullOrWhiteSpace(product.Name)) + { + product.Names = new LocalizedStrings + { + Pl = product.Name, + En = product.Name, + De = product.Name, + Cs = product.Name, + }; + changed = true; + } + else if (!string.IsNullOrWhiteSpace(product.Names.Pl) && + (string.IsNullOrWhiteSpace(product.Names.De) || + string.IsNullOrWhiteSpace(product.Names.Cs))) + { + if (string.IsNullOrWhiteSpace(product.Names.De)) + { + product.Names.De = product.Names.En.Length > 0 + ? product.Names.En + : product.Names.Pl; + } + + if (string.IsNullOrWhiteSpace(product.Names.Cs)) + { + product.Names.Cs = product.Names.En.Length > 0 + ? product.Names.En + : product.Names.Pl; + } + + changed = true; + } + + if (string.IsNullOrWhiteSpace(product.ShortDescriptions.Pl) && + !string.IsNullOrWhiteSpace(product.ShortDescription)) + { + product.ShortDescriptions = new LocalizedStrings + { + Pl = product.ShortDescription, + En = product.ShortDescription, + De = product.ShortDescription, + Cs = product.ShortDescription, + }; + changed = true; + } + + if (string.IsNullOrWhiteSpace(product.Descriptions.Pl) && + !string.IsNullOrWhiteSpace(product.Description)) + { + product.Descriptions = new LocalizedStrings + { + Pl = product.Description, + En = product.Description, + De = product.Description, + Cs = product.Description, + }; + changed = true; + } + + if (string.IsNullOrWhiteSpace(product.Model) && + !string.IsNullOrWhiteSpace(product.ProductCode)) + { + product.Model = product.ProductCode; + changed = true; + } + + foreach (var attribute in product.Attributes) + { + if (string.IsNullOrWhiteSpace(attribute.Names.Pl) && + !string.IsNullOrWhiteSpace(attribute.Name)) + { + attribute.Names = new LocalizedStrings + { + Pl = attribute.Name, + En = attribute.Name, + }; + changed = true; + } + + if (string.IsNullOrWhiteSpace(attribute.Values.Pl) && + !string.IsNullOrWhiteSpace(attribute.Value)) + { + attribute.Values = new LocalizedStrings + { + Pl = attribute.Value, + En = attribute.Value, + }; + changed = true; + } + } + } + + if (changed) + { + await db.SaveChangesAsync(); + } + + var groups = await db.ProductGroups.ToListAsync(); + foreach (var group in groups) + { + if (group.Code == "PENDANT") + { + EnsureGroupLocale( + group, + "Lampy wiszące", + "Pendant lamps", + "Pendelleuchten", + "Závěsná svítidla", + ref changed); + } + else if (group.Code == "FLOOR") + { + EnsureGroupLocale( + group, + "Lampy podłogowe", + "Floor lamps", + "Stehleuchten", + "Stojací lampy", + ref changed); + } + else if (group.Code == "WALL") + { + EnsureGroupLocale( + group, + "Kinkiety", + "Wall lamps", + "Wandleuchten", + "Nástěnná svítidla", + ref changed); + } + + if (string.IsNullOrWhiteSpace(group.Names.Pl) && + !string.IsNullOrWhiteSpace(group.Name)) + { + group.Names = new LocalizedStrings + { + Pl = group.Name, + En = group.Name, + }; + changed = true; + } + } + + if (changed) + { + await db.SaveChangesAsync(); + } + } + + private static async Task BackfillProductGroupsAsync(ApplicationDbContext db) + { + var groups = await db.ProductGroups.ToListAsync(); + if (groups.Count == 0) + { + return; + } + + var pendant = groups.FirstOrDefault(g => g.Code == "PENDANT"); + var floor = groups.FirstOrDefault(g => g.Code == "FLOOR"); + var wall = groups.FirstOrDefault(g => g.Code == "WALL"); + var fallback = pendant ?? groups[0]; + + var products = await db.Products + .Where(p => p.ProductGroupId == null) + .ToListAsync(); + + if (products.Count == 0) + { + return; + } + + foreach (var product in products) + { + var slug = product.Slug.ToLowerInvariant(); + if (slug.Contains("podlog") || slug.Contains("floor")) + { + product.ProductGroupId = floor?.Id ?? fallback.Id; + } + else if (slug.Contains("kinkiet") || slug.Contains("wall")) + { + product.ProductGroupId = wall?.Id ?? fallback.Id; + } + else + { + product.ProductGroupId = pendant?.Id ?? fallback.Id; + } + } + + await db.SaveChangesAsync(); + } + + private static async Task SeedCatalogAsync(ApplicationDbContext db) + { + if (await db.Products.AnyAsync()) + { + return; + } + + var category = new Category + { + Name = "Lampy sufitowe", + Slug = "lampy-sufitowe", + Description = "Nowoczesne lampy sufitowe LED", + }; + db.Categories.Add(category); + + var pendant = await db.ProductGroups + .FirstOrDefaultAsync(g => g.Code == "PENDANT"); + var floor = await db.ProductGroups + .FirstOrDefaultAsync(g => g.Code == "FLOOR"); + + var products = new[] + { + CreateProduct( + "Lampa sufitowa LED drewno, CCT", + "lampa-sufitowa-led-drewno-cct", + "Regulacja barwy światła i drewno w jednym.", + "Ceiling LED with wood accent and CCT control.", + "Lampa sufitowa z akcentem drewnianym i płynną regulacją temperatury barwowej (CCT). Idealna do salonu i jadalni — ciepłe światło wieczorem, chłodniejsze do pracy.", + "Ceiling lamp with a wood accent and smooth CCT control. Warm evenings, cooler task light when you need focus.", + 499.99m, + "5345346436", + true, + false, + category.Id, + pendant?.Id, + "#000000", + "czarny", + "48 W", + "drewno, metal", + "520 mm"), + CreateProduct( + "Lampa LED Gold", + "lampa-led-gold", + "Złoty glamour do eleganckich wnętrz.", + "Gold-finish LED for elegant interiors.", + "Elegancka lampa LED w złotym wykończeniu. Miękkie, równomierne światło podkreśla faktury i dodaje ciepła nowoczesnym aranżacjom.", + "Elegant gold-finish LED lamp. Soft, even light that flatters textures in modern rooms.", + 499.99m, + "5345346437", + true, + false, + category.Id, + pendant?.Id, + "#FFBC39", + "złoty", + "45 W", + "metal", + "500 mm"), + CreateProduct( + "Lampa LED Wood", + "lampa-led-wood", + "Naturalne drewno i ciepłe LED.", + "Natural wood with warm LED light.", + "Ciepła lampa z naturalnym drewnem i dyskretnym źródłem LED. Spokojny charakter do sypialni i stref relaksu.", + "Warm lamp with natural wood and a discreet LED source. Calm character for bedrooms and lounge corners.", + 479.99m, + "5345346438", + false, + true, + category.Id, + pendant?.Id, + "#D4A574", + "drewno", + "40 W", + "drewno, metal", + "480 mm"), + CreateProduct( + "Lampa LED Black", + "lampa-led-black", + "Matowa czerń, czysta forma.", + "Matte black, clean form.", + "Matowa czarna lampa sufitowa o minimalistycznej formie. Mocne, skupione światło bez zbędnych ozdób.", + "Matte black ceiling lamp with a minimalist silhouette. Strong, focused light without clutter.", + 449.99m, + "5345346439", + false, + true, + category.Id, + pendant?.Id, + "#000000", + "czarny", + "42 W", + "metal", + "460 mm"), + CreateProduct( + "Lampa LED Strip", + "lampa-led-strip", + "Smukły pasek LED, nowoczesny profil.", + "Slim LED profile for modern ceilings.", + "Smukła lampa liniowa z paskiem LED. Świetna nad stołem lub wyspą kuchenną — równomierne oświetlenie bez ostrych cieni.", + "Slim linear LED lamp. Great above a table or kitchen island — even light without harsh shadows.", + 399.99m, + "5345346440", + false, + false, + category.Id, + floor?.Id ?? pendant?.Id, + "#8C8C8C", + "srebrny", + "36 W", + "aluminium", + "900 mm"), + }; + + db.Products.AddRange(products); + await db.SaveChangesAsync(); + } + + private static async Task SeedReviewsAsync(ApplicationDbContext db) + { + if (await db.Reviews.AnyAsync()) + { + return; + } + + var gold = await db.Products.FirstOrDefaultAsync(p => p.Slug == "lampa-led-gold"); + var woodCct = await db.Products.FirstOrDefaultAsync( + p => p.Slug == "lampa-sufitowa-led-drewno-cct"); + if (gold is null || woodCct is null) + { + return; + } + + db.Reviews.AddRange( + new Review + { + ProductId = gold.Id, + AuthorName = "Anna K.", + AuthorEmail = "anna@example.com", + Rating = 5, + Content = "Piękne złote wykończenie, światło równomierne. Montaż prosty.", + IsApproved = true, + }, + new Review + { + ProductId = gold.Id, + AuthorName = "Marek", + AuthorEmail = "marek@example.com", + Rating = 4, + Content = + "Bardzo ładna lampa, trochę cięższa niż myślałem — warto solidny strop.", + IsApproved = true, + }, + new Review + { + ProductId = woodCct.Id, + AuthorName = "Ola", + AuthorEmail = "ola@example.com", + Rating = 5, + Content = + "CCT działa świetnie — wieczorem ciepło, w dzień chłodniej do pracy.", + IsApproved = true, + }, + new Review + { + ProductId = woodCct.Id, + AuthorName = "Tomek", + AuthorEmail = "tomek@example.com", + Rating = 5, + Content = "Drewno wygląda premium, paczka dobrze zabezpieczona.", + IsApproved = true, + }); + await db.SaveChangesAsync(); + } + + private static ProductAttribute CreateAttribute( + string name, + string value, + string? filterGroup = null) + { + return new ProductAttribute + { + Name = name, + Value = value, + Names = new LocalizedStrings + { + Pl = name, + En = name, + De = name, + Cs = name, + }, + Values = new LocalizedStrings + { + Pl = value, + En = value, + De = value, + Cs = value, + }, + FilterGroup = filterGroup, + }; + } + + private static Product CreateProduct( + string name, + string slug, + string shortDescPl, + string shortDescEn, + string descPl, + string descEn, + decimal price, + string code, + bool bestseller, + bool isNew, + Guid categoryId, + Guid? productGroupId, + string colorHex, + string colorName, + string power, + string material, + string height) + { + var product = new Product + { + Name = name, + Slug = slug, + Names = new LocalizedStrings + { + Pl = name, + En = name, + De = name, + Cs = name, + }, + ShortDescription = shortDescPl, + ShortDescriptions = new LocalizedStrings + { + Pl = shortDescPl, + En = shortDescEn, + De = shortDescEn, + Cs = shortDescEn, + }, + Description = descPl, + Descriptions = new LocalizedStrings + { + Pl = descPl, + En = descEn, + De = descEn, + Cs = descEn, + }, + BasePrice = price, + LowestPrice30Days = price, + ProductCode = code, + Model = code, + IsBestseller = bestseller, + IsNewArrival = isNew, + CategoryId = categoryId, + ProductGroupId = productGroupId, + Images = + [ + new ProductImage + { + Url = $"/assets/products/{slug}.svg", + AltText = name, + SortOrder = 0, + IsPrimary = true, + }, + ], + Attributes = + [ + CreateAttribute("Moc", power), + CreateAttribute("Materiał", material, "Materiał"), + CreateAttribute("Wysokość", height), + ], + }; + + product.Variants.Add(new ProductVariant + { + Sku = $"{code}-01", + ColorName = colorName, + ColorHex = colorHex, + IsDefault = true, + Stock = new Stock { Quantity = 25, LowStockThreshold = 5 }, + }); + + return product; + } + + private static async Task SeedMarketingAsync(ApplicationDbContext db) + { + if (!await db.DiscountCodes.AnyAsync()) + { + var bannerTexts = new LocalizedStrings + { + Pl = "Promocja z kodem", + En = "Promo with code", + De = "Aktion mit Code", + Cs = "Akce s kódem", + }; + var rule = new DiscountRule + { + Name = "LOVE20 - 20% rabatu", + Type = Domain.Enums.DiscountType.Percentage, + Value = 20, + IsActive = true, + MaxTotalRedemptions = 1000, + ShowInBanner = true, + BannerTexts = bannerTexts, + BannerHighlightedCode = "LOVE20", + }; + db.DiscountRules.Add(rule); + await db.SaveChangesAsync(); + + db.DiscountCodes.Add(new DiscountCode + { + DiscountRuleId = rule.Id, + Code = "LOVE20", + MaxUsages = 1000, + Source = Domain.Enums.DiscountCodeSource.Manual, + }); + + db.PromoBanners.Add(new PromoBanner + { + Texts = new LocalizedStrings + { + Pl = bannerTexts.Pl, + En = bannerTexts.En, + De = bannerTexts.De, + Cs = bannerTexts.Cs, + }, + HighlightedCode = "LOVE20", + IsActive = true, + }); + } + else if (!await db.PromoBanners.AnyAsync()) + { + db.PromoBanners.Add(new PromoBanner + { + Texts = new LocalizedStrings + { + Pl = "Promocja z kodem", + En = "Promo with code", + De = "Aktion mit Code", + Cs = "Akce s kódem", + }, + HighlightedCode = "LOVE20", + IsActive = true, + }); + } + + await BackfillBannerLocalesAsync(db); + await db.SaveChangesAsync(); + } + + private static async Task BackfillBannerLocalesAsync(ApplicationDbContext db) + { + var polishDefault = "Promocja z kodem"; + var rules = await db.DiscountRules + .Where(r => r.ShowInBanner) + .ToListAsync(); + foreach (var rule in rules) + { + if (rule.BannerTexts.Pl == polishDefault && + (rule.BannerTexts.En == polishDefault || + string.IsNullOrWhiteSpace(rule.BannerTexts.En))) + { + rule.BannerTexts.En = "Promo with code"; + rule.BannerTexts.De = "Aktion mit Code"; + rule.BannerTexts.Cs = "Akce s kódem"; + } + } + + var banners = await db.PromoBanners.ToListAsync(); + foreach (var banner in banners) + { + if (banner.Texts.Pl == polishDefault && + (banner.Texts.En == polishDefault || + string.IsNullOrWhiteSpace(banner.Texts.En))) + { + banner.Texts.En = "Promo with code"; + banner.Texts.De = "Aktion mit Code"; + banner.Texts.Cs = "Akce s kódem"; + } + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Services/CartService.cs b/src/backend/src/iLED.Infrastructure/Services/CartService.cs new file mode 100644 index 0000000..d9d60a3 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/CartService.cs @@ -0,0 +1,226 @@ +using iLED.Application.Cart.Commands; +using iLED.Application.Common.Interfaces; +using iLED.Domain.Cart; +using Microsoft.EntityFrameworkCore; +using Persistence = iLED.Infrastructure.Persistence; + +namespace iLED.Infrastructure.Services; + +public sealed class CartService( + Persistence.ApplicationDbContext db, + ICurrentUserService currentUser) : ICartService +{ + public async Task GetCartAsync(CancellationToken cancellationToken) + { + var cart = await GetOrCreateCartAsync(cancellationToken); + return await MapCartAsync(cart.Id, cancellationToken); + } + + public async Task AddItemAsync( + Guid productId, + Guid variantId, + int quantity, + CancellationToken cancellationToken) + { + var cart = await GetOrCreateCartAsync(cancellationToken); + var existing = await db.CartItems.FirstOrDefaultAsync( + i => i.CartId == cart.Id && + i.ProductVariantId == variantId, + cancellationToken); + + if (existing is not null) + { + existing.Quantity += quantity; + } + else + { + db.CartItems.Add(new CartItem + { + CartId = cart.Id, + ProductId = productId, + ProductVariantId = variantId, + Quantity = quantity, + }); + } + + await db.SaveChangesAsync(cancellationToken); + return await MapCartAsync(cart.Id, cancellationToken); + } + + public async Task UpdateItemAsync( + Guid itemId, + int quantity, + CancellationToken cancellationToken) + { + var item = await db.CartItems + .FirstOrDefaultAsync(i => i.Id == itemId, cancellationToken) + ?? throw new Application.Common.Exceptions.NotFoundException( + "Pozycja koszyka nie znaleziona."); + + if (quantity <= 0) + { + db.CartItems.Remove(item); + } + else + { + item.Quantity = quantity; + } + + await db.SaveChangesAsync(cancellationToken); + return await MapCartAsync(item.CartId, cancellationToken); + } + + public async Task RemoveItemAsync( + Guid itemId, + CancellationToken cancellationToken) + { + var item = await db.CartItems + .FirstOrDefaultAsync(i => i.Id == itemId, cancellationToken) + ?? throw new Application.Common.Exceptions.NotFoundException( + "Pozycja koszyka nie znaleziona."); + + db.CartItems.Remove(item); + await db.SaveChangesAsync(cancellationToken); + return await MapCartAsync(item.CartId, cancellationToken); + } + + public async Task MergeGuestCartAsync( + string sessionId, + string userId, + CancellationToken cancellationToken) + { + var guestCart = await db.Carts + .Include(c => c.Items) + .FirstOrDefaultAsync(c => c.SessionId == sessionId, cancellationToken); + + if (guestCart is null || guestCart.Items.Count == 0) + { + return; + } + + var userCart = await db.Carts + .Include(c => c.Items) + .FirstOrDefaultAsync(c => c.UserId == userId, cancellationToken); + + if (userCart is null) + { + guestCart.UserId = userId; + guestCart.SessionId = null; + } + else + { + foreach (var item in guestCart.Items) + { + var existing = userCart.Items.FirstOrDefault( + i => i.ProductVariantId == item.ProductVariantId); + if (existing is not null) + { + existing.Quantity += item.Quantity; + } + else + { + item.CartId = userCart.Id; + } + } + + db.Carts.Remove(guestCart); + } + + await db.SaveChangesAsync(cancellationToken); + } + + private async Task GetOrCreateCartAsync(CancellationToken cancellationToken) + { + Cart? cart = null; + + if (currentUser.UserId is not null) + { + cart = await db.Carts + .FirstOrDefaultAsync(c => c.UserId == currentUser.UserId, cancellationToken); + } + else if (currentUser.SessionId is not null) + { + cart = await db.Carts + .FirstOrDefaultAsync( + c => c.SessionId == currentUser.SessionId, + cancellationToken); + } + + if (cart is not null) + { + return cart; + } + + cart = new Cart + { + UserId = currentUser.UserId, + SessionId = currentUser.UserId is null + ? currentUser.SessionId ?? Guid.NewGuid().ToString() + : null, + }; + db.Carts.Add(cart); + await db.SaveChangesAsync(cancellationToken); + return cart; + } + + private async Task MapCartAsync( + Guid cartId, + CancellationToken cancellationToken) + { + var items = await db.CartItems + .AsNoTracking() + .Where(i => i.CartId == cartId) + .Select(i => new + { + i.Id, + i.ProductId, + VariantId = i.ProductVariantId, + ProductName = i.ProductId.ToString(), + Product = db.Products + .Where(p => p.Id == i.ProductId) + .Select(p => new + { + p.Name, + p.Names, + p.Slug, + p.BasePrice, + Image = p.Images.OrderBy(img => img.SortOrder) + .Select(img => img.Url).FirstOrDefault(), + }).First(), + Variant = db.ProductVariants + .Where(v => v.Id == i.ProductVariantId) + .Select(v => new + { + v.ColorName, + v.PriceAdjustment, + InStock = v.Stock.Quantity > 0, + }).First(), + i.Quantity, + }) + .ToListAsync(cancellationToken); + + var locale = currentUser.Locale; + var mapped = items.Select(i => + { + var price = i.Product.BasePrice + i.Variant.PriceAdjustment; + return new CartItemDto( + i.Id, + i.ProductId, + i.VariantId, + i.Product.Names.Resolve(locale, i.Product.Name), + i.Product.Slug, + i.Variant.ColorName, + price, + i.Quantity, + i.Product.Image, + i.Variant.InStock, + price * i.Quantity); + }).ToList(); + + return new CartDto( + cartId, + mapped, + mapped.Sum(i => i.LineTotal), + mapped.Sum(i => i.Quantity)); + } +} diff --git a/src/backend/src/iLED.Infrastructure/Services/DpdPudoClient.cs b/src/backend/src/iLED.Infrastructure/Services/DpdPudoClient.cs new file mode 100644 index 0000000..bfa92b6 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/DpdPudoClient.cs @@ -0,0 +1,388 @@ +using System.Collections.Concurrent; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using iLED.Application.Common.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace iLED.Infrastructure.Services; + +/// +/// Loads DPD Pickup points from the public pudofinder catalogue. +/// pickup.dpd.pl is not usable (broken TLS / marketing site). +/// +public sealed class DpdPudoClient( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + ILogger logger) +{ + private const string DefaultBaseUrl = "https://pudofinder.dpd.com.pl"; + private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(6); + private static readonly ConcurrentDictionary Cache = new(); + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + public async Task> SearchAsync( + string query, + double? latitude, + double? longitude, + CancellationToken cancellationToken) + { + var catalogue = await GetCatalogueAsync(cancellationToken); + var normalized = (query ?? string.Empty).Trim(); + + // Direct point-code lookup (e.g. PL19299). + if (LooksLikePointId(normalized)) + { + var exact = catalogue.FirstOrDefault(p => + p.Enabled && + p.Id.Equals(normalized, StringComparison.OrdinalIgnoreCase)); + if (exact is null) + { + return []; + } + + var one = WithDistance(exact, latitude, longitude); + var oneDetails = await GetDetailsAsync([one.Id], cancellationToken); + return [MergeDetails(one, oneDetails.GetValueOrDefault(one.Id))]; + } + + var originLat = latitude; + var originLng = longitude; + if ((originLat is null || originLng is null) && + !string.IsNullOrWhiteSpace(normalized)) + { + var geocoded = await GeocodeAsync(normalized, cancellationToken); + originLat = geocoded?.Lat; + originLng = geocoded?.Lng; + } + + // Default map center when neither query nor coords are provided. + originLat ??= 52.2297; + originLng ??= 21.0122; + + var matches = catalogue + .Where(p => p.Enabled) + .Select(p => WithDistance(p, originLat, originLng)) + .OrderBy(p => p.DistanceKm ?? double.MaxValue) + .ThenBy(p => p.Name, StringComparer.OrdinalIgnoreCase) + .Take(40) + .ToList(); + + if (matches.Count == 0) + { + return []; + } + + var details = await GetDetailsAsync( + matches.Select(p => p.Id).ToArray(), + cancellationToken); + + return matches + .Select(p => MergeDetails(p, details.GetValueOrDefault(p.Id))) + .ToList(); + } + + private async Task> GetCatalogueAsync( + CancellationToken cancellationToken) + { + var baseUrl = GetBaseUrl(); + if (Cache.TryGetValue(baseUrl, out var cached) && + cached.ExpiresAt > DateTimeOffset.UtcNow) + { + return cached.Points; + } + + var client = httpClientFactory.CreateClient(nameof(DpdPudoClient)); + var url = $"{baseUrl}/ajax/search?lang=pl&country=PL&address="; + using var response = await client.GetAsync(url, cancellationToken); + response.EnsureSuccessStatusCode(); + + await using var stream = await response.Content.ReadAsStreamAsync( + cancellationToken); + var points = await JsonSerializer.DeserializeAsync>( + stream, + JsonOptions, + cancellationToken) ?? []; + + Cache[baseUrl] = new CacheEntry( + points, + DateTimeOffset.UtcNow.Add(CacheTtl)); + + logger.LogInformation( + "Cached {Count} DPD Pickup points from pudofinder", + points.Count); + + return points; + } + + private async Task> GetDetailsAsync( + IReadOnlyList ids, + CancellationToken cancellationToken) + { + if (ids.Count == 0) + { + return []; + } + + var client = httpClientFactory.CreateClient(nameof(DpdPudoClient)); + var baseUrl = GetBaseUrl(); + var url = $"{baseUrl}/ajax/points/details?lang=pl"; + + using var content = new FormUrlEncodedContent( + ids.Select(id => new KeyValuePair("points[]", id))); + + using var response = await client.PostAsync( + url, + content, + cancellationToken); + + if (!response.IsSuccessStatusCode) + { + logger.LogWarning( + "DPD point details failed with {Status}", + response.StatusCode); + return []; + } + + var points = await response.Content.ReadFromJsonAsync>( + JsonOptions, + cancellationToken) ?? []; + + return points + .Where(p => p.Details is not null) + .ToDictionary( + p => p.Id, + p => p.Details!, + StringComparer.OrdinalIgnoreCase); + } + + private string GetBaseUrl() + { + var configured = configuration["Shipping:Dpd:WidgetUrl"]; + if (string.IsNullOrWhiteSpace(configured)) + { + return DefaultBaseUrl; + } + + var trimmed = configured.Trim().TrimEnd('/'); + if (trimmed.Contains("pickup.dpd.pl", StringComparison.OrdinalIgnoreCase)) + { + return DefaultBaseUrl; + } + + // WidgetUrl may point at /widget — use origin only. + if (Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)) + { + return $"{uri.Scheme}://{uri.Host}"; + } + + return DefaultBaseUrl; + } + + private static ParcelPointDto MergeDetails( + ParcelPointDto point, + PudoDetails? details) + { + if (details is null) + { + return point; + } + + var hours = FormatHours(details); + var supportsCod = details.Services?.Any(s => + s.Service.Contains("pobraniem", StringComparison.OrdinalIgnoreCase) || + s.Service.Contains("COD", StringComparison.OrdinalIgnoreCase)) ?? false; + + return point with + { + Name = string.IsNullOrWhiteSpace(details.PointName) + ? point.Name + : details.PointName, + Address = details.Address ?? point.Address, + City = details.City ?? point.City, + PostalCode = FormatPostal(details.PostCode) ?? point.PostalCode, + OpeningHours = hours, + SupportsCod = supportsCod, + }; + } + + private static string? FormatHours(PudoDetails details) + { + var slots = (details.OpeningHours1 ?? []) + .Concat(details.OpeningHours2 ?? []) + .Where(h => !string.IsNullOrWhiteSpace(h.Day) && + !string.IsNullOrWhiteSpace(h.Hours)) + .Select(h => $"{h.Day} {h.Hours}") + .ToList(); + + return slots.Count == 0 ? null : string.Join(", ", slots); + } + + private static string? FormatPostal(string? postCode) + { + if (string.IsNullOrWhiteSpace(postCode)) + { + return null; + } + + var digits = new string(postCode.Where(char.IsDigit).ToArray()); + return digits.Length == 5 + ? $"{digits[..2]}-{digits[2..]}" + : postCode.Trim(); + } + + private static ParcelPointDto WithDistance( + PudoPoint point, + double? latitude, + double? longitude) + { + double? distance = null; + if (latitude is not null && longitude is not null) + { + distance = Math.Round( + HaversineKm( + latitude.Value, + longitude.Value, + point.Lat, + point.Lng), + 1); + } + + return new ParcelPointDto( + point.Id, + point.Name, + string.Empty, + string.Empty, + string.Empty, + point.Lat, + point.Lng, + null, + false, + distance); + } + + private async Task<(double Lat, double Lng)?> GeocodeAsync( + string query, + CancellationToken cancellationToken) + { + try + { + var client = httpClientFactory.CreateClient(nameof(DpdPudoClient)); + var url = + "https://nominatim.openstreetmap.org/search" + + $"?q={Uri.EscapeDataString(query)}" + + "&countrycodes=pl&format=json&limit=1"; + + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.TryAddWithoutValidation( + "User-Agent", + "iLED-Shop/1.0 (shipping-pickup)"); + + using var response = await client.SendAsync(request, cancellationToken); + if (!response.IsSuccessStatusCode) + { + return null; + } + + var hits = await response.Content + .ReadFromJsonAsync>(JsonOptions, cancellationToken); + var hit = hits?.FirstOrDefault(); + if (hit is null || + !double.TryParse( + hit.Lat, + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out var lat) || + !double.TryParse( + hit.Lon, + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out var lon)) + { + return null; + } + + return (lat, lon); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to geocode DPD search query"); + return null; + } + } + + private static bool LooksLikePointId(string value) => + Regex.IsMatch(value, @"^PL\d+$", RegexOptions.IgnoreCase); + + private static double HaversineKm( + double lat1, + double lon1, + double lat2, + double lon2) + { + const double earthRadiusKm = 6371; + var dLat = (lat2 - lat1) * Math.PI / 180; + var dLon = (lon2 - lon1) * Math.PI / 180; + var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(lat1 * Math.PI / 180) * + Math.Cos(lat2 * Math.PI / 180) * + Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + return earthRadiusKm * 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); + } + + private sealed record CacheEntry( + IReadOnlyList Points, + DateTimeOffset ExpiresAt); + + private sealed class PudoPoint + { + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public double Lat { get; set; } + public double Lng { get; set; } + public bool Enabled { get; set; } = true; + public PudoDetails? Details { get; set; } + } + + private sealed class PudoDetails + { + [JsonPropertyName("point_name")] + public string? PointName { get; set; } + + public string? Address { get; set; } + public string? City { get; set; } + + [JsonPropertyName("post_code")] + public string? PostCode { get; set; } + + public List? Services { get; set; } + + [JsonPropertyName("opening_hours_1")] + public List? OpeningHours1 { get; set; } + + [JsonPropertyName("opening_hours_2")] + public List? OpeningHours2 { get; set; } + } + + private sealed class PudoService + { + public string Service { get; set; } = string.Empty; + } + + private sealed class PudoHours + { + public string Day { get; set; } = string.Empty; + public string Hours { get; set; } = string.Empty; + } + + private sealed class NominatimHit + { + public string Lat { get; set; } = string.Empty; + public string Lon { get; set; } = string.Empty; + } +} diff --git a/src/backend/src/iLED.Infrastructure/Services/ExternalServices.cs b/src/backend/src/iLED.Infrastructure/Services/ExternalServices.cs new file mode 100644 index 0000000..9796e25 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/ExternalServices.cs @@ -0,0 +1,469 @@ +using System.Globalization; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using iLED.Application.Common.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace iLED.Infrastructure.Services; + +public sealed class PayUPaymentService( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + ILogger logger) : IPaymentService +{ + public async Task InitiatePaymentAsync( + PaymentInitiationRequest request, + CancellationToken cancellationToken = default) + { + var posId = configuration["PayU:PosId"]; + var clientId = configuration["PayU:ClientId"]; + var clientSecret = configuration["PayU:ClientSecret"]; + var oauthUrl = configuration["PayU:OAuthUrl"]; + var ordersUrl = configuration["PayU:OrdersUrl"]; + var useSandbox = configuration.GetValue("PayU:UseSandbox", true); + var hasCredentials = + !string.IsNullOrWhiteSpace(posId) && + !string.IsNullOrWhiteSpace(clientId) && + !string.IsNullOrWhiteSpace(clientSecret) && + !string.IsNullOrWhiteSpace(oauthUrl) && + !string.IsNullOrWhiteSpace(ordersUrl); + + // No POS credentials → local mock page (dev fallback only). + if (!hasCredentials) + { + if (!useSandbox) + { + return new PaymentInitiationResult( + false, + null, + null, + "Configure PayU:PosId, ClientId, ClientSecret and URLs."); + } + + var externalId = $"PAYU-{Guid.NewGuid():N}"; + logger.LogWarning( + "PayU credentials missing — using in-app sandbox page for {OrderNumber}", + request.OrderNumber); + + return new PaymentInitiationResult( + true, + externalId, + $"/checkout/payu-sandbox/{request.OrderId}" + + $"?extId={Uri.EscapeDataString(externalId)}", + null); + } + + try + { + var token = await GetAccessTokenAsync( + oauthUrl!, + clientId!, + clientSecret!, + cancellationToken); + + var totalAmount = ((int)Math.Round( + request.Amount * 100m, + MidpointRounding.AwayFromZero)).ToString(CultureInfo.InvariantCulture); + + var body = new + { + notifyUrl = request.NotifyUrl, + continueUrl = request.ContinueUrl, + customerIp = "127.0.0.1", + merchantPosId = posId, + description = $"Order {request.OrderNumber}", + currencyCode = "PLN", + totalAmount, + extOrderId = request.OrderId.ToString(), + buyer = new + { + email = request.CustomerEmail, + firstName = string.IsNullOrWhiteSpace(request.CustomerFirstName) + ? "Customer" + : request.CustomerFirstName, + lastName = string.IsNullOrWhiteSpace(request.CustomerLastName) + ? "iLED" + : request.CustomerLastName, + language = "pl", + }, + products = new[] + { + new + { + name = $"Order {request.OrderNumber}", + unitPrice = totalAmount, + quantity = "1", + }, + }, + }; + + var client = httpClientFactory.CreateClient(nameof(PayUPaymentService)); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, ordersUrl); + httpRequest.Headers.Authorization = + new AuthenticationHeaderValue("Bearer", token); + httpRequest.Content = new StringContent( + JsonSerializer.Serialize(body), + Encoding.UTF8, + "application/json"); + + using var response = await client.SendAsync( + httpRequest, + cancellationToken); + var responseBody = await response.Content.ReadAsStringAsync( + cancellationToken); + + var isRedirect = + response.StatusCode is System.Net.HttpStatusCode.Found + or System.Net.HttpStatusCode.RedirectMethod + or System.Net.HttpStatusCode.RedirectKeepVerb + or System.Net.HttpStatusCode.PermanentRedirect; + + if (!response.IsSuccessStatusCode && !isRedirect) + { + logger.LogError( + "PayU order create failed: {Status} {Body}", + (int)response.StatusCode, + responseBody); + return new PaymentInitiationResult( + false, + null, + null, + "PayU payment initiation failed."); + } + + string? redirectUri = response.Headers.Location?.ToString(); + string? payuOrderId = null; + + if (!string.IsNullOrWhiteSpace(responseBody)) + { + try + { + using var doc = JsonDocument.Parse(responseBody); + var root = doc.RootElement; + if (root.TryGetProperty("redirectUri", out var r)) + { + redirectUri ??= r.GetString(); + } + + if (root.TryGetProperty("orderId", out var o)) + { + payuOrderId = o.GetString(); + } + } + catch (JsonException) + { + // 302 responses may have an empty/non-JSON body. + } + } + + if (string.IsNullOrWhiteSpace(redirectUri)) + { + logger.LogError( + "PayU did not return redirectUri. Status={Status} Body={Body}", + (int)response.StatusCode, + responseBody); + return new PaymentInitiationResult( + false, + null, + null, + "PayU did not return redirectUri."); + } + + logger.LogInformation( + "PayU payment created for {OrderNumber} → {Redirect}", + request.OrderNumber, + redirectUri); + + return new PaymentInitiationResult( + true, + payuOrderId ?? request.OrderId.ToString(), + redirectUri, + null); + } + catch (Exception ex) + { + logger.LogError(ex, "PayU payment failed for {OrderNumber}", + request.OrderNumber); + return new PaymentInitiationResult( + false, + null, + null, + "PayU payment initiation failed."); + } + } + + public async Task GetPaymentStatusAsync( + string externalId, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(externalId) || + externalId.StartsWith("PAYU-", StringComparison.OrdinalIgnoreCase)) + { + return new PaymentStatusResult( + false, + externalId, + null, + false, + false, + "No PayU order id to sync."); + } + + var clientId = configuration["PayU:ClientId"]; + var clientSecret = configuration["PayU:ClientSecret"]; + var oauthUrl = configuration["PayU:OAuthUrl"]; + var ordersUrl = configuration["PayU:OrdersUrl"]; + + if (string.IsNullOrWhiteSpace(clientId) || + string.IsNullOrWhiteSpace(clientSecret) || + string.IsNullOrWhiteSpace(oauthUrl) || + string.IsNullOrWhiteSpace(ordersUrl)) + { + return new PaymentStatusResult( + false, + externalId, + null, + false, + false, + "PayU is not configured."); + } + + try + { + var token = await GetAccessTokenAsync( + oauthUrl, + clientId, + clientSecret, + cancellationToken); + + var client = httpClientFactory.CreateClient(nameof(PayUPaymentService)); + var url = $"{ordersUrl.TrimEnd('/')}/{Uri.EscapeDataString(externalId)}"; + using var httpRequest = new HttpRequestMessage(HttpMethod.Get, url); + httpRequest.Headers.Authorization = + new AuthenticationHeaderValue("Bearer", token); + + using var response = await client.SendAsync( + httpRequest, + cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); + + if (!response.IsSuccessStatusCode) + { + logger.LogError( + "PayU status fetch failed: {Status} {Body}", + (int)response.StatusCode, + body); + return new PaymentStatusResult( + false, + externalId, + null, + false, + false, + "PayU status fetch failed."); + } + + using var doc = JsonDocument.Parse(body); + var root = doc.RootElement; + string? status = null; + string? payuOrderId = externalId; + + if (root.TryGetProperty("orders", out var orders) && + orders.ValueKind == JsonValueKind.Array && + orders.GetArrayLength() > 0) + { + var order = orders[0]; + status = order.TryGetProperty("status", out var s) + ? s.GetString() + : null; + if (order.TryGetProperty("orderId", out var oid)) + { + payuOrderId = oid.GetString() ?? externalId; + } + } + + var completed = string.Equals( + status, + "COMPLETED", + StringComparison.OrdinalIgnoreCase); + var canceled = string.Equals( + status, + "CANCELED", + StringComparison.OrdinalIgnoreCase); + + logger.LogInformation( + "PayU status for {ExternalId}: {Status}", + payuOrderId, + status); + + return new PaymentStatusResult( + true, + payuOrderId, + status, + completed, + canceled, + null); + } + catch (Exception ex) + { + logger.LogError(ex, "PayU status fetch error for {ExternalId}", externalId); + return new PaymentStatusResult( + false, + externalId, + null, + false, + false, + "PayU status fetch failed."); + } + } + + public Task HandleWebhookAsync( + string payload, + string signature, + CancellationToken cancellationToken = default) + { + try + { + var secondKey = configuration["PayU:SecondKey"]; + if (!string.IsNullOrWhiteSpace(secondKey) && + !VerifySignature(payload, signature, secondKey)) + { + logger.LogWarning("PayU webhook signature mismatch"); + return Task.FromResult(new PaymentWebhookResult( + false, null, null, false, "Invalid signature")); + } + + using var doc = JsonDocument.Parse(payload); + var root = doc.RootElement; + var order = root.TryGetProperty("order", out var orderEl) + ? orderEl + : root; + + var extId = order.TryGetProperty("extOrderId", out var e) + ? e.GetString() + : root.TryGetProperty("extOrderId", out var e2) + ? e2.GetString() + : null; + var payuOrderId = order.TryGetProperty("orderId", out var o) + ? o.GetString() + : root.TryGetProperty("orderId", out var o2) + ? o2.GetString() + : null; + var status = order.TryGetProperty("status", out var s) + ? s.GetString() + : root.TryGetProperty("status", out var s2) + ? s2.GetString() + : null; + + Guid? orderId = Guid.TryParse(extId, out var id) ? id : null; + var completed = string.Equals( + status, + "COMPLETED", + StringComparison.OrdinalIgnoreCase); + + return Task.FromResult(new PaymentWebhookResult( + true, + orderId, + payuOrderId ?? extId, + completed, + null)); + } + catch (Exception ex) + { + logger.LogError(ex, "PayU webhook parse error"); + return Task.FromResult(new PaymentWebhookResult( + false, null, null, false, ex.Message)); + } + } + + private async Task GetAccessTokenAsync( + string oauthUrl, + string clientId, + string clientSecret, + CancellationToken cancellationToken) + { + var client = httpClientFactory.CreateClient(nameof(PayUPaymentService)); + using var content = new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "client_credentials", + ["client_id"] = clientId, + ["client_secret"] = clientSecret, + }); + + using var response = await client.PostAsync( + oauthUrl, + content, + cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + logger.LogError( + "PayU OAuth failed: {Status} {Body}", + (int)response.StatusCode, + body); + throw new HttpRequestException( + $"PayU OAuth failed: {(int)response.StatusCode}"); + } + + using var doc = JsonDocument.Parse(body); + var token = doc.RootElement.GetProperty("access_token").GetString(); + if (string.IsNullOrWhiteSpace(token)) + { + throw new InvalidOperationException("PayU OAuth token missing."); + } + + return token; + } + + private static bool VerifySignature( + string payload, + string signatureHeader, + string secondKey) + { + if (string.IsNullOrWhiteSpace(signatureHeader)) + { + return false; + } + + var expected = Convert.ToHexString( + MD5.HashData(Encoding.UTF8.GetBytes(payload + secondKey))) + .ToLowerInvariant(); + + foreach (var part in signatureHeader.Split( + ',', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var kv = part.Split('=', 2); + if (kv.Length == 2 && + kv[0].Equals("signature", StringComparison.OrdinalIgnoreCase) && + string.Equals(kv[1], expected, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return string.Equals( + signatureHeader.Trim(), + expected, + StringComparison.OrdinalIgnoreCase); + } +} + +public sealed class ConsoleEmailService(ILogger logger) + : IEmailService +{ + public Task SendAsync( + string to, + string subject, + string htmlBody, + CancellationToken cancellationToken = default) + { + logger.LogInformation( + "EMAIL to {To}: {Subject}\n{Body}", + to, + subject, + htmlBody); + return Task.CompletedTask; + } +} diff --git a/src/backend/src/iLED.Infrastructure/Services/OrderService.cs b/src/backend/src/iLED.Infrastructure/Services/OrderService.cs new file mode 100644 index 0000000..4c72306 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/OrderService.cs @@ -0,0 +1,298 @@ +using iLED.Application.Cart.Commands; +using iLED.Application.Common; +using iLED.Application.Common.Interfaces; +using iLED.Application.Marketing; +using iLED.Application.Orders.Commands; +using iLED.Domain.Enums; +using iLED.Domain.Marketing; +using iLED.Domain.Orders; +using Microsoft.EntityFrameworkCore; +using Persistence = iLED.Infrastructure.Persistence; + +namespace iLED.Infrastructure.Services; + +public sealed class OrderService( + Persistence.ApplicationDbContext db, + ICartService cartService, + ICurrentUserService currentUser, + IPromotionRewardService promotionRewards) : IOrderService +{ + private const decimal VatRate = 0.23m; + private const decimal CourierDeliveryCost = 15.99m; + private const decimal ParcelLockerDeliveryCost = 12.99m; + private const decimal CashOnDeliveryFee = 5m; + private const decimal FreeDeliveryThreshold = 499.99m; + + public async Task CreateOrderAsync( + CreateOrderCommand command, + CancellationToken cancellationToken) + { + var cart = await cartService.GetCartAsync(cancellationToken); + if (cart.Items.Count == 0) + { + throw new Application.Common.Exceptions.ValidationException( + "Koszyk jest pusty."); + } + + var subtotal = cart.Subtotal; + var discountAmount = 0m; + var freeShipping = false; + DiscountCode? appliedCode = null; + + if (!string.IsNullOrWhiteSpace(command.DiscountCode)) + { + var code = await db.DiscountCodes + .Include(c => c.DiscountRule) + .FirstOrDefaultAsync( + c => c.Code == command.DiscountCode.ToUpperInvariant(), + cancellationToken); + + var email = command.CustomerEmail.Trim().ToLowerInvariant(); + var ruleId = code?.DiscountRuleId ?? Guid.Empty; + var ruleCount = code is null + ? 0 + : await db.DiscountRedemptions.CountAsync( + r => r.DiscountRuleId == ruleId, + cancellationToken); + var customerCount = code is null || string.IsNullOrEmpty(email) + ? 0 + : await db.DiscountRedemptions.CountAsync( + r => r.DiscountRuleId == ruleId && r.CustomerEmail == email, + cancellationToken); + + var eval = PromotionEvaluation.Evaluate( + code, + subtotal, + email, + currentUser.UserId, + ruleCount, + customerCount, + DateTime.UtcNow); + + if (!eval.IsValid) + { + throw new Application.Common.Exceptions.ValidationException( + eval.ErrorCode ?? PromotionErrorCodes.Invalid, + eval.ErrorCode ?? PromotionErrorCodes.Invalid, + eval.MinOrderAmount); + } + + discountAmount = eval.DiscountAmount; + freeShipping = eval.FreeShipping; + appliedCode = eval.Code; + appliedCode!.UsageCount++; + } + + ValidateShipping(command); + + var afterDiscount = Math.Max(0, subtotal - discountAmount); + var delivery = CalculateDeliveryCost( + command.ShippingCarrier, + command.ShippingServiceType, + afterDiscount, + freeShipping); + var codFee = command.PaymentMethod == PaymentMethod.CashOnDelivery + ? CashOnDeliveryFee + : 0m; + var totalBeforeVat = afterDiscount + delivery + codFee; + var vat = Math.Round(totalBeforeVat * VatRate / (1 + VatRate), 2); + var total = afterDiscount + delivery + codFee; + + var order = new Order + { + OrderNumber = $"ILED-{DateTime.UtcNow:yyyyMMdd}-{Random.Shared.Next(1000, 9999)}", + UserId = currentUser.UserId, + CustomerEmail = command.CustomerEmail, + Locale = LocaleHelper.Normalize(command.Locale), + CustomerPhone = command.CustomerPhone, + CustomerFirstName = command.CustomerFirstName, + CustomerLastName = command.CustomerLastName, + ShippingStreet = command.ShippingStreet, + ShippingCity = command.ShippingCity, + ShippingPostalCode = command.ShippingPostalCode, + ShippingCountry = command.ShippingCountry, + BillingStreet = command.BillingStreet, + BillingCity = command.BillingCity, + BillingPostalCode = command.BillingPostalCode, + BillingCountry = command.BillingCountry, + ShippingCarrier = command.ShippingCarrier, + ShippingServiceType = command.ShippingServiceType, + ParcelPointId = command.ParcelPointId, + ParcelPointName = command.ParcelPointName, + ParcelPointAddress = command.ParcelPointAddress, + PaymentMethod = command.PaymentMethod, + CustomerNotes = command.CustomerNotes, + Status = OrderStatus.PendingPayment, + Subtotal = subtotal, + DeliveryCost = delivery, + DiscountAmount = discountAmount, + VatAmount = vat, + Total = total, + DiscountCodeId = appliedCode?.Id, + Lines = cart.Items.Select(i => new OrderLine + { + ProductId = i.ProductId, + ProductVariantId = i.VariantId, + ProductName = i.ProductName, + VariantColor = i.VariantColor, + Sku = i.VariantId.ToString(), + ImageUrl = i.ImageUrl, + UnitPrice = i.UnitPrice, + Quantity = i.Quantity, + }).ToList(), + }; + + db.Orders.Add(order); + + if (appliedCode is not null) + { + db.DiscountRedemptions.Add(new DiscountRedemption + { + OrderId = order.Id, + DiscountCodeId = appliedCode.Id, + DiscountRuleId = appliedCode.DiscountRuleId, + CustomerEmail = command.CustomerEmail.Trim().ToLowerInvariant(), + UserId = currentUser.UserId, + DiscountAmount = discountAmount, + }); + } + + var cartEntity = await db.Carts + .Include(c => c.Items) + .FirstOrDefaultAsync(c => c.Id == cart.Id, cancellationToken); + + if (cartEntity is not null) + { + db.CartItems.RemoveRange(cartEntity.Items); + } + + await db.SaveChangesAsync(cancellationToken); + return MapOrder(order); + } + + public async Task GetOrderAsync( + Guid orderId, + CancellationToken cancellationToken) + { + var order = await db.Orders + .AsNoTracking() + .Include(o => o.Lines) + .Include(o => o.Payment) + .FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken) + ?? throw new Application.Common.Exceptions.NotFoundException( + "Zamówienie nie znalezione."); + + if (currentUser.UserId is not null && + order.UserId != currentUser.UserId && + !currentUser.IsAdmin) + { + throw new Application.Common.Exceptions.ValidationException( + "Brak dostępu."); + } + + return MapOrder(order); + } + + public async Task> GetUserOrdersAsync( + CancellationToken cancellationToken) + { + if (currentUser.UserId is null) + { + return []; + } + + return await db.Orders + .AsNoTracking() + .Where(o => o.UserId == currentUser.UserId) + .OrderByDescending(o => o.CreatedAt) + .Select(o => new OrderSummaryDto( + o.Id, + o.OrderNumber, + o.Status, + o.Total, + o.CreatedAt, + o.Lines.Count)) + .ToListAsync(cancellationToken); + } + + public async Task UpdateOrderStatusAsync( + Guid orderId, + OrderStatus status, + CancellationToken cancellationToken) + { + var order = await db.Orders + .FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken) + ?? throw new Application.Common.Exceptions.NotFoundException( + "Zamówienie nie znalezione."); + + var becamePaid = + status == OrderStatus.Paid && order.Status != OrderStatus.Paid; + + order.Status = status; + order.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + + if (becamePaid) + { + await promotionRewards.TryIssueRewardsForPaidOrderAsync( + order, + cancellationToken); + } + } + + private static void ValidateShipping(CreateOrderCommand command) + { + if (command.ShippingServiceType == ShippingServiceType.ParcelLocker && + string.IsNullOrWhiteSpace(command.ParcelPointId)) + { + throw new Application.Common.Exceptions.ValidationException( + "Select a delivery point."); + } + } + + private static decimal CalculateDeliveryCost( + ShippingCarrier carrier, + ShippingServiceType serviceType, + decimal afterDiscount, + bool freeShippingFromDiscount) + { + if (freeShippingFromDiscount || afterDiscount >= FreeDeliveryThreshold) + { + return 0m; + } + + if (serviceType == ShippingServiceType.ParcelLocker) + { + return ParcelLockerDeliveryCost; + } + + return CourierDeliveryCost; + } + + private static OrderDto MapOrder(Order order) => new( + order.Id, + order.OrderNumber, + order.Status, + order.Subtotal, + order.DeliveryCost, + order.DiscountAmount, + order.VatAmount, + order.Total, + order.CustomerEmail, + order.CustomerPhone, + order.CustomerFirstName, + order.CustomerLastName, + order.ShippingCarrier, + order.ShippingServiceType, + order.ParcelPointId, + order.ParcelPointName, + order.ParcelPointAddress, + order.PaymentMethod, + order.Lines.Select(l => new OrderLineDto( + l.ProductName, + l.VariantColor, + l.UnitPrice, + l.Quantity, + l.LineTotal)).ToList(), + order.Payment?.RedirectUrl); +} diff --git a/src/backend/src/iLED.Infrastructure/Services/PayUPaymentSyncBackgroundService.cs b/src/backend/src/iLED.Infrastructure/Services/PayUPaymentSyncBackgroundService.cs new file mode 100644 index 0000000..2008ac0 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/PayUPaymentSyncBackgroundService.cs @@ -0,0 +1,120 @@ +using iLED.Application.Payments.Commands; +using iLED.Domain.Enums; +using iLED.Infrastructure.Persistence; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace iLED.Infrastructure.Services; + +/// +/// Periodically pulls PayU status for pending payments. +/// Covers missed webhooks and local/dev where notifyUrl is unreachable. +/// +public sealed class PayUPaymentSyncBackgroundService( + IServiceScopeFactory scopeFactory, + IConfiguration configuration, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var enabled = configuration.GetValue("PayU:StatusPollEnabled", true); + if (!enabled) + { + logger.LogInformation("PayU status poller disabled."); + return; + } + + var intervalSeconds = configuration.GetValue( + "PayU:StatusPollIntervalSeconds", + 60); + if (intervalSeconds < 15) + { + intervalSeconds = 15; + } + + // Let the API finish starting before the first poll. + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + + logger.LogInformation( + "PayU status poller started (every {Seconds}s).", + intervalSeconds); + + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(intervalSeconds)); + do + { + try + { + await SyncPendingAsync(stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + logger.LogError(ex, "PayU status poller tick failed."); + } + } + while (await timer.WaitForNextTickAsync(stoppingToken)); + } + + private async Task SyncPendingAsync(CancellationToken cancellationToken) + { + using var scope = scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + var orderIds = await ( + from p in db.Payments.AsNoTracking() + join o in db.Orders.AsNoTracking() on p.OrderId equals o.Id + where p.Status == PaymentStatus.Pending + && p.Provider == "PayU" + && p.ExternalId != "" + && !p.ExternalId.StartsWith("PAYU-") + && o.Status == OrderStatus.PendingPayment + && o.PaymentMethod == PaymentMethod.PayU + orderby p.UpdatedAt + select p.OrderId) + .Take(40) + .ToListAsync(cancellationToken); + + if (orderIds.Count == 0) + { + return; + } + + logger.LogInformation( + "PayU poller syncing {Count} pending payment(s).", + orderIds.Count); + + var paid = 0; + foreach (var orderId in orderIds) + { + cancellationToken.ThrowIfCancellationRequested(); + var result = await mediator.Send( + new SyncPaymentStatusCommand(orderId), + cancellationToken); + + if (result.Success && + string.Equals( + result.OrderStatus, + OrderStatus.Paid.ToString(), + StringComparison.OrdinalIgnoreCase)) + { + paid++; + } + } + + if (paid > 0) + { + logger.LogInformation( + "PayU poller marked {Paid}/{Total} order(s) as paid.", + paid, + orderIds.Count); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Services/ProductImageStorage.cs b/src/backend/src/iLED.Infrastructure/Services/ProductImageStorage.cs new file mode 100644 index 0000000..0fcad5e --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/ProductImageStorage.cs @@ -0,0 +1,68 @@ +using iLED.Application.Common.Interfaces; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Options; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Processing; + +namespace iLED.Infrastructure.Services; + +public sealed class ProductImageStorage( + IWebHostEnvironment environment, + IOptions options) + : IProductImageStorage +{ + private static readonly HashSet AllowedExtensions = + [ + ".jpg", + ".jpeg", + ".png", + ".gif", + ".webp", + ".bmp", + ]; + + private readonly ProductImageStorageOptions _options = options.Value; + + public async Task SaveAsWebpAsync( + Stream input, + string originalFileName, + CancellationToken cancellationToken) + { + var extension = Path.GetExtension(originalFileName).ToLowerInvariant(); + if (!AllowedExtensions.Contains(extension)) + { + throw new Application.Common.Exceptions.ValidationException( + "Nieobsługiwany format pliku. Dozwolone: JPG, PNG, GIF, WebP, BMP."); + } + + var webRoot = environment.WebRootPath + ?? Path.Combine(environment.ContentRootPath, "wwwroot"); + var uploadDir = Path.Combine(webRoot, "uploads", "products"); + Directory.CreateDirectory(uploadDir); + + var fileName = $"{Guid.NewGuid():N}.webp"; + var fullPath = Path.Combine(uploadDir, fileName); + + using var image = await Image.LoadAsync(input, cancellationToken); + image.Mutate(x => x.AutoOrient()); + + if (image.Width > _options.MaxWidth) + { + image.Mutate(x => x.Resize(new ResizeOptions + { + Size = new Size(_options.MaxWidth, 0), + Mode = ResizeMode.Max, + })); + } + + var encoder = new WebpEncoder + { + Quality = _options.WebpQuality, + }; + + await image.SaveAsync(fullPath, encoder, cancellationToken); + + return $"{_options.PublicPathPrefix}/{fileName}"; + } +} diff --git a/src/backend/src/iLED.Infrastructure/Services/ProductImageStorageOptions.cs b/src/backend/src/iLED.Infrastructure/Services/ProductImageStorageOptions.cs new file mode 100644 index 0000000..9625059 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/ProductImageStorageOptions.cs @@ -0,0 +1,14 @@ +namespace iLED.Infrastructure.Services; + +public sealed class ProductImageStorageOptions +{ + public const string SectionName = "ProductImages"; + + public string PublicPathPrefix { get; set; } = "/uploads/products"; + + public int MaxWidth { get; set; } = 2000; + + public int WebpQuality { get; set; } = 85; + + public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; +} diff --git a/src/backend/src/iLED.Infrastructure/Services/PromotionRewardService.cs b/src/backend/src/iLED.Infrastructure/Services/PromotionRewardService.cs new file mode 100644 index 0000000..ceda3bb --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/PromotionRewardService.cs @@ -0,0 +1,154 @@ +using iLED.Application.Common; +using iLED.Application.Common.Interfaces; +using iLED.Domain.Enums; +using iLED.Domain.Marketing; +using iLED.Domain.Orders; +using iLED.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace iLED.Infrastructure.Services; + +public interface IPromotionRewardService +{ + Task TryIssueRewardsForPaidOrderAsync( + Order order, + CancellationToken cancellationToken); +} + +/// +/// Issues one-time reward codes when a paid order meets promotion thresholds. +/// +public sealed class PromotionRewardService( + ApplicationDbContext db, + IEmailService emailService, + ILogger logger) : IPromotionRewardService +{ + public async Task TryIssueRewardsForPaidOrderAsync( + Order order, + CancellationToken cancellationToken) + { + var promotions = await db.DiscountRules + .Where(r => + r.IsActive && + r.IssueRewardOnPaidOrder && + r.RewardMinPaidOrderTotal != null && + order.Total >= r.RewardMinPaidOrderTotal) + .ToListAsync(cancellationToken); + + foreach (var promo in promotions) + { + var alreadyIssued = await db.DiscountCodes.AnyAsync( + c => c.IssuedForOrderId == order.Id && + c.DiscountRuleId == promo.Id, + cancellationToken); + if (alreadyIssued) + { + continue; + } + + var prefix = string.IsNullOrWhiteSpace(promo.RewardCodePrefix) + ? "THANKS" + : promo.RewardCodePrefix.Trim().ToUpperInvariant(); + string codeValue; + do + { + codeValue = $"{prefix}{Random.Shared.Next(100000, 999999)}"; + } + while (await db.DiscountCodes.AnyAsync( + c => c.Code == codeValue, + cancellationToken)); + + var validUntil = promo.RewardValidDays is > 0 + ? DateTime.UtcNow.AddDays(promo.RewardValidDays.Value) + : (DateTime?)null; + + db.DiscountCodes.Add(new DiscountCode + { + DiscountRuleId = promo.Id, + Code = codeValue, + MaxUsages = 1, + Source = DiscountCodeSource.Reward, + IssuedToEmail = order.CustomerEmail.Trim().ToLowerInvariant(), + IssuedForOrderId = order.Id, + ValidUntil = validUntil, + }); + + await db.SaveChangesAsync(cancellationToken); + + var locale = LocaleHelper.Normalize(order.Locale); + var (subject, body) = BuildEmail( + locale, + order.OrderNumber, + codeValue, + validUntil); + + try + { + await emailService.SendAsync( + order.CustomerEmail, + subject, + body.Replace("\n", "
"), + cancellationToken); + } + catch (Exception ex) + { + logger.LogError( + ex, + "Failed to email reward code {Code} for order {OrderId}", + codeValue, + order.Id); + } + + logger.LogInformation( + "Issued reward code {Code} for order {OrderNumber}", + codeValue, + order.OrderNumber); + } + } + + private static (string Subject, string Body) BuildEmail( + string locale, + string orderNumber, + string code, + DateTime? validUntil) + { + var expiry = validUntil.HasValue + ? locale switch + { + "en" => $"Valid until: {validUntil:yyyy-MM-dd} UTC\n", + "de" => $"Gültig bis: {validUntil:yyyy-MM-dd} UTC\n", + "cs" => $"Platí do: {validUntil:yyyy-MM-dd} UTC\n", + _ => $"Ważny do: {validUntil:yyyy-MM-dd} UTC\n", + } + : string.Empty; + + return locale switch + { + "en" => ( + "Your iLED discount code", + $"Thank you for order {orderNumber}.\n\n" + + $"Your discount code: {code}\n" + + expiry + + "This code is one-time and tied to your email."), + "de" => ( + "Ihr iLED-Rabattcode", + $"Danke für Bestellung {orderNumber}.\n\n" + + $"Ihr Rabattcode: {code}\n" + + expiry + + "Der Code ist einmalig und an Ihre E-Mail gebunden."), + "cs" => ( + "Váš slevový kód iLED", + $"Děkujeme za objednávku {orderNumber}.\n\n" + + $"Váš slevový kód: {code}\n" + + expiry + + "Kód je jednorázový a vázaný na váš e-mail."), + _ => ( + "Twój kod rabatowy iLED", + $"Dziękujemy za zamówienie {orderNumber}.\n\n" + + $"Twój kod rabatowy: {code}\n" + + expiry + + "Kod jest jednorazowy i przypisany do Twojego e-maila."), + }; + } +} diff --git a/src/backend/src/iLED.Infrastructure/Services/ShippingCarrierClients.cs b/src/backend/src/iLED.Infrastructure/Services/ShippingCarrierClients.cs new file mode 100644 index 0000000..9919a28 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/ShippingCarrierClients.cs @@ -0,0 +1,376 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Xml.Linq; +using iLED.Application.Common.Interfaces; +using iLED.Domain.Enums; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace iLED.Infrastructure.Services; + +public sealed class ShippingCarrierClients( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + ILogger logger, + ShippingLabelStorage labelStorage) +{ + public Task CreateAsync( + ShipmentRequest request, + CancellationToken cancellationToken) => + request.Carrier switch + { + ShippingCarrier.InPost => CreateInPostAsync(request, cancellationToken), + ShippingCarrier.Dhl => CreateDhlAsync(request, cancellationToken), + ShippingCarrier.Dpd => CreateDpdAsync(request, cancellationToken), + _ => Task.FromResult(new ShipmentLabelResult( + false, null, null, null, $"Unsupported carrier {request.Carrier}")), + }; + + private async Task CreateInPostAsync( + ShipmentRequest request, + CancellationToken cancellationToken) + { + var token = configuration["Shipping:InPost:ShipXToken"]; + var orgId = configuration["Shipping:InPost:OrganizationId"]; + var baseUrl = configuration["Shipping:InPost:ShipXBaseUrl"] + ?? "https://api-shipx-pl.easypack24.net"; + + if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(orgId)) + { + return Fail("Configure Shipping:InPost:ShipXToken and OrganizationId."); + } + + var isLocker = request.ServiceType == ShippingServiceType.ParcelLocker; + var service = isLocker ? "inpost_locker_standard" : "inpost_courier_standard"; + + var payload = new Dictionary + { + ["receiver"] = new Dictionary + { + ["name"] = $"{request.CustomerFirstName} {request.CustomerLastName}".Trim(), + ["email"] = request.CustomerEmail, + ["phone"] = request.CustomerPhone, + ["address"] = isLocker + ? null + : new + { + street = request.ShippingStreet, + city = request.ShippingCity, + post_code = request.ShippingPostalCode, + country_code = string.IsNullOrWhiteSpace(request.ShippingCountry) + ? "PL" + : request.ShippingCountry, + }, + }, + ["parcels"] = new[] + { + new + { + dimensions = new { length = "300", width = "200", height = "150", unit = "mm" }, + weight = new { amount = "1", unit = "kg" }, + }, + }, + ["service"] = service, + ["reference"] = request.OrderNumber, + ["custom_attributes"] = isLocker + ? new { target_point = request.ParcelPointId } + : null, + }; + + if (request.CodAmount > 0) + { + payload["cod"] = new + { + amount = request.CodAmount, + currency = "PLN", + }; + } + + var client = httpClientFactory.CreateClient(nameof(ShippingCarrierClients)); + using var httpRequest = new HttpRequestMessage( + HttpMethod.Post, + $"{baseUrl.TrimEnd('/')}/v1/organizations/{orgId}/shipments"); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + httpRequest.Content = new StringContent( + JsonSerializer.Serialize(payload), + Encoding.UTF8, + "application/json"); + + using var response = await client.SendAsync(httpRequest, cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + logger.LogError("InPost ShipX create failed: {Status} {Body}", + (int)response.StatusCode, body); + return Fail("InPost ShipX shipment create failed."); + } + + using var doc = JsonDocument.Parse(body); + var id = doc.RootElement.TryGetProperty("id", out var idEl) + ? idEl.ToString() + : null; + var tracking = doc.RootElement.TryGetProperty("tracking_number", out var t) + ? t.GetString() + : id; + + string? labelPath = null; + if (!string.IsNullOrWhiteSpace(id)) + { + labelPath = await DownloadInPostLabelAsync( + client, + baseUrl, + token, + id, + request.OrderId, + cancellationToken); + } + + return new ShipmentLabelResult(true, tracking, id, labelPath, null); + } + + private async Task DownloadInPostLabelAsync( + HttpClient client, + string baseUrl, + string token, + string shipmentId, + Guid orderId, + CancellationToken cancellationToken) + { + try + { + using var req = new HttpRequestMessage( + HttpMethod.Get, + $"{baseUrl.TrimEnd('/')}/v1/shipments/{shipmentId}/label?format=pdf&type=A6"); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + using var res = await client.SendAsync(req, cancellationToken); + if (!res.IsSuccessStatusCode) + { + return null; + } + + var bytes = await res.Content.ReadAsByteArrayAsync(cancellationToken); + return await labelStorage.SaveLabelBytesAsync( + orderId, + bytes, + "pdf", + cancellationToken); + } + catch (Exception ex) + { + logger.LogWarning(ex, "InPost label download failed for {ShipmentId}", shipmentId); + return null; + } + } + + private async Task CreateDhlAsync( + ShipmentRequest request, + CancellationToken cancellationToken) + { + var apiKey = configuration["Shipping:Dhl:ApiKey"]; + var apiSecret = configuration["Shipping:Dhl:ApiSecret"]; + var baseUrl = configuration["Shipping:Dhl:ApiBaseUrl"]; + + if (string.IsNullOrWhiteSpace(apiKey) || + string.IsNullOrWhiteSpace(apiSecret) || + string.IsNullOrWhiteSpace(baseUrl)) + { + return Fail("Configure Shipping:Dhl:ApiKey, ApiSecret and ApiBaseUrl."); + } + + var payload = new + { + profile = "STANDARD_GRD", + shipments = new[] + { + new + { + productCode = request.ServiceType == ShippingServiceType.ParcelLocker + ? "PS" + : "N", + pickup = new + { + accounts = Array.Empty(), + }, + consignee = new + { + name = $"{request.CustomerFirstName} {request.CustomerLastName}".Trim(), + email = request.CustomerEmail, + phone = request.CustomerPhone, + address = new + { + addressLine1 = request.ServiceType == ShippingServiceType.ParcelLocker + ? request.ParcelPointAddress ?? request.ParcelPointName + : request.ShippingStreet, + cityName = request.ShippingCity, + postalCode = request.ShippingPostalCode, + countryCode = string.IsNullOrWhiteSpace(request.ShippingCountry) + ? "PL" + : request.ShippingCountry, + }, + lockerId = request.ParcelPointId, + }, + packages = new[] + { + new + { + weight = 1.0, + dimensions = new { length = 30, width = 20, height = 15 }, + }, + }, + customerReferences = new[] { request.OrderNumber }, + valueAddedServices = request.CodAmount > 0 + ? new[] + { + new + { + serviceCode = "COD", + value = request.CodAmount, + currency = "PLN", + }, + } + : null, + }, + }, + }; + + var client = httpClientFactory.CreateClient(nameof(ShippingCarrierClients)); + using var httpRequest = new HttpRequestMessage( + HttpMethod.Post, + $"{baseUrl.TrimEnd('/')}/orders"); + var credentials = Convert.ToBase64String( + Encoding.UTF8.GetBytes($"{apiKey}:{apiSecret}")); + httpRequest.Headers.Authorization = + new AuthenticationHeaderValue("Basic", credentials); + httpRequest.Content = new StringContent( + JsonSerializer.Serialize(payload), + Encoding.UTF8, + "application/json"); + + using var response = await client.SendAsync(httpRequest, cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + logger.LogError("DHL create failed: {Status} {Body}", + (int)response.StatusCode, body); + return Fail("DHL shipment create failed."); + } + + using var doc = JsonDocument.Parse(body); + string? tracking = null; + string? externalId = null; + if (doc.RootElement.TryGetProperty("shipments", out var shipments) && + shipments.ValueKind == JsonValueKind.Array && + shipments.GetArrayLength() > 0) + { + var first = shipments[0]; + externalId = first.TryGetProperty("shipmentTrackingNumber", out var st) + ? st.GetString() + : first.TryGetProperty("id", out var id) + ? id.ToString() + : null; + tracking = externalId; + } + + tracking ??= $"DHL-{request.OrderNumber}"; + return new ShipmentLabelResult(true, tracking, externalId, null, null); + } + + private async Task CreateDpdAsync( + ShipmentRequest request, + CancellationToken cancellationToken) + { + var login = configuration["Shipping:Dpd:Login"]; + var password = configuration["Shipping:Dpd:Password"]; + var fid = configuration["Shipping:Dpd:Fid"]; + var baseUrl = configuration["Shipping:Dpd:ApiBaseUrl"]; + + if (string.IsNullOrWhiteSpace(login) || + string.IsNullOrWhiteSpace(password) || + string.IsNullOrWhiteSpace(fid) || + string.IsNullOrWhiteSpace(baseUrl)) + { + return Fail("Configure Shipping:Dpd:Login, Password, Fid and ApiBaseUrl."); + } + + // DPD PL PackageService-style XML envelope (simplified create). + var receiverAddress = request.ServiceType == ShippingServiceType.ParcelLocker + ? request.ParcelPointAddress ?? request.ParcelPointName ?? request.ShippingStreet + : request.ShippingStreet; + + var xml = new XDocument( + new XElement("packages", + new XElement("package", + new XElement("reference", request.OrderNumber), + new XElement("receiver", + new XElement("name", + $"{request.CustomerFirstName} {request.CustomerLastName}".Trim()), + new XElement("email", request.CustomerEmail), + new XElement("phone", request.CustomerPhone), + new XElement("address", receiverAddress), + new XElement("city", request.ShippingCity), + new XElement("postalCode", request.ShippingPostalCode), + new XElement("country", + string.IsNullOrWhiteSpace(request.ShippingCountry) + ? "PL" + : request.ShippingCountry), + new XElement("pointId", request.ParcelPointId ?? string.Empty)), + new XElement("parcels", + new XElement("parcel", + new XElement("weight", "1"), + new XElement("sizeX", "30"), + new XElement("sizeY", "20"), + new XElement("sizeZ", "15"))), + request.CodAmount > 0 + ? new XElement("cod", + new XElement("amount", request.CodAmount), + new XElement("currency", "PLN")) + : null))); + + var client = httpClientFactory.CreateClient(nameof(ShippingCarrierClients)); + using var httpRequest = new HttpRequestMessage( + HttpMethod.Post, + baseUrl.TrimEnd('/')); + httpRequest.Headers.Add("X-DPD-Login", login); + httpRequest.Headers.Add("X-DPD-Password", password); + httpRequest.Headers.Add("X-DPD-Fid", fid); + httpRequest.Content = new StringContent( + xml.ToString(SaveOptions.DisableFormatting), + Encoding.UTF8, + "application/xml"); + + using var response = await client.SendAsync(httpRequest, cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + logger.LogError("DPD create failed: {Status} {Body}", + (int)response.StatusCode, body); + return Fail("DPD shipment create failed."); + } + + string? tracking = null; + string? externalId = null; + try + { + var doc = XDocument.Parse(body); + tracking = doc.Descendants().FirstOrDefault(e => + e.Name.LocalName.Equals("waybill", StringComparison.OrdinalIgnoreCase) || + e.Name.LocalName.Equals("trackingNumber", StringComparison.OrdinalIgnoreCase)) + ?.Value; + externalId = doc.Descendants().FirstOrDefault(e => + e.Name.LocalName.Equals("packageId", StringComparison.OrdinalIgnoreCase) || + e.Name.LocalName.Equals("id", StringComparison.OrdinalIgnoreCase)) + ?.Value; + } + catch (Exception ex) + { + logger.LogWarning(ex, "DPD response parse fallback"); + } + + tracking ??= $"DPD-{request.OrderNumber}"; + return new ShipmentLabelResult(true, tracking, externalId, null, null); + } + + private static ShipmentLabelResult Fail(string message) => + new(false, null, null, null, message); +} diff --git a/src/backend/src/iLED.Infrastructure/Services/ShippingService.cs b/src/backend/src/iLED.Infrastructure/Services/ShippingService.cs new file mode 100644 index 0000000..3e496c7 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/ShippingService.cs @@ -0,0 +1,352 @@ +using System.Globalization; +using System.Text; +using iLED.Application.Common.Interfaces; +using iLED.Domain.Enums; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace iLED.Infrastructure.Services; + +public sealed class ShippingService( + IConfiguration configuration, + ILogger logger, + ShippingLabelStorage labelStorage, + ShippingCarrierClients carrierClients, + DpdPudoClient dpdPudoClient) : IShippingService +{ + public async Task> SearchParcelPointsAsync( + ShippingParcelPointSearch search, + CancellationToken cancellationToken = default) + { + if (search.Carrier == ShippingCarrier.Dpd) + { + try + { + return await dpdPudoClient.SearchAsync( + search.Query ?? string.Empty, + search.Latitude, + search.Longitude, + cancellationToken); + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "DPD pudofinder search failed; falling back to demo points"); + } + } + + var normalized = (search.Query ?? string.Empty).Trim(); + var tokens = TokenizeQuery(normalized); + var points = GetPointsForCarrier(search.Carrier) + .Where(p => MatchesQuery(p, tokens)) + .Select(p => WithDistance(p, search.Latitude, search.Longitude)) + .OrderBy(p => p.DistanceKm ?? double.MaxValue) + .ThenBy(p => p.City) + .ThenBy(p => p.Name) + .Take(50) + .ToList(); + + return points; + } + + public ShippingWidgetConfigDto GetWidgetConfig() + { + var useSandbox = configuration.GetValue("Shipping:UseSandbox", true); + var inPostToken = useSandbox + ? configuration["Shipping:InPost:GeowidgetSandboxToken"] + ?? configuration["Shipping:InPost:GeowidgetPublicToken"] + : configuration["Shipping:InPost:GeowidgetPublicToken"]; + var inPostConfig = configuration["Shipping:InPost:GeowidgetConfig"] + ?? "parcelCollect"; + var inPostSdk = configuration["Shipping:InPost:GeowidgetSdkBaseUrl"]; + if (string.IsNullOrWhiteSpace(inPostSdk)) + { + inPostSdk = useSandbox + ? "https://sandbox-easy-geowidget-sdk.easypack24.net" + : "https://geowidget.inpost.pl"; + } + + var dhlEnabled = configuration.GetValue("Shipping:Dhl:WidgetEnabled", true); + var dhlUrl = configuration["Shipping:Dhl:WidgetUrl"] + ?? "https://parcelshop.dhl.pl/mapa"; + var dpdEnabled = configuration.GetValue("Shipping:Dpd:WidgetEnabled", true); + var dpdUrl = configuration["Shipping:Dpd:WidgetUrl"] + ?? "https://pudofinder.dpd.com.pl/"; + if (dpdUrl.Contains("pickup.dpd.pl", StringComparison.OrdinalIgnoreCase)) + { + // Broken TLS / not a pickup widget — use the official PL finder. + dpdUrl = "https://pudofinder.dpd.com.pl/"; + } + + return new ShippingWidgetConfigDto( + useSandbox, + string.IsNullOrWhiteSpace(inPostToken) + ? null + : new InPostGeowidgetConfigDto( + inPostToken, + inPostConfig, + inPostSdk.TrimEnd('/')), + dhlEnabled ? new DhlWidgetConfigDto(true, dhlUrl) : null, + dpdEnabled ? new DpdWidgetConfigDto(true, dpdUrl) : null); + } + + private static ParcelPointDto WithDistance( + ParcelPointDto point, + double? latitude, + double? longitude) + { + if (latitude is null || longitude is null) + { + return point; + } + + var distance = HaversineKm( + latitude.Value, + longitude.Value, + point.Latitude, + point.Longitude); + + return point with { DistanceKm = Math.Round(distance, 1) }; + } + + private static double HaversineKm( + double lat1, + double lon1, + double lat2, + double lon2) + { + const double earthRadiusKm = 6371; + var dLat = DegreesToRadians(lat2 - lat1); + var dLon = DegreesToRadians(lon2 - lon1); + var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(DegreesToRadians(lat1)) * Math.Cos(DegreesToRadians(lat2)) * + Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); + return earthRadiusKm * c; + } + + private static double DegreesToRadians(double degrees) => + degrees * Math.PI / 180; + + private static bool MatchesQuery(ParcelPointDto point, IReadOnlyList tokens) + { + if (tokens.Count == 0) + { + return true; + } + + var haystack = NormalizeForSearch( + $"{point.Name} {point.Address} {point.City} {point.PostalCode}"); + + return tokens.All(token => haystack.Contains(token)); + } + + private static IReadOnlyList TokenizeQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + return []; + } + + return query + .Split([' ', ',', '-', '—', '/'], StringSplitOptions.RemoveEmptyEntries) + .Select(NormalizeForSearch) + .Where(token => token.Length > 0) + .Distinct() + .ToList(); + } + + private static string NormalizeForSearch(string value) + { + var normalized = value.Normalize(NormalizationForm.FormD); + var builder = new StringBuilder(normalized.Length); + + foreach (var ch in normalized) + { + if (CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark) + { + builder.Append(ch); + } + } + + return builder.ToString().ToLowerInvariant(); + } + + public async Task CreateShipmentLabelAsync( + ShipmentRequest request, + CancellationToken cancellationToken = default) + { + var useSandbox = configuration.GetValue("Shipping:UseSandbox", true); + + if (!useSandbox) + { + var live = await carrierClients.CreateAsync(request, cancellationToken); + if (live.Success && + string.IsNullOrWhiteSpace(live.LabelRelativePath) && + !string.IsNullOrWhiteSpace(live.TrackingNumber)) + { + var fallbackHtml = BuildLabelHtml( + request, + live.TrackingNumber, + live.ExternalShipmentId ?? live.TrackingNumber); + var path = await labelStorage.SaveLabelAsync( + request.OrderId, + fallbackHtml, + cancellationToken); + return live with { LabelRelativePath = path }; + } + + return live; + } + + var tracking = $"{request.Carrier.ToString().ToUpperInvariant()}-{DateTime.UtcNow:yyyyMMdd}-{Random.Shared.Next(100000, 999999)}"; + var externalId = $"{request.Carrier}-{Guid.NewGuid():N}"; + + var labelHtml = BuildLabelHtml(request, tracking, externalId); + var labelPath = await labelStorage.SaveLabelAsync( + request.OrderId, + labelHtml, + cancellationToken); + + logger.LogInformation( + "Sandbox {Carrier} label for order {OrderNumber}: {Tracking}", + request.Carrier, + request.OrderNumber, + tracking); + + return new ShipmentLabelResult( + true, + tracking, + externalId, + labelPath, + null); + } + + private static string BuildLabelHtml( + ShipmentRequest request, + string tracking, + string externalId) + { + var destination = request.ServiceType == ShippingServiceType.ParcelLocker && + !string.IsNullOrWhiteSpace(request.ParcelPointName) + ? $"{request.ParcelPointName}
{request.ParcelPointAddress}" + : $"{request.ShippingStreet}
{request.ShippingPostalCode} {request.ShippingCity}"; + + var lines = string.Join( + "", + request.Lines.Select(l => + $"{l.ProductName}{l.Quantity}")); + + var sb = new StringBuilder(); + sb.Append(""); + sb.Append("Shipping label"); + sb.Append(""); + sb.Append($"

{request.Carrier} — {request.ServiceType}

"); + sb.Append($"

{tracking}

"); + sb.Append($"

Order: {request.OrderNumber}

"); + sb.Append($"

Ref: {externalId}

"); + sb.Append("

Recipient

"); + sb.Append($"

{request.CustomerFirstName} {request.CustomerLastName}
"); + sb.Append($"{request.CustomerPhone}
{destination}

"); + if (request.CodAmount > 0) + { + sb.Append($"

COD: {request.CodAmount:F2} PLN

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

Contents

"); + sb.Append(lines); + sb.Append("
ProductQty
"); + return sb.ToString(); + } + + private static IReadOnlyList GetPointsForCarrier( + ShippingCarrier carrier) => + carrier switch + { + ShippingCarrier.Dhl => MockDhlPoints(), + ShippingCarrier.Dpd => MockDpdPoints(), + _ => MockInPostPoints(), + }; + + private static IReadOnlyList MockInPostPoints() => + [ + new("INPOST-WAW01", "Paczkomat WAW01", "ul. Marszałkowska 1", "Warszawa", "00-001", 52.2297, 21.0122, "24/7", true), + new("INPOST-WAW02", "Paczkomat WAW02", "ul. Puławska 12", "Warszawa", "02-566", 52.1964, 21.0230, "24/7", true), + new("INPOST-KRK01", "Paczkomat KRK01", "ul. Floriańska 3", "Kraków", "31-019", 50.0619, 19.9370, "24/7", true), + new("INPOST-GDN01", "Paczkomat GDN01", "ul. Długa 5", "Gdańsk", "80-831", 54.3498, 18.6536, "24/7", false), + new("INPOST-WRO01", "Paczkomat WRO01", "ul. Świdnicka 8", "Wrocław", "50-066", 51.1079, 17.0385, "24/7", true), + new("INPOST-POZ01", "Paczkomat POZ01", "ul. Półwiejska 20", "Poznań", "61-888", 52.4024, 16.9262, "24/7", true), + ]; + + private static IReadOnlyList MockDhlPoints() => + [ + new("DHL-WAW01", "DHL POP Warszawa Centrum", "ul. Złota 59", "Warszawa", "00-120", 52.2324, 21.0025, "Pn–Pt 8:00–18:00", true), + new("DHL-WAW02", "DHL POP Mokotów", "ul. Puławska 145", "Warszawa", "02-715", 52.1889, 21.0261, "Pn–Pt 9:00–19:00", true), + new("DHL-KRK01", "DHL POP Kraków Stare Miasto", "ul. Grodzka 8", "Kraków", "31-044", 50.0570, 19.9378, "Pn–Sb 8:00–20:00", true), + new("DHL-GDN01", "DHL POP Gdańsk", "ul. Długi Targ 28", "Gdańsk", "80-828", 54.3485, 18.6533, "Pn–Pt 8:00–18:00", false), + new("DHL-WRO01", "DHL POP Wrocław", "ul. Oławska 1", "Wrocław", "50-123", 51.0980, 17.0340, "Pn–Pt 8:00–18:00", true), + new("DHL-POZ01", "DHL POP Poznań", "ul. Półwiejska 42", "Poznań", "61-888", 52.4010, 16.9285, "Pn–Pt 8:00–18:00", true), + ]; + + private static IReadOnlyList MockDpdPoints() => + [ + new("DPD-WAW01", "DPD Pickup Warszawa Śródmieście", "ul. Nowy Świat 33", "Warszawa", "00-029", 52.2319, 21.0185, "Pn–Pt 8:00–20:00", true), + new("DPD-WAW02", "DPD Pickup Wilanów", "ul. Przyczółkowa 50", "Warszawa", "02-967", 52.1634, 21.0875, "Pn–Pt 9:00–18:00", true), + new("DPD-KRK01", "DPD Pickup Kraków Kazimierz", "ul. Józefa 22", "Kraków", "31-056", 50.0515, 19.9442, "Pn–Sb 8:00–20:00", true), + new("DPD-GDN01", "DPD Pickup Gdańsk Oliwa", "ul. Grunwaldzka 141", "Gdańsk", "80-264", 54.4110, 18.5710, "Pn–Pt 8:00–18:00", false), + new("DPD-WRO01", "DPD Pickup Wrocław Rynek", "Rynek 13", "Wrocław", "50-101", 51.1099, 17.0325, "Pn–Pt 8:00–18:00", true), + new("DPD-POZ01", "DPD Pickup Poznań Jeżyce", "ul. Dąbrowskiego 77", "Poznań", "60-523", 52.4150, 16.9050, "Pn–Pt 8:00–18:00", true), + ]; +} + +public sealed class ShippingLabelStorageOptions +{ + public const string SectionName = "Shipping:Labels"; + + public string RelativePath { get; set; } = "uploads/labels"; +} + +public sealed class ShippingLabelStorage +{ + private readonly string _rootPath; + private readonly string _relativePath; + + public ShippingLabelStorage( + Microsoft.AspNetCore.Hosting.IWebHostEnvironment env, + Microsoft.Extensions.Options.IOptions options) + { + _relativePath = options.Value.RelativePath.Trim('/'); + _rootPath = Path.Combine(env.WebRootPath, _relativePath); + Directory.CreateDirectory(_rootPath); + } + + public async Task SaveLabelAsync( + Guid orderId, + string html, + CancellationToken cancellationToken) + { + var fileName = $"{orderId:N}.html"; + var fullPath = Path.Combine(_rootPath, fileName); + await File.WriteAllTextAsync(fullPath, html, cancellationToken); + return $"/{_relativePath}/{fileName}"; + } + + public async Task SaveLabelBytesAsync( + Guid orderId, + byte[] bytes, + string extension, + CancellationToken cancellationToken) + { + var safeExt = string.IsNullOrWhiteSpace(extension) + ? "bin" + : extension.Trim().TrimStart('.'); + var fileName = $"{orderId:N}.{safeExt}"; + var fullPath = Path.Combine(_rootPath, fileName); + await File.WriteAllBytesAsync(fullPath, bytes, cancellationToken); + return $"/{_relativePath}/{fileName}"; + } +} diff --git a/src/backend/src/iLED.Infrastructure/iLED.Infrastructure.csproj b/src/backend/src/iLED.Infrastructure/iLED.Infrastructure.csproj new file mode 100644 index 0000000..0f4f085 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/iLED.Infrastructure.csproj @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/src/backend/tests/iLED.Application.Tests/UnitTest1.cs b/src/backend/tests/iLED.Application.Tests/UnitTest1.cs new file mode 100644 index 0000000..7b4928a --- /dev/null +++ b/src/backend/tests/iLED.Application.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace iLED.Application.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/src/backend/tests/iLED.Application.Tests/iLED.Application.Tests.csproj b/src/backend/tests/iLED.Application.Tests/iLED.Application.Tests.csproj new file mode 100644 index 0000000..f5a58b8 --- /dev/null +++ b/src/backend/tests/iLED.Application.Tests/iLED.Application.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/src/backend/tests/iLED.Domain.Tests/DiscountCodeTests.cs b/src/backend/tests/iLED.Domain.Tests/DiscountCodeTests.cs new file mode 100644 index 0000000..e582b7c --- /dev/null +++ b/src/backend/tests/iLED.Domain.Tests/DiscountCodeTests.cs @@ -0,0 +1,34 @@ +using iLED.Domain.Marketing; + +namespace iLED.Domain.Tests; + +public class DiscountCodeTests +{ + [Fact] + public void CanBeUsed_WhenUnderMaxUsages_ReturnsTrue() + { + var code = new DiscountCode + { + Code = "LOVE20", + IsActive = true, + MaxUsages = 10, + UsageCount = 5, + }; + + Assert.True(code.CanBeUsed); + } + + [Fact] + public void CanBeUsed_WhenMaxUsagesReached_ReturnsFalse() + { + var code = new DiscountCode + { + Code = "LOVE20", + IsActive = true, + MaxUsages = 10, + UsageCount = 10, + }; + + Assert.False(code.CanBeUsed); + } +} diff --git a/src/backend/tests/iLED.Domain.Tests/UnitTest1.cs b/src/backend/tests/iLED.Domain.Tests/UnitTest1.cs new file mode 100644 index 0000000..a9e940c --- /dev/null +++ b/src/backend/tests/iLED.Domain.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace iLED.Domain.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/src/backend/tests/iLED.Domain.Tests/iLED.Domain.Tests.csproj b/src/backend/tests/iLED.Domain.Tests/iLED.Domain.Tests.csproj new file mode 100644 index 0000000..e090c6f --- /dev/null +++ b/src/backend/tests/iLED.Domain.Tests/iLED.Domain.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/src/shop/.editorconfig b/src/shop/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/src/shop/.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/src/shop/.gitignore b/src/shop/.gitignore new file mode 100644 index 0000000..b1d225e --- /dev/null +++ b/src/shop/.gitignore @@ -0,0 +1,43 @@ +# 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 +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/src/shop/.postcssrc.json b/src/shop/.postcssrc.json new file mode 100644 index 0000000..e092dc7 --- /dev/null +++ b/src/shop/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/src/shop/.vscode/extensions.json b/src/shop/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/src/shop/.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/src/shop/.vscode/launch.json b/src/shop/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/src/shop/.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/src/shop/.vscode/tasks.json b/src/shop/.vscode/tasks.json new file mode 100644 index 0000000..244306f --- /dev/null +++ b/src/shop/.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": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/src/shop/README.md b/src/shop/README.md new file mode 100644 index 0000000..ff91105 --- /dev/null +++ b/src/shop/README.md @@ -0,0 +1,59 @@ +# Shop + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.4. + +## Development server + +To start a local development server, run: + +```bash +ng serve +``` + +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. + +## Code scaffolding + +Angular CLI includes powerful code scaffolding tools. To generate a new component, run: + +```bash +ng generate component component-name +``` + +For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: + +```bash +ng generate --help +``` + +## Building + +To build the project run: + +```bash +ng build +``` + +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. + +## Running unit tests + +To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: + +```bash +ng test +``` + +## Running end-to-end tests + +For end-to-end (e2e) testing, run: + +```bash +ng e2e +``` + +Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. + +## Additional Resources + +For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/src/shop/angular.json b/src/shop/angular.json new file mode 100644 index 0000000..c58dcfa --- /dev/null +++ b/src/shop/angular.json @@ -0,0 +1,82 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm" + }, + "newProjectRoot": "projects", + "projects": { + "shop": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "options": { + "host": "0.0.0.0", + "port": 4200 + }, + "configurations": { + "production": { + "buildTarget": "shop:build:production" + }, + "development": { + "buildTarget": "shop:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/src/shop/package-lock.json b/src/shop/package-lock.json new file mode 100644 index 0000000..da6d593 --- /dev/null +++ b/src/shop/package-lock.json @@ -0,0 +1,9266 @@ +{ + "name": "shop", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shop", + "version": "0.0.0", + "dependencies": { + "@angular/common": "^21.0.0", + "@angular/compiler": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/router": "^21.0.0", + "@lucide/angular": "^1.23.0", + "leaflet": "^1.9.4", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.0.4", + "@angular/cli": "^21.0.4", + "@angular/compiler-cli": "^21.0.0", + "@tailwindcss/postcss": "^4.3.2", + "@types/leaflet": "^1.9.21", + "jsdom": "^27.1.0", + "postcss": "^8.5.16", + "tailwindcss": "^4.3.2", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/abtesting": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", + "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", + "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", + "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", + "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", + "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", + "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", + "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", + "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", + "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", + "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", + "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", + "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", + "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", + "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@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.2102.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.18.tgz", + "integrity": "sha512-bEkF4ApWa6FJ2+ZyFPdhQr05PgJv/oKwx+3goNRa4lSxzJO+O3KXr0ivTyE5zEJ8l4JdV3h93bsCP54Rk3v7Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.18", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.18.tgz", + "integrity": "sha512-783AuDmzAjFkOBHey7cx1mXd8CTiMg9LvG+BCxEp8i4EB7cOGYrq3lBzkwhKJq6yYpz7dNgM1QgeLk+NcrFBSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.18.tgz", + "integrity": "sha512-PakHs+Eefx06KORDFyH4wmkIZ/wm4mI4DqhXe4XFuWJOazMQoYJxPmMeby47BD0HhvJN7BskN61CPS8HCAvICg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.18", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.3.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/build": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.18.tgz", + "integrity": "sha512-9Jic0Dud6d0ULPR5SceHV86AzZE42cNWPsbLKrK1dUneaAC44GSVRTmLXcaSX/5Exa3gGjFANQw4qashgxUekQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2102.18", + "@babel/core": "7.29.7", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.4", + "beasties": "0.4.1", + "browserslist": "^4.26.0", + "esbuild": "0.28.1", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.4", + "piscina": "5.2.0", + "rolldown": "1.0.0-rc.4", + "sass": "1.97.3", + "semver": "7.7.4", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.15", + "undici": "7.28.0", + "vite": "7.3.6", + "watchpack": "2.5.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.1" + }, + "peerDependencies": { + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.2.18", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^21.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.18.tgz", + "integrity": "sha512-WOZF4p0SnrmBpNeiOA2pdp9OBvGnOSLWbGdRLXJykyseIiEn7+N+GZp+ZA1aNZB4SWolpYCZfTYwkpo6Sf/LXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2102.18", + "@angular-devkit/core": "21.2.18", + "@angular-devkit/schematics": "21.2.18", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.2.18", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.5.1", + "parse5-html-rewriting-stream": "8.0.0", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.3.6" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.17.tgz", + "integrity": "sha512-hqAQxRfi5ldFE42suAXRcY+JCANrUh7fuSQ/DtZ7L896id5BT/exuv6dWNBC1PyAfQmRbpD5Pt6/pd+tNLyhDQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.17.tgz", + "integrity": "sha512-p+NdjYiwAz9Zmu2yul0LlMXaFjMISVVa24+/MVMoKFeQeI82QE8jDywPlnOSHQHvdCcQVpS7saeEriZzX3JuBQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.17.tgz", + "integrity": "sha512-KithZ3b0HBFH0NbUcswBcjpN9y09vLbarMD7qmGWTnGUBk4W8cn4sbT8zJyv9CRKg9ZcuUBeJYKUfUPn/u/5OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.29.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.17", + "typescript": ">=5.9 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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/@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": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.17.tgz", + "integrity": "sha512-wYHpwIdnUnjQFOJJNqRcGx7LS3u64jT+R9L0TnMR/ViBM9dQgGYImlSikkftg2yrFCNo5aKRxhG2LLskQurVdg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.17.tgz", + "integrity": "sha512-WKu8XeRSNZo+a+aDDZ3M5OtReF7KYqR/PmZ2l1lSf6N5EEAmc+Ky4aqbRhTL/mTSfHrO4+TDJ4C5A2tFmuwIeA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.17", + "@angular/core": "21.2.17", + "@angular/platform-browser": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.17.tgz", + "integrity": "sha512-ROdSliejY37g1EphYmweYdm5cHM8HY3X4tbWt4ubxmhTyYgfN3nxrxfGQ/n7Mz5tDY9VXVLIGDgjLOGYOo4uTQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "21.2.17", + "@angular/common": "21.2.17", + "@angular/core": "21.2.17" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.17.tgz", + "integrity": "sha512-RSCtK5ppAV6y6wfRLHSK2a9Wc/vm8j0wsC+/j9PH9yQmppWFVXDWsg5E39MKOIpnoYVx2+hI6eak6+wYtZTe1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.17", + "@angular/core": "21.2.17", + "@angular/platform-browser": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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/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.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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-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-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "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-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "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/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/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", + "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", + "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", + "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", + "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", + "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", + "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", + "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lucide/angular": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lucide/angular/-/angular-1.23.0.tgz", + "integrity": "sha512-zLt2s9EynuYO/kUd+b8dVeAER/ra8Pwcl5LVH31XRSc5TIoSNXCPQINN9LjTMqaZAeAw2ghwOTWO5FVumB8Ppw==", + "license": "ISC", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=17.0.0", + "@angular/core": ">=17.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", + "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", + "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", + "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "21.2.18", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.18.tgz", + "integrity": "sha512-XNEWvJSUg/ljLXgGVs7picBOtzVC4abBf2+i/ds9E7GaYYVyH19Mpgk6BOuMHAgRpjPWBAeWiBkTqetiR2Ps5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.18", + "@angular-devkit/schematics": "21.2.18", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "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.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.2.tgz", + "integrity": "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "postcss": "^8.5.15", + "tailwindcss": "4.3.2" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/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/@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": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "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/algoliasearch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "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.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/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": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "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": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/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/cliui/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/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/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "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/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/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/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.383", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz", + "integrity": "sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==", + "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/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/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/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": "2.2.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", + "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "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-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.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/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.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "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": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/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/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "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.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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-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-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/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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/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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/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", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.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/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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/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/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.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": ">=20.0.0" + } + }, + "node_modules/listr2/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/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.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "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/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/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/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/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.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/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "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": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "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.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "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.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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", + "optional": true, + "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/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.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/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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/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/ora": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-map": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.5.tgz", + "integrity": "sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.5.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.1.tgz", + "integrity": "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/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/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "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-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-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", + "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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/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/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.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/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/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/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/rolldown": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", + "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.113.0", + "@rolldown/pluginutils": "1.0.0-rc.4" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-x64": "1.0.0-rc.4", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/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.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/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/sass/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/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?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/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.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.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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-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-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": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "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/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.5.tgz", + "integrity": "sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.5" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.5.tgz", + "integrity": "sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==", + "dev": true, + "license": "MIT" + }, + "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/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.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": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.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/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/wrap-ansi/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/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/wrap-ansi/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/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.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "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": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/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/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/src/shop/package.json b/src/shop/package.json new file mode 100644 index 0000000..f8d904f --- /dev/null +++ b/src/shop/package.json @@ -0,0 +1,49 @@ +{ + "name": "shop", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve --host 0.0.0.0 --port 4200", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "prettier": { + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] + }, + "private": true, + "packageManager": "npm@10.9.4", + "dependencies": { + "@angular/common": "^21.0.0", + "@angular/compiler": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/router": "^21.0.0", + "@lucide/angular": "^1.23.0", + "leaflet": "^1.9.4", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.0.4", + "@angular/cli": "^21.0.4", + "@angular/compiler-cli": "^21.0.0", + "@tailwindcss/postcss": "^4.3.2", + "@types/leaflet": "^1.9.21", + "jsdom": "^27.1.0", + "postcss": "^8.5.16", + "tailwindcss": "^4.3.2", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } +} diff --git a/src/shop/public/assets/products/lampa-led-black.svg b/src/shop/public/assets/products/lampa-led-black.svg new file mode 100644 index 0000000..308afcb --- /dev/null +++ b/src/shop/public/assets/products/lampa-led-black.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/shop/public/assets/products/lampa-led-gold.svg b/src/shop/public/assets/products/lampa-led-gold.svg new file mode 100644 index 0000000..9a47863 --- /dev/null +++ b/src/shop/public/assets/products/lampa-led-gold.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/shop/public/assets/products/lampa-led-strip.svg b/src/shop/public/assets/products/lampa-led-strip.svg new file mode 100644 index 0000000..295902b --- /dev/null +++ b/src/shop/public/assets/products/lampa-led-strip.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/shop/public/assets/products/lampa-led-wood.svg b/src/shop/public/assets/products/lampa-led-wood.svg new file mode 100644 index 0000000..78842c9 --- /dev/null +++ b/src/shop/public/assets/products/lampa-led-wood.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/shop/public/assets/products/lampa-sufitowa-led-drewno-cct.svg b/src/shop/public/assets/products/lampa-sufitowa-led-drewno-cct.svg new file mode 100644 index 0000000..b718f6d --- /dev/null +++ b/src/shop/public/assets/products/lampa-sufitowa-led-drewno-cct.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/shop/public/assets/products/placeholder.svg b/src/shop/public/assets/products/placeholder.svg new file mode 100644 index 0000000..16ebd0b --- /dev/null +++ b/src/shop/public/assets/products/placeholder.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/shop/public/favicon.ico b/src/shop/public/favicon.ico new file mode 100644 index 0000000..57614f9 Binary files /dev/null and b/src/shop/public/favicon.ico differ diff --git a/src/shop/public/favicon.svg b/src/shop/public/favicon.svg new file mode 100644 index 0000000..7e3f62f --- /dev/null +++ b/src/shop/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/shop/public/og-default.svg b/src/shop/public/og-default.svg new file mode 100644 index 0000000..f5c8a97 --- /dev/null +++ b/src/shop/public/og-default.svg @@ -0,0 +1,6 @@ + + + + iLED + Light that makes a difference + diff --git a/src/shop/public/robots.txt b/src/shop/public/robots.txt new file mode 100644 index 0000000..bf622e9 --- /dev/null +++ b/src/shop/public/robots.txt @@ -0,0 +1,10 @@ +User-agent: * +Allow: / +Disallow: /account +Disallow: /checkout +Disallow: /cart +Disallow: /login +Disallow: /favorites +Disallow: /order/ + +Sitemap: http://localhost:4200/sitemap.xml diff --git a/src/shop/public/site.webmanifest b/src/shop/public/site.webmanifest new file mode 100644 index 0000000..2a8896d --- /dev/null +++ b/src/shop/public/site.webmanifest @@ -0,0 +1,10 @@ +{ + "name": "iLED", + "short_name": "iLED", + "icons": [ + { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml" } + ], + "theme_color": "#000000", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/src/shop/public/sitemap.xml b/src/shop/public/sitemap.xml new file mode 100644 index 0000000..bf39326 --- /dev/null +++ b/src/shop/public/sitemap.xml @@ -0,0 +1,7 @@ + + + http://localhost:4200/daily1.0 + http://localhost:4200/productsdaily0.9 + http://localhost:4200/privacyyearly0.3 + http://localhost:4200/termsyearly0.3 + diff --git a/src/shop/src/app/app.config.ts b/src/shop/src/app/app.config.ts new file mode 100644 index 0000000..652c136 --- /dev/null +++ b/src/shop/src/app/app.config.ts @@ -0,0 +1,30 @@ +import { registerLocaleData } from '@angular/common'; +import localeCs from '@angular/common/locales/cs'; +import localeDe from '@angular/common/locales/de'; +import localeEn from '@angular/common/locales/en'; +import localePl from '@angular/common/locales/pl'; +import { + ApplicationConfig, + LOCALE_ID, + provideBrowserGlobalErrorListeners, + provideZonelessChangeDetection, +} from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { routes } from './app.routes'; +import { DEFAULT_LOCALE } from './core/i18n/i18n.models'; + +registerLocaleData(localePl); +registerLocaleData(localeEn); +registerLocaleData(localeDe); +registerLocaleData(localeCs); + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideZonelessChangeDetection(), + provideRouter(routes), + provideHttpClient(), + { provide: LOCALE_ID, useValue: DEFAULT_LOCALE }, + ], +}; diff --git a/src/shop/src/app/app.html b/src/shop/src/app/app.html new file mode 100644 index 0000000..e0118a1 --- /dev/null +++ b/src/shop/src/app/app.html @@ -0,0 +1,342 @@ + + + + + + + + + + + +
+
+
+ +

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: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, + { 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/src/shop/src/app/app.routes.ts b/src/shop/src/app/app.routes.ts new file mode 100644 index 0000000..6509229 --- /dev/null +++ b/src/shop/src/app/app.routes.ts @@ -0,0 +1,225 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: '', + loadComponent: () => + import('./features/home/home.component').then((m) => m.HomeComponent), + data: { + titleKey: 'meta.home.title', + descriptionKey: 'meta.home.description', + }, + }, + { + path: 'products', + loadComponent: () => + import('./features/products/product-list.component').then( + (m) => m.ProductListComponent, + ), + data: { + titleKey: 'meta.products.title', + descriptionKey: 'meta.products.description', + }, + }, + { + path: 'products/:slug', + loadComponent: () => + import('./features/products/product-detail.component').then( + (m) => m.ProductDetailComponent, + ), + }, + { + path: 'cart', + loadComponent: () => + import('./features/cart/cart.component').then((m) => m.CartPageComponent), + data: { + titleKey: 'meta.cart.title', + descriptionKey: 'meta.cart.description', + }, + }, + { + path: 'checkout/payu-sandbox/:orderId', + loadComponent: () => + import('./features/checkout/payu-sandbox.component').then( + (m) => m.PayuSandboxComponent, + ), + data: { + titleKey: 'meta.checkout.title', + descriptionKey: 'meta.checkout.description', + }, + }, + { + path: 'checkout', + pathMatch: 'full', + loadComponent: () => + import('./features/checkout/checkout.component').then( + (m) => m.CheckoutComponent, + ), + data: { + titleKey: 'meta.checkout.title', + descriptionKey: 'meta.checkout.description', + }, + }, + { + path: 'order/:id', + loadComponent: () => + import('./features/checkout/checkout.component').then( + (m) => m.OrderConfirmationComponent, + ), + data: { + titleKey: 'meta.order.title', + descriptionKey: 'meta.order.description', + }, + }, + { + path: 'login', + loadComponent: () => + import('./features/auth/login.component').then((m) => m.LoginComponent), + data: { + titleKey: 'meta.login.title', + descriptionKey: 'meta.login.description', + }, + }, + { + path: 'favorites', + loadComponent: () => + import('./features/account/account.components').then( + (m) => m.FavoritesComponent, + ), + data: { + titleKey: 'meta.favorites.title', + descriptionKey: 'meta.favorites.description', + }, + }, + { + path: 'account', + loadComponent: () => + import('./features/account/account.components').then( + (m) => m.AccountComponent, + ), + data: { + titleKey: 'meta.account.title', + descriptionKey: 'meta.account.description', + }, + }, + { + path: 'privacy', + loadComponent: () => + import('./features/legal/legal-page.component').then( + (m) => m.LegalPageComponent, + ), + data: { + page: 'privacy', + titleKey: 'meta.privacy.title', + descriptionKey: 'meta.privacy.description', + }, + }, + { + path: 'terms', + loadComponent: () => + import('./features/legal/legal-page.component').then( + (m) => m.LegalPageComponent, + ), + data: { + page: 'terms', + titleKey: 'meta.terms.title', + descriptionKey: 'meta.terms.description', + }, + }, + { + path: 'about', + loadComponent: () => + import('./features/legal/legal-page.component').then( + (m) => m.LegalPageComponent, + ), + data: { + page: 'about', + titleKey: 'meta.about.title', + descriptionKey: 'meta.about.description', + }, + }, + { + path: 'contact', + loadComponent: () => + import('./features/legal/legal-page.component').then( + (m) => m.LegalPageComponent, + ), + data: { + page: 'contact', + titleKey: 'meta.contact.title', + descriptionKey: 'meta.contact.description', + }, + }, + { + path: 'shipping', + loadComponent: () => + import('./features/legal/legal-page.component').then( + (m) => m.LegalPageComponent, + ), + data: { + page: 'shipping', + titleKey: 'meta.shipping.title', + descriptionKey: 'meta.shipping.description', + }, + }, + { + path: 'returns', + loadComponent: () => + import('./features/legal/legal-page.component').then( + (m) => m.LegalPageComponent, + ), + data: { + page: 'returns', + titleKey: 'meta.returns.title', + descriptionKey: 'meta.returns.description', + }, + }, + { + path: 'faq', + loadComponent: () => + import('./features/legal/legal-page.component').then( + (m) => m.LegalPageComponent, + ), + data: { + page: 'faq', + titleKey: 'meta.faq.title', + descriptionKey: 'meta.faq.description', + }, + }, + { + path: 'cookies', + loadComponent: () => + import('./features/legal/legal-page.component').then( + (m) => m.LegalPageComponent, + ), + data: { + page: 'cookies', + titleKey: 'meta.cookies.title', + descriptionKey: 'meta.cookies.description', + }, + }, + { path: 'produkty', redirectTo: 'products', pathMatch: 'prefix' }, + { path: 'koszyk', redirectTo: 'cart', pathMatch: 'full' }, + { path: 'kasa', redirectTo: 'checkout', pathMatch: 'full' }, + { path: 'zamowienie/:id', redirectTo: 'order/:id', pathMatch: 'full' }, + { path: 'logowanie', redirectTo: 'login', pathMatch: 'full' }, + { path: 'ulubione', redirectTo: 'favorites', pathMatch: 'full' }, + { path: 'konto', redirectTo: 'account', pathMatch: 'full' }, + { path: 'polityka-prywatnosci', redirectTo: 'privacy', pathMatch: 'full' }, + { path: 'regulamin', redirectTo: 'terms', pathMatch: 'full' }, + { path: 'o-nas', redirectTo: 'about', pathMatch: 'full' }, + { path: 'kontakt', redirectTo: 'contact', pathMatch: 'full' }, + { path: 'dostawa', redirectTo: 'shipping', pathMatch: 'full' }, + { path: 'zwroty', redirectTo: 'returns', pathMatch: 'full' }, + { + path: '**', + loadComponent: () => + import('./features/not-found/not-found.component').then( + (m) => m.NotFoundComponent, + ), + data: { + titleKey: 'meta.notFound.title', + descriptionKey: 'meta.notFound.description', + }, + }, +]; diff --git a/src/shop/src/app/app.scss b/src/shop/src/app/app.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/shop/src/app/app.spec.ts b/src/shop/src/app/app.spec.ts new file mode 100644 index 0000000..686752c --- /dev/null +++ b/src/shop/src/app/app.spec.ts @@ -0,0 +1,42 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { App } from './app'; +import { SessionService } from './core/api.service'; +import { AuthService } from './core/auth.service'; +import { CartService } from './core/shop.services'; + +describe('App', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [ + provideRouter([]), + { + provide: SessionService, + useValue: { ensureSession: () => of(null) }, + }, + { + provide: CartService, + useValue: { load: () => undefined }, + }, + { + provide: AuthService, + useValue: { loadProfile: () => undefined }, + }, + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(App); + expect(fixture.componentInstance).toBeTruthy(); + }); + + it('should render router outlet', () => { + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('router-outlet')).toBeTruthy(); + }); +}); diff --git a/src/shop/src/app/app.ts b/src/shop/src/app/app.ts new file mode 100644 index 0000000..6b36373 --- /dev/null +++ b/src/shop/src/app/app.ts @@ -0,0 +1,47 @@ +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + OnInit, +} from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { SessionService } from './core/api.service'; +import { AnalyticsService } from './core/analytics.service'; +import { I18nService } from './core/i18n/i18n.service'; +import { SeoInitializer } from './core/seo.initializer'; +import { CartService } from './core/shop.services'; +import { AuthService } from './core/auth.service'; +import { CookieBannerComponent } from './shared/cookie-banner.component'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet, CookieBannerComponent], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App implements OnInit { + readonly #session = inject(SessionService); + readonly #cart = inject(CartService); + readonly #auth = inject(AuthService); + readonly #i18n = inject(I18nService); + readonly #seoInit = inject(SeoInitializer); + readonly #analytics = inject(AnalyticsService); + + constructor() { + effect(() => { + this.#i18n.locale(); + this.#seoInit.refresh(); + }); + } + + ngOnInit(): void { + this.#i18n.init(); + this.#seoInit.init(); + this.#session.ensureSession(); + this.#cart.load(); + this.#auth.loadProfile(); + void this.#analytics; + } +} diff --git a/src/shop/src/app/core/analytics.service.ts b/src/shop/src/app/core/analytics.service.ts new file mode 100644 index 0000000..1195f3a --- /dev/null +++ b/src/shop/src/app/core/analytics.service.ts @@ -0,0 +1,84 @@ +import { Injectable, effect, inject } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs'; +import { CookieConsentService } from './cookie-consent.service'; +import { environment } from '../../environments/environment'; + +declare global { + interface Window { + op?: ((...args: unknown[]) => void) & { q?: unknown[] }; + } +} + +@Injectable({ providedIn: 'root' }) +export class AnalyticsService { + readonly #consent = inject(CookieConsentService); + readonly #router = inject(Router); + #scriptLoaded = false; + + constructor() { + effect(() => { + if (this.#consent.analyticsAllowed()) { + this.#ensureScript(); + } + }); + + this.#router.events + .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) + .subscribe((e) => this.trackPageView(e.urlAfterRedirects)); + } + + trackPageView(path: string): void { + if (!this.#consent.analyticsAllowed() || !this.#scriptLoaded) { + return; + } + + window.op?.('track', 'page_view', { path }); + } + + track(event: string, props?: Record): void { + if (!this.#consent.analyticsAllowed() || !this.#scriptLoaded) { + return; + } + + window.op?.('track', event, props ?? {}); + } + + #ensureScript(): void { + const { openPanelApiUrl, openPanelClientId } = environment; + if (!openPanelApiUrl || !openPanelClientId || this.#scriptLoaded) { + return; + } + + window.op = + window.op || + function (...args: unknown[]) { + (window.op as { q?: unknown[] }).q = + (window.op as { q?: unknown[] }).q || []; + (window.op as { q: unknown[] }).q.push(args); + }; + + window.op('init', { + apiUrl: openPanelApiUrl, + clientId: openPanelClientId, + trackScreenViews: false, + trackOutgoingLinks: true, + trackAttributes: true, + }); + + const script = document.createElement('script'); + script.async = true; + script.defer = true; + const base = openPanelApiUrl.replace(/\/$/, ''); + script.src = base.endsWith('/api') + ? `${base.replace(/\/api$/, '')}/op.js` + : `${base}/op.js`; + script.dataset['clientId'] = openPanelClientId; + script.dataset['apiUrl'] = openPanelApiUrl; + script.onload = () => { + this.#scriptLoaded = true; + this.trackPageView(this.#router.url); + }; + document.head.appendChild(script); + } +} diff --git a/src/shop/src/app/core/api.service.ts b/src/shop/src/app/core/api.service.ts new file mode 100644 index 0000000..7200105 --- /dev/null +++ b/src/shop/src/app/core/api.service.ts @@ -0,0 +1,69 @@ +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { API_URL, SESSION_KEY } from './constants'; +import { I18nService } from './i18n/i18n.service'; + +@Injectable({ providedIn: 'root' }) +export class ApiService { + readonly #http = inject(HttpClient); + readonly #i18n = inject(I18nService); + + #getHeaders(): HttpHeaders { + let headers = new HttpHeaders().set('X-Locale', this.#i18n.locale()); + const token = localStorage.getItem('iled_token'); + const session = localStorage.getItem(SESSION_KEY); + + if (token) { + headers = headers.set('Authorization', `Bearer ${token}`); + } + + if (session) { + headers = headers.set('X-Session-Id', session); + } + + return headers; + } + + get(path: string, params?: Record): Observable { + let httpParams = new HttpParams().set('locale', this.#i18n.locale()); + if (params) { + Object.entries(params).forEach(([k, v]) => { + if (v !== null && v !== undefined && v !== '') { + httpParams = httpParams.set(k, String(v)); + } + }); + } + return this.#http.get(`${API_URL}${path}`, { + headers: this.#getHeaders(), + params: httpParams, + }); + } + + post(path: string, body: unknown): Observable { + return this.#http.post(`${API_URL}${path}`, body, { + headers: this.#getHeaders(), + }); + } + + put(path: string, body: unknown): Observable { + return this.#http.put(`${API_URL}${path}`, body, { + headers: this.#getHeaders(), + }); + } + + delete(path: string): Observable { + return this.#http.delete(`${API_URL}${path}`, { + headers: this.#getHeaders(), + }); + } +} + +@Injectable({ providedIn: 'root' }) +export class SessionService { + ensureSession(): void { + if (!localStorage.getItem(SESSION_KEY)) { + localStorage.setItem(SESSION_KEY, crypto.randomUUID()); + } + } +} diff --git a/src/shop/src/app/core/auth.service.ts b/src/shop/src/app/core/auth.service.ts new file mode 100644 index 0000000..9e77a49 --- /dev/null +++ b/src/shop/src/app/core/auth.service.ts @@ -0,0 +1,62 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { tap } from 'rxjs'; +import { ApiService } from './api.service'; +import { AuthResponse, UserProfile } from './models'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + readonly #api = inject(ApiService); + + readonly #user = signal(null); + readonly user = this.#user.asReadonly(); + + readonly isLoggedIn = signal(!!localStorage.getItem('iled_token')); + + register(data: { + email: string; + password: string; + firstName: string; + lastName: string; + }) { + return this.#api.post('/auth/register', data).pipe( + tap((res) => this.#storeAuth(res)), + ); + } + + login(email: string, password: string) { + return this.#api.post('/auth/login', { email, password }).pipe( + tap((res) => this.#storeAuth(res)), + ); + } + + logout(): void { + localStorage.removeItem('iled_token'); + localStorage.removeItem('iled_refresh'); + this.#user.set(null); + this.isLoggedIn.set(false); + } + + loadProfile(): void { + if (!this.isLoggedIn()) { + return; + } + + this.#api.get('/auth/me').subscribe({ + next: (profile) => this.#user.set(profile), + error: () => this.logout(), + }); + } + + #storeAuth(res: AuthResponse): void { + localStorage.setItem('iled_token', res.accessToken); + localStorage.setItem('iled_refresh', res.refreshToken); + this.#user.set({ + userId: res.userId, + email: res.email, + firstName: res.firstName, + lastName: res.lastName, + roles: res.roles, + }); + this.isLoggedIn.set(true); + } +} diff --git a/src/shop/src/app/core/checkout.utils.ts b/src/shop/src/app/core/checkout.utils.ts new file mode 100644 index 0000000..2ae85de --- /dev/null +++ b/src/shop/src/app/core/checkout.utils.ts @@ -0,0 +1,132 @@ +export const FREE_DELIVERY_THRESHOLD = 499.99; + +export const COURIER_DELIVERY_COST = 15.99; + +export const PARCEL_LOCKER_DELIVERY_COST = 12.99; + +export const CASH_ON_DELIVERY_FEE = 5; + +export type ShippingCarrier = 'InPost' | 'Dhl' | 'Dpd'; + +export type ShippingServiceType = 'ParcelLocker' | 'Courier'; + +export type PaymentMethod = 'PayU' | 'BankTransfer' | 'CashOnDelivery'; + +export const SHIPPING_CARRIERS: ShippingCarrier[] = ['InPost', 'Dhl', 'Dpd']; + +export const PAYMENT_METHODS: PaymentMethod[] = [ + 'PayU', + 'BankTransfer', + 'CashOnDelivery', +]; + +export interface ParcelPoint { + id: string; + name: string; + address: string; + city: string; + postalCode: string; + latitude: number; + longitude: number; + openingHours?: string; + supportsCod?: boolean; + distanceKm?: number; +} + +export interface ShippingWidgetConfig { + useSandbox: boolean; + inPost?: { + publicToken: string; + config: string; + sdkBaseUrl: string; + }; + dhl?: { + enabled: boolean; + widgetUrl: string; + }; + dpd?: { + enabled: boolean; + widgetUrl: string; + }; +} + +export type DeliveryWidgetMode = 'inpost' | 'dhl' | 'dpd' | 'sandbox'; + +export function normalizeParcelPoint(raw: ParcelPoint): ParcelPoint { + return { + id: raw.id, + name: raw.name, + address: raw.address, + city: raw.city, + postalCode: raw.postalCode, + latitude: Number(raw.latitude), + longitude: Number(raw.longitude), + openingHours: raw.openingHours, + supportsCod: raw.supportsCod, + distanceKm: raw.distanceKm != null ? Number(raw.distanceKm) : undefined, + }; +} + +export function inPostGeowidgetConfig( + paymentMethod: PaymentMethod, +): string { + return paymentMethod === 'CashOnDelivery' + ? 'parcelCollectPayment' + : 'parcelCollect'; +} + +/** Normalizes API camelCase enums to PascalCase i18n key segments. */ +export function enumKey(value: string): string { + if (!value) { + return value; + } + + return value.charAt(0).toUpperCase() + value.slice(1); +} + +export function getDeliveryCost( + carrier: ShippingCarrier, + serviceType: ShippingServiceType, + subtotal: number, + freeShipping = false, +): number { + if (freeShipping || subtotal >= FREE_DELIVERY_THRESHOLD) { + return 0; + } + + if (serviceType === 'ParcelLocker') { + return PARCEL_LOCKER_DELIVERY_COST; + } + + return COURIER_DELIVERY_COST; +} + +export function getCodFee(method: PaymentMethod): number { + return method === 'CashOnDelivery' ? CASH_ON_DELIVERY_FEE : 0; +} + +export function getOrderTotal( + subtotal: number, + carrier: ShippingCarrier, + serviceType: ShippingServiceType, + paymentMethod: PaymentMethod, + discountAmount = 0, + freeShipping = false, +): number { + const afterDiscount = Math.max(0, subtotal - discountAmount); + const delivery = getDeliveryCost( + carrier, + serviceType, + afterDiscount, + freeShipping, + ); + const codFee = getCodFee(paymentMethod); + + return afterDiscount + delivery + codFee; +} + +export function defaultServiceType( + carrier: ShippingCarrier, +): ShippingServiceType { + return carrier === 'InPost' ? 'ParcelLocker' : 'Courier'; +} diff --git a/src/shop/src/app/core/constants.ts b/src/shop/src/app/core/constants.ts new file mode 100644 index 0000000..05ee959 --- /dev/null +++ b/src/shop/src/app/core/constants.ts @@ -0,0 +1,9 @@ +export const API_URL = 'http://localhost:5000/api/v1'; + +export const API_BASE_URL = 'http://localhost:5000'; + +export const SESSION_KEY = 'iled_session'; + +export const FREE_DELIVERY_THRESHOLD = 499.99; + +export const DELIVERY_COST = 15.99; diff --git a/src/shop/src/app/core/cookie-consent.service.ts b/src/shop/src/app/core/cookie-consent.service.ts new file mode 100644 index 0000000..b947c0a --- /dev/null +++ b/src/shop/src/app/core/cookie-consent.service.ts @@ -0,0 +1,42 @@ +import { Injectable, computed, signal } from '@angular/core'; + +export type CookieConsentStatus = 'accepted' | 'rejected' | 'unset'; + +const STORAGE_KEY = 'iled_cookie_consent'; + +@Injectable({ providedIn: 'root' }) +export class CookieConsentService { + readonly #status = signal(this.#read()); + readonly status = this.#status.asReadonly(); + readonly bannerVisible = computed(() => this.#status() === 'unset'); + + showBanner(): boolean { + return this.bannerVisible(); + } + + analyticsAllowed(): boolean { + return this.#status() === 'accepted'; + } + + accept(): void { + this.#persist('accepted'); + } + + reject(): void { + this.#persist('rejected'); + } + + #persist(status: CookieConsentStatus): void { + localStorage.setItem(STORAGE_KEY, status); + this.#status.set(status); + } + + #read(): CookieConsentStatus { + const value = localStorage.getItem(STORAGE_KEY); + if (value === 'accepted' || value === 'rejected') { + return value; + } + + return 'unset'; + } +} diff --git a/src/shop/src/app/core/i18n/i18n-currency.pipe.ts b/src/shop/src/app/core/i18n/i18n-currency.pipe.ts new file mode 100644 index 0000000..ab2c9ae --- /dev/null +++ b/src/shop/src/app/core/i18n/i18n-currency.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform, inject } from '@angular/core'; +import { I18nService } from './i18n.service'; + +/** Locale-aware PLN currency formatting. */ +@Pipe({ name: 'i18nCurrency', standalone: true, pure: false }) +export class I18nCurrencyPipe implements PipeTransform { + readonly #i18n = inject(I18nService); + + transform(value: number | null | undefined): string { + this.#i18n.locale(); + if (value == null) { + return ''; + } + return this.#i18n.formatCurrency(value); + } +} diff --git a/src/shop/src/app/core/i18n/i18n.models.ts b/src/shop/src/app/core/i18n/i18n.models.ts new file mode 100644 index 0000000..06a1d37 --- /dev/null +++ b/src/shop/src/app/core/i18n/i18n.models.ts @@ -0,0 +1,51 @@ +/** Supported shop locales (cs = Czech; UI may show CZ). */ +export type LocaleCode = 'pl' | 'en' | 'de' | 'cs'; + +export type TranslationMap = Record; + +export interface LocaleOption { + readonly code: LocaleCode; + readonly label: string; + readonly name: string; + readonly flag: string; + readonly htmlLang: string; + readonly intlLocale: string; +} + +export const LOCALE_OPTIONS: readonly LocaleOption[] = [ + { + code: 'pl', + label: 'PL', + name: 'Polski', + flag: '🇵🇱', + htmlLang: 'pl', + intlLocale: 'pl-PL', + }, + { + code: 'en', + label: 'EN', + name: 'English', + flag: '🇬🇧', + htmlLang: 'en', + intlLocale: 'en-GB', + }, + { + code: 'de', + label: 'DE', + name: 'Deutsch', + flag: '🇩🇪', + htmlLang: 'de', + intlLocale: 'de-DE', + }, + { + code: 'cs', + label: 'CZ', + name: 'Čeština', + flag: '🇨🇿', + htmlLang: 'cs', + intlLocale: 'cs-CZ', + }, +] as const; + +export const DEFAULT_LOCALE: LocaleCode = 'pl'; +export const LOCALE_STORAGE_KEY = 'iled-locale'; diff --git a/src/shop/src/app/core/i18n/i18n.service.spec.ts b/src/shop/src/app/core/i18n/i18n.service.spec.ts new file mode 100644 index 0000000..4cc9ae9 --- /dev/null +++ b/src/shop/src/app/core/i18n/i18n.service.spec.ts @@ -0,0 +1,43 @@ +import { DOCUMENT } from '@angular/common'; +import { TestBed } from '@angular/core/testing'; +import { DEFAULT_LOCALE } from './i18n.models'; +import { I18nService } from './i18n.service'; + +describe('I18nService', () => { + let service: I18nService; + let documentRef: Document; + + beforeEach(() => { + localStorage.clear(); + TestBed.configureTestingModule({}); + service = TestBed.inject(I18nService); + documentRef = TestBed.inject(DOCUMENT); + service.setLocale(DEFAULT_LOCALE); + service.init(); + }); + + it('returns Polish translation by default', () => { + expect(service.t('nav.home')).toBe('STRONA GŁÓWNA'); + }); + + it('interpolates translation params', () => { + expect(service.t('products.title', { count: 12 })).toBe('Produkty (12)'); + }); + + it('switches locale and updates document lang', () => { + service.setLocale('en'); + service.init(); + expect(service.t('nav.home')).toBe('HOME'); + expect(documentRef.documentElement.lang).toBe('en'); + }); + + it('falls back to key when missing in all locales', () => { + service.setLocale('en'); + expect(service.t('missing.key')).toBe('missing.key'); + }); + + it('formats currency for active locale', () => { + service.setLocale('de'); + expect(service.formatCurrency(100)).toContain('100'); + }); +}); diff --git a/src/shop/src/app/core/i18n/i18n.service.ts b/src/shop/src/app/core/i18n/i18n.service.ts new file mode 100644 index 0000000..87f364e --- /dev/null +++ b/src/shop/src/app/core/i18n/i18n.service.ts @@ -0,0 +1,124 @@ +import { DOCUMENT } from '@angular/common'; +import { + Injectable, + effect, + inject, + signal, +} from '@angular/core'; +import { + DEFAULT_LOCALE, + LOCALE_OPTIONS, + LOCALE_STORAGE_KEY, + LocaleCode, + LocaleOption, +} from './i18n.models'; +import { TRANSLATIONS } from './translations'; + +type TranslateParams = Record; + +/** + * Runtime i18n with signal-based locale and document a11y updates. + */ +@Injectable({ providedIn: 'root' }) +export class I18nService { + readonly #document = inject(DOCUMENT); + readonly #locale = signal(this.#readStoredLocale()); + readonly locale = this.#locale.asReadonly(); + readonly options = LOCALE_OPTIONS; + + constructor() { + effect(() => { + this.#applyDocumentLocale(this.#locale()); + }); + } + + /** Initializes locale from storage and syncs the document. */ + init(): void { + this.#applyDocumentLocale(this.#locale()); + } + + /** Returns translated string for the active locale. */ + t(key: string, params?: TranslateParams): string { + const dict = TRANSLATIONS[this.#locale()]; + const template = dict[key] ?? TRANSLATIONS[DEFAULT_LOCALE][key] ?? key; + return this.#interpolate(template, params); + } + + /** Changes active locale and persists the choice. */ + setLocale(code: LocaleCode): void { + this.#locale.set(code); + localStorage.setItem(LOCALE_STORAGE_KEY, code); + } + + /** Active locale metadata (html lang, Intl locale). */ + activeOption(): LocaleOption { + return ( + LOCALE_OPTIONS.find((o) => o.code === this.#locale()) ?? LOCALE_OPTIONS[0] + ); + } + + /** Formats amount in PLN for the active locale. */ + formatCurrency(value: number): string { + const { intlLocale } = this.activeOption(); + return new Intl.NumberFormat(intlLocale, { + style: 'currency', + currency: 'PLN', + }).format(value); + } + + /** Formats a decimal number for the active locale. */ + formatNumber(value: number, digits = '1.2-2'): string { + const { intlLocale } = this.activeOption(); + const [min, max] = this.#parseDigits(digits); + return new Intl.NumberFormat(intlLocale, { + minimumFractionDigits: min, + maximumFractionDigits: max, + }).format(value); + } + + #readStoredLocale(): LocaleCode { + const stored = localStorage.getItem(LOCALE_STORAGE_KEY); + if (stored === 'pl' || stored === 'en' || stored === 'de' || stored === 'cs') { + return stored; + } + return this.#localeFromBrowser(); + } + + #localeFromBrowser(): LocaleCode { + const lang = navigator.language.toLowerCase(); + if (lang.startsWith('de')) { + return 'de'; + } + if (lang.startsWith('cs') || lang.startsWith('cz')) { + return 'cs'; + } + if (lang.startsWith('en')) { + return 'en'; + } + return DEFAULT_LOCALE; + } + + #applyDocumentLocale(code: LocaleCode): void { + const option = LOCALE_OPTIONS.find((o) => o.code === code) ?? LOCALE_OPTIONS[0]; + this.#document.documentElement.lang = option.htmlLang; + this.#document.title = this.t('meta.pageTitle'); + } + + #interpolate(template: string, params?: TranslateParams): string { + if (!params) { + return template; + } + return Object.entries(params).reduce( + (text, [key, value]) => + text.replaceAll(`{{${key}}}`, String(value)), + template, + ); + } + + #parseDigits(digits: string): [number, number] { + const parts = digits.split('-'); + const min = Number(parts[0].split('.')[1] ?? 0); + const max = Number(parts[1] ?? parts[0].split('.')[1] ?? 2); + return [min, max]; + } +} diff --git a/src/shop/src/app/core/i18n/locale-switcher.component.ts b/src/shop/src/app/core/i18n/locale-switcher.component.ts new file mode 100644 index 0000000..ced6901 --- /dev/null +++ b/src/shop/src/app/core/i18n/locale-switcher.component.ts @@ -0,0 +1,86 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + HostListener, + inject, + signal, +} from '@angular/core'; +import { LocaleCode } from './i18n.models'; +import { I18nService } from './i18n.service'; +import { TranslatePipe } from './translate.pipe'; + +@Component({ + selector: 'app-locale-switcher', + standalone: true, + imports: [TranslatePipe], + template: ` +
+ + + @if (open()) { +
    + @for (option of i18n.options; track option.code) { +
  • + +
  • + } +
+ } +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LocaleSwitcherComponent { + readonly i18n = inject(I18nService); + readonly #host = inject(ElementRef); + readonly open = signal(false); + + @HostListener('document:click', ['$event']) + handleDocumentClick(event: MouseEvent): void { + if (!this.open()) { + return; + } + if (!this.#host.nativeElement.contains(event.target as Node)) { + this.open.set(false); + } + } + + @HostListener('document:keydown.escape') + handleEscape(): void { + this.open.set(false); + } + + handleToggle(): void { + this.open.update((value) => !value); + } + + handleSelect(code: LocaleCode): void { + this.i18n.setLocale(code); + this.open.set(false); + } +} diff --git a/src/shop/src/app/core/i18n/translate.pipe.ts b/src/shop/src/app/core/i18n/translate.pipe.ts new file mode 100644 index 0000000..f390a28 --- /dev/null +++ b/src/shop/src/app/core/i18n/translate.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform, inject } from '@angular/core'; +import { I18nService } from './i18n.service'; + +/** + * Translates keys for the active locale. Impure to react to locale changes. + */ +@Pipe({ name: 'translate', standalone: true, pure: false }) +export class TranslatePipe implements PipeTransform { + readonly #i18n = inject(I18nService); + + transform( + key: string, + params?: Record, + ): string { + this.#i18n.locale(); + return this.#i18n.t(key, params); + } +} diff --git a/src/shop/src/app/core/i18n/translations/cs.ts b/src/shop/src/app/core/i18n/translations/cs.ts new file mode 100644 index 0000000..60dd4ef --- /dev/null +++ b/src/shop/src/app/core/i18n/translations/cs.ts @@ -0,0 +1,361 @@ +import { TranslationMap } from '../i18n.models'; + +export const CS_TRANSLATIONS: TranslationMap = { + 'meta.pageTitle': 'iLED — Obchod s lampami', + 'meta.defaultDescription': + 'Moderní LED lampy. Ověřená řešení pro váš domov.', + 'meta.home.title': 'iLED — Obchod s lampami', + 'meta.home.description': + 'Moderní LED lampy. Ověřená řešení pro váš domov.', + 'meta.products.title': 'Produkty — iLED', + 'meta.products.description': 'Procházejte naši nabídku LED lamp.', + 'meta.cart.title': 'Košík — iLED', + 'meta.cart.description': 'Váš nákupní košík.', + 'meta.checkout.title': 'Pokladna — iLED', + 'meta.checkout.description': 'Dokončete objednávku.', + 'meta.order.title': 'Potvrzení objednávky — iLED', + 'meta.order.description': 'Děkujeme za objednávku.', + 'meta.login.title': 'Přihlášení — iLED', + 'meta.login.description': 'Přihlaste se nebo vytvořte účet.', + 'meta.favorites.title': 'Oblíbené — iLED', + 'meta.favorites.description': 'Vaše oblíbené produkty.', + 'meta.account.title': 'Účet — iLED', + 'meta.account.description': 'Váš účet a objednávky.', + 'meta.privacy.title': 'Ochrana soukromí — iLED', + 'meta.privacy.description': 'Jak zpracováváme osobní údaje.', + 'meta.terms.title': 'Obchodní podmínky — iLED', + 'meta.terms.description': 'Podmínky používání e-shopu iLED.', + 'meta.notFound.title': 'Stránka nenalezena — iLED', + 'meta.notFound.description': 'Požadovaná stránka neexistuje.', + 'notFound.title': 'Stránka nenalezena', + 'notFound.subtitle': 'Odkaz mohl vypršet nebo byla stránka přesunuta.', + 'notFound.home': 'Domů', + 'notFound.products': 'Prohlížet produkty', + 'footer.privacy': 'Ochrana soukromí', + 'cookies.title': 'Cookies a analytika', + 'cookies.body': + 'Používáme volitelnou analytiku (self-hosted OpenPanel). Monitoring chyb API (OpenObserve) nepoužívá cookies v prohlížeči.', + 'cookies.accept': 'Přijmout analytiku', + 'cookies.reject': 'Odmítnout', + 'common.loading': 'Načítání…', + 'common.saving': 'Ukládání…', + 'common.close': 'Zavřít', + 'common.clear': 'Vymazat', + 'a11y.paymentMethods': 'Platební metody', + 'a11y.productThumbs': 'Náhledy produktu', + 'footer.shipping': 'Doprava', + 'footer.aboutUs': 'O iLED', + 'footer.secureCheckout': 'Bezpečná platba PayU, BLIK a kartou.', + 'trust.aria': 'Proč nakupovat u nás', + 'trust.deliveryTitle': 'Doručení do 24 h', + 'trust.deliveryBody': 'Odesíláme v pracovní dny při dostupnosti.', + 'trust.freeShipTitle': 'Doprava zdarma', + 'trust.freeShipBody': 'Zdarma od {{amount}}', + 'trust.payTitle': 'Bezpečné platby', + 'trust.payBody': 'PayU — karta, BLIK a další.', + 'trust.returnsTitle': '14 dní na vrácení', + 'trust.returnsBody': 'Snadné vrácení pro spotřebitele.', + 'cart.freeShipRemaining': 'Přidejte ještě {{amount}} pro dopravu zdarma', + 'cart.freeShipUnlocked': 'Máte dopravu zdarma!', + 'cart.freeShipShopMore': 'Pokračovat v nákupu', + 'cart.emptyTitle': 'Košík je prázdný', + 'cart.emptyBody': 'Projděte kolekci a přidejte lampy, které se vám líbí.', + 'cart.emptyCta': 'Prohlížet produkty', + 'cart.deliveryAtCheckout': 'Vypočítáno v pokladně', + 'cart.discountCheckoutHint': 'Platné kódy uplatníme v pokladně.', + 'productCard.freeShipChip': 'Doprava zdarma', + 'productCard.removeFavorite': 'Odebrat z oblíbených', + 'products.outOfStock': 'Vyprodáno', + 'products.reviewsEmpty': 'Napište první recenzi této lampy.', + 'products.color.czarny': 'Černá', + 'products.color.drewno': 'Dřevo', + 'products.color.srebrny': 'Stříbrná', + 'products.color.złoty': 'Zlatá', + 'products.color.zloty': 'Zlatá', + 'products.clearFilters': 'Vymazat filtry', + 'products.showFilters': 'Filtry', + 'products.noResults': 'Žádné produkty neodpovídají filtrům.', + 'products.viewAll': 'Zobrazit vše', + 'home.viewAllBestsellers': 'Všechny bestsellery', + 'home.viewAllNew': 'Všechny novinky', + 'auth.registerTitle': 'Vytvořit účet', + 'auth.haveAccountTitle': 'Už máte účet?', + 'auth.haveAccountBody': + 'Přihlaste se pro objednávky, oblíbené a rychlejší pokladnu.', + 'auth.showPassword': 'Zobrazit heslo', + 'auth.hidePassword': 'Skrýt heslo', + 'auth.forgotPasswordMailto': 'Napište nám pro obnovení hesla', + 'auth.guestCheckout': 'Pokračovat jako host v pokladně', + 'auth.secureHint': 'Vaše data jsou chráněna a neprodáváme je.', + 'account.browseCta': 'Prohlížet produkty', + 'account.status': 'Stav', + 'favorites.browseCta': 'Objevit produkty', + 'meta.about.title': 'O nás — iLED', + 'meta.about.description': 'Poznejte iLED a naše osvětlení.', + 'meta.contact.title': 'Kontakt — iLED', + 'meta.contact.description': 'Spojte se s podporou iLED.', + 'meta.shipping.title': 'Doprava — iLED', + 'meta.shipping.description': 'Možnosti doručení, termíny a doprava zdarma.', + 'meta.returns.title': 'Vrácení — iLED', + 'meta.returns.description': 'Jak vrátit objednávku do 14 dnů.', + 'meta.faq.title': 'FAQ — iLED', + 'meta.faq.description': 'Odpovědi k objednávkám, dopravě a platbám.', + 'meta.cookies.title': 'Cookies — iLED', + 'meta.cookies.description': 'Jak používáme cookies a analytiku.', + 'legal.privacy.title': 'Zásady ochrany osobních údajů', + 'legal.privacy.body': + 'NÁVRH — k právní kontrole.\n\n1. Správce\niLED provozuje e-shop a zpracovává údaje potřebné k plnění objednávek.\n\n2. Účely\nObjednávky, účet, newsletter (se souhlasem), zlepšování obchodu.\n\n3. Analytika a monitoring\nSe souhlasem: self-hosted OpenPanel. API: OpenObserve (jen na serveru).\n\n4. Práva\nPřístup, oprava, výmaz, stížnost u dozorového úřadu.\n\n5. Kontakt\nKontaktní údaje na webu obchodu.', + 'legal.terms.title': 'Obchodní podmínky', + 'legal.terms.body': + 'NÁVRH — k právní kontrole.\n\n1. Rozsah\nTyto podmínky upravují nákupy v e-shopu iLED.\n\n2. Objednávky\nObjednávka je nabídkou; smlouva vzniká potvrzením.\n\n3. Ceny a platba\nCeny v PLN včetně DPH, metody platby v pokladně.\n\n4. Doručení\nPřes InPost, DHL nebo DPD na adresu nebo výdejní místo.\n\n5. Odstoupení\nSpotřebitel: 14 dní, zákonné výjimky vyhrazeny.\n\n6. Reklamace\nDle platného spotřebitelského práva.', + 'content.about.title': 'O iLED', + 'content.about.body': + 'iLED je specializovaný obchod s moderním LED osvětlením pro domov i kancelář.\n\nVybíráme produkty podle úspory energie, spolehlivých driverů a designu. Objednávky pečlivě balíme a odesíláme z Polska.\n\nPomůžeme vybrat správné světlo před nákupem i po něm.', + 'content.contact.title': 'Kontakt', + 'content.contact.body': + 'Dotazy k produktům, objednávkám nebo vrácení? Rádi pomůžeme.\n\nPište na kontakt@iled.pl — obvykle odpovídáme do 2 pracovních dnů (po–pá, mimo polské svátky).\n\nUveďte číslo objednávky, pokud jde o konkrétní nákup.', + 'content.shipping.title': 'Doprava', + 'content.shipping.body': + 'Doručujeme v Polsku přes InPost (boxy a kurýr), DHL a DPD.\n\nSkladové objednávky v pracovní dny obvykle odesíláme do 24 hodin. Čas doručení závisí na dopravci a volbě v pokladně.\n\nDoprava zdarma od 499,99 PLN (zboží před dopravou). Po odeslání pošleme e-mail s trackováním.', + 'content.returns.title': 'Vrácení a odstoupení', + 'content.returns.body': + 'Spotřebitel může odstoupit do 14 dnů od převzetí (s výjimkami dle zákona).\n\nNapište na kontakt@iled.pl s číslem objednávky. Zboží bezpečně zabalte a odešlete na uvedenou adresu.\n\nPeníze vrátíme po kontrole zásilky stejnou platební metodou.', + 'content.cookies.title': 'Cookies a analytika', + 'content.cookies.body': + 'Nezbytné cookies pro košík, pokladnu a jazyk.\n\nVolitelně self-hosted OpenPanel se souhlasem v banneru. OpenObserve na serveru bez sledovacích cookies v prohlížeči.', + 'content.faq.title': 'Časté dotazy', + 'content.faq.body': + 'Krátké odpovědi o dopravě, platbách, vrácení a výdejních místech. Více na stránce Kontakt.', + 'content.faq.q1': 'Jak rychle dorazí objednávka?', + 'content.faq.a1': + 'Skladové objednávky v pracovní dny obvykle odesíláme do 24 hodin. Termín závisí na dopravci a zvolené možnosti.', + 'content.faq.q2': 'Jaké platební metody nabízíte?', + 'content.faq.a2': + 'PayU (karta, BLIK aj.), bankovní převod a dobírka, pokud je k dispozici — online platby přes zabezpečené PayU.', + 'content.faq.q3': 'Od kdy je doprava zdarma?', + 'content.faq.a3': + 'Zdarma od 499,99 PLN hodnoty zboží před dopravou; košík ukazuje zbývající částku.', + 'content.faq.q4': 'Jak vrátit produkt?', + 'content.faq.a4': + 'Spotřebitel má 14 dnů na odstoupení. E-mail na kontakt@iled.pl s číslem objednávky — pošleme instrukce a adresu.', + 'content.faq.q5': 'Jaká je záruka na lampy?', + 'content.faq.a5': + 'Zákonná odpovědnost za vady a případně záruka výrobce. Při vadě pošlete fotky a číslo objednávky.', + 'content.faq.q6': 'Jak vybrat výdejní místo?', + 'content.faq.a6': + 'V pokladně zvolte InPost, DHL nebo DPD, vyhledejte místo na mapě a klikněte. Fakturační adresu zadejte zvlášť.', + 'content.faq.q7': 'Mohu změnit nebo zrušit objednávku?', + 'content.faq.a7': + 'Před odesláním napište co nejdříve na kontakt@iled.pl. Po odeslání lze využít 14denní odstoupení po doručení.', + 'content.faq.q8': 'Doručujete mimo Polsko?', + 'content.faq.a8': + 'Pokladna je pro Polsko. Pro jinou zemi nás kontaktujte před nákupem — potvrdíme možnost, cenu a termín.', + 'checkout.billingInvoiceHint': + 'Při vyzvednutí zadejte fakturační adresu (ne adresu boxu).', + 'newsletter.error': 'Odběr se nepodařilo dokončit. Zkuste to znovu.', + 'newsletter.submitting': 'Odesílání…', + 'a11y.skipToContent': 'Přejít na obsah', + + 'a11y.mainNav': 'Hlavní navigace', + 'a11y.search': 'Hledat produkty', + 'a11y.langSwitcher': 'Vyberte jazyk', + 'a11y.decreaseQty': 'Snížit množství', + 'a11y.increaseQty': 'Zvýšit množství', + 'a11y.mainContent': 'Hlavní obsah', + 'a11y.promoBanner': 'Propagační banner', + 'nav.home': 'DOMŮ', + 'nav.bestsellers': 'NEJPRODÁVANĚJŠÍ', + 'nav.newArrivals': 'NOVINKY', + 'nav.searchPlaceholder': 'Hledat...', + 'nav.favorites': 'Oblíbené', + 'nav.cart': 'Košík', + 'nav.account': 'Účet', + 'footer.tagline': 'Moderní lampy. Ověřená řešení.', + 'footer.customerService': 'Zákaznický servis', + 'footer.returnsPolicy': 'Podmínky vrácení', + 'footer.about': 'O společnosti', + 'footer.contact': 'Kontakt', + 'footer.terms': 'Obchodní podmínky', + 'footer.cookies': 'Soubory cookies', + 'footer.advice': 'Poradíme', + 'footer.faq': 'Časté dotazy', + 'footer.copyright': 'Copyright 2026 © iLED', + 'newsletter.headline': 'Světlo, které dělá rozdíl.', + 'newsletter.subtitle': 'Novinky a nabídky pouze pro odběratele.', + 'newsletter.emailPlaceholder': 'Váš e-mail', + 'newsletter.privacyNotice': + "Kliknutím na 'přihlásit' souhlasíte se zásadami ochrany osobních údajů.", + 'newsletter.submit': 'Přihlásit se', + 'newsletter.success': 'Děkujeme za přihlášení!', + 'home.heroTitle': 'Světlo, které dělá rozdíl', + 'home.heroSubtitle': + 'Moderní LED lampy. Ověřená řešení pro váš domov.', + 'home.cta': 'Zobrazit produkty', + 'home.bestsellers': 'Nejprodávanější', + 'home.newArrivals': 'Novinky', + 'products.filters': 'Filtry', + 'products.priceLabel': 'Cena (PLN)', + 'products.colorLabel': 'Barva', + 'products.applyFilters': 'Použít', + 'products.title': 'Produkty ({{count}})', + 'products.bestsellerBadge': 'Bestseller', + 'products.lowestPrice30Days': 'Nejnižší cena za posledních 30 dní:', + 'products.addToCart': 'Přidat do košíku', + 'products.quantity': 'Množství', + 'products.inStock': 'Skladem', + 'products.deliveryTime': 'Doba dodání {{hours}} h', + 'products.freeDeliveryFrom': 'Doprava zdarma od 499,99 PLN', + 'products.description': 'Popis produktu', + 'products.specs': 'Technické údaje', + 'products.related': 'Produkty ve stejné kategorii', + 'products.reviews': 'Recenze', + 'products.reviewsCount': '({{count}} recenzí)', + 'products.yourRating': 'Vaše hodnocení', + 'products.addReview': 'Přidat recenzi', + 'productCard.favorite': 'Přidat do oblíbených', + 'common.firstName': 'Jméno', + 'common.lastName': 'Příjmení', + 'common.email': 'E-mail', + 'common.password': 'Heslo', + 'common.content': 'Text', + 'common.street': 'Ulice', + 'common.city': 'Město', + 'common.postalCode': 'PSČ', + 'common.phone': 'Telefon', + 'common.discountCode': 'Slevový kód', + 'common.discountCodeOptional': 'Slevový kód (volitelné)', + 'common.code': 'Kód', + 'common.remove': 'Odebrat', + 'cart.title': 'Košík', + 'cart.addedTitle': 'Přidáno do košíku', + 'cart.addedMessage': '{{name}} × {{qty}} bylo přidáno do košíku', + 'cart.goToCart': 'Přejít do košíku', + 'cart.continueShopping': 'Pokračovat v nákupu', + 'cart.summary': 'Celkem ({{count}} položek) {{amount}}', + 'cart.deliveryTime24h': 'Doba dodání 24 h', + 'cart.checkout': 'Přejít k pokladně', + 'cart.confirmDiscount': 'Potvrdit', + 'cart.itemCount': 'Počet produktů:', + 'cart.deliveryCost': 'Cena dopravy:', + 'cart.total': 'Celkem:', + 'cart.vatNote': 'Cena včetně 23 % DPH', + 'cart.recommended': 'Doporučené produkty', + 'cart.discount': 'Sleva', + 'cart.discountApplied': 'Kód použit!', + 'cart.discountInvalid': 'Neplatný kód', + 'promo.error.Invalid': 'Neplatný kód.', + 'promo.error.Inactive': 'Tento kód není aktivní.', + 'promo.error.NotYetActive': 'Tento kód ještě není aktivní.', + 'promo.error.Expired': 'Tento kód vypršel nebo byl vyčerpán.', + 'promo.error.MinOrder': 'Minimální hodnota objednávky: {{amount}}.', + 'promo.error.GlobalCap': 'Limit použití této akce byl vyčerpán.', + 'promo.error.CustomerCap': 'Tuto akci jste již použili.', + 'promo.error.WrongEmail': 'Tento kód je přiřazen k jinému e-mailu.', + 'cart.updateError': 'Košík se nepodařilo aktualizovat. Zkuste to znovu.', + 'checkout.submitting': 'Odesílání objednávky…', + 'checkout.title': 'Pokladna', + 'checkout.subtitle': 'Vyplňte údaje a zvolte doručení a platbu.', + 'checkout.contactDetails': 'Kontaktní údaje', + 'checkout.shippingAddress': 'Dodací adresa', + 'checkout.billingAddress': 'Fakturační adresa', + 'checkout.billingSameAsShipping': 'Stejná jako dodací adresa', + 'checkout.deliveryMethod': 'Způsob doručení', + 'checkout.paymentMethod': 'Způsob platby', + 'checkout.orderSummary': 'Souhrn objednávky', + 'checkout.notes': 'Poznámka k objednávce', + 'checkout.notesPlaceholder': 'Např. kód domu, preferovaný čas doručení…', + 'checkout.codFee': 'Poplatek za dobírku:', + 'checkout.placeOrder': 'Odeslat objednávku', + 'checkout.emptyCart': 'Košík je prázdný.', + 'checkout.amountDue': 'K úhradě: {{amount}}', + 'checkout.payWithPayU': 'Zaplatit přes PayU', + 'checkout.orderError': 'Objednávku se nepodařilo vytvořit.', + 'checkout.thankYou': 'Děkujeme za objednávku!', + 'checkout.orderNumber': 'Číslo objednávky:', + 'checkout.status': 'Stav:', + 'checkout.status.PendingPayment': 'Čeká na platbu', + 'checkout.status.Paid': 'Zaplaceno', + 'checkout.status.Processing': 'Zpracovává se', + 'checkout.status.Shipped': 'Odesláno', + 'checkout.status.Delivered': 'Doručeno', + 'checkout.status.Cancelled': 'Zrušeno', + 'checkout.payuSandbox.badge': 'PayU sandbox', + 'checkout.payuSandbox.title': 'Simulace platby PayU', + 'checkout.payuSandbox.subtitle': + 'Nejde o skutečnou platbu. Potvrďte, aby byla objednávka označena jako zaplacená.', + 'checkout.payuSandbox.pay': 'Zaplatit (sandbox)', + 'checkout.payuSandbox.paying': 'Zpracování…', + 'checkout.payuSandbox.cancel': 'Zrušit', + 'checkout.backToShop': 'Zpět do obchodu', + 'checkout.country.PL': 'Polsko', + 'checkout.country.DE': 'Německo', + 'checkout.country.CZ': 'Česko', + 'checkout.country.SK': 'Slovensko', + 'checkout.carrier': 'Dopravce', + 'checkout.parcelPoint': 'Výdejní místo', + 'checkout.parcelPoint.InPost': 'InPost výdejní box', + 'checkout.parcelPoint.Dhl': 'DHL výdejní místo', + 'checkout.parcelPoint.Dpd': 'DPD výdejní místo', + 'checkout.parcelSearchPlaceholder': 'Město nebo PSČ…', + 'checkout.searchParcelPoints': 'Hledat', + 'checkout.parcelPointRequired': 'Vyberte výdejní místo.', + 'checkout.parcelSearchError': + 'Výdejní místa se nepodařilo načíst. Zkuste to znovu.', + 'checkout.parcelMapHint': + 'Klikněte na bod na mapě nebo vyberte ze seznamu níže.', + 'checkout.selectedPoint': 'Vybrané místo', + 'checkout.choosePoint': 'Vyberte výdejní místo', + 'checkout.changePoint': 'Změnit', + 'checkout.closePicker': 'Zavřít', + 'checkout.nearMe': 'V mé blízkosti', + 'checkout.geoNotSupported': 'Geolokace není v tomto prohlížeči podporována.', + 'checkout.geoDenied': 'Přístup k poloze byl odepřen. Hledejte podle města nebo PSČ.', + 'checkout.loadingPoints': 'Načítání výdejních míst…', + 'checkout.loadingMap': 'Načítání mapy…', + 'checkout.openingHours': 'Otevírací doba', + 'checkout.supportsCod': 'Dobírka', + 'checkout.sandboxMapHint': + 'Ukázková mapa s vzorovými body. Pro živé mapy nakonfigurujte widgety dopravců v appsettings.', + 'checkout.parcelNoResults': + 'Nebyla nalezena žádná výdejní místa. Zkuste jiné vyhledávání.', + 'checkout.service.ParcelLocker': 'Výdejní místo', + 'checkout.service.ParcelLocker.InPost': 'Výdejní box', + 'checkout.service.ParcelLocker.Dhl': 'DHL výdejní místo', + 'checkout.service.ParcelLocker.Dpd': 'DPD výdejní místo', + 'checkout.service.Courier': 'Kurýr', + 'checkout.carrier.InPost': 'InPost', + 'checkout.carrier.Dhl': 'DHL', + 'checkout.carrier.Dpd': 'DPD', + 'checkout.carrierDesc.InPost': 'InPost box nebo kurýr.', + 'checkout.carrierDesc.Dhl': 'DHL výdejní místo nebo kurýr.', + 'checkout.carrierDesc.Dpd': 'DPD výdejní místo nebo kurýr.', + 'checkout.payment.PayU': 'PayU (karta, BLIK)', + 'checkout.payment.BankTransfer': 'Bankovní převod', + 'checkout.payment.CashOnDelivery': 'Dobírka', + 'checkout.paymentDesc.PayU': 'Bezpečná online platba — přesměrování na PayU.', + 'checkout.paymentDesc.BankTransfer': 'Údaje k převodu obdržíte e-mailem po objednávce.', + 'checkout.paymentDesc.CashOnDelivery': 'Platba kurýrovi při doručení (+ poplatek za dobírku).', + 'checkout.paymentHint.BankTransfer': + 'Převeďte částku podle údajů v potvrzovacím e-mailu.', + 'checkout.paymentHint.CashOnDelivery': + 'Mějte připravenou hotovost při převzetí zásilky.', + 'auth.loginTitle': 'Přihlášení', + 'auth.loginPlaceholder': 'E-mail', + 'auth.login': 'Přihlásit se', + 'auth.register': 'Vytvořit účet', + 'auth.forgotPassword': 'Zapomněl jsem login nebo heslo', + 'auth.noAccountTitle': 'Nemáte účet?', + 'auth.noAccountBody': + 'Vytvořte si účet během chvilky a využívejte historii objednávek a oblíbené.', + 'auth.haveAccount': 'Mám účet', + 'auth.registerError': 'Registrace se nezdařila.', + 'auth.loginError': 'Neplatný e-mail nebo heslo.', + 'account.welcome': 'Vítejte, {{name}}!', + 'account.logout': 'Odhlásit se', + 'account.myOrders': 'Moje objednávky', + 'account.details': 'Detail', + 'account.noOrders': 'Žádné objednávky.', + 'account.loginPrompt': ' pro zobrazení účtu.', + 'favorites.title': 'Oblíbené produkty', + 'favorites.empty': 'Žádné oblíbené produkty.', +}; diff --git a/src/shop/src/app/core/i18n/translations/de.ts b/src/shop/src/app/core/i18n/translations/de.ts new file mode 100644 index 0000000..7bd31b4 --- /dev/null +++ b/src/shop/src/app/core/i18n/translations/de.ts @@ -0,0 +1,361 @@ +import { TranslationMap } from '../i18n.models'; + +export const DE_TRANSLATIONS: TranslationMap = { + 'meta.pageTitle': 'iLED — Lampenshop', + 'meta.defaultDescription': + 'Moderne LED-Lampen. Bewährte Lösungen für Ihr Zuhause.', + 'meta.home.title': 'iLED — Lampenshop', + 'meta.home.description': + 'Moderne LED-Lampen. Bewährte Lösungen für Ihr Zuhause.', + 'meta.products.title': 'Produkte — iLED', + 'meta.products.description': 'Durchsuchen Sie unsere LED-Lampen.', + 'meta.cart.title': 'Warenkorb — iLED', + 'meta.cart.description': 'Ihr Warenkorb.', + 'meta.checkout.title': 'Kasse — iLED', + 'meta.checkout.description': 'Bestellung abschließen.', + 'meta.order.title': 'Bestellbestätigung — iLED', + 'meta.order.description': 'Vielen Dank für Ihre Bestellung.', + 'meta.login.title': 'Anmelden — iLED', + 'meta.login.description': 'Anmelden oder Konto erstellen.', + 'meta.favorites.title': 'Favoriten — iLED', + 'meta.favorites.description': 'Ihre Lieblingsprodukte.', + 'meta.account.title': 'Konto — iLED', + 'meta.account.description': 'Ihr Konto und Bestellungen.', + 'meta.privacy.title': 'Datenschutz — iLED', + 'meta.privacy.description': 'Wie wir personenbezogene Daten verarbeiten.', + 'meta.terms.title': 'AGB — iLED', + 'meta.terms.description': 'Nutzungsbedingungen des iLED-Shops.', + 'meta.notFound.title': 'Seite nicht gefunden — iLED', + 'meta.notFound.description': 'Die angeforderte Seite existiert nicht.', + 'notFound.title': 'Seite nicht gefunden', + 'notFound.subtitle': 'Der Link ist veraltet oder die Seite wurde verschoben.', + 'notFound.home': 'Startseite', + 'notFound.products': 'Produkte ansehen', + 'footer.privacy': 'Datenschutz', + 'cookies.title': 'Cookies & Analyse', + 'cookies.body': + 'Wir nutzen optionale Analyse (selbst gehostetes OpenPanel). API-Fehlerüberwachung (OpenObserve) verwendet keine Browser-Cookies.', + 'cookies.accept': 'Analyse akzeptieren', + 'cookies.reject': 'Ablehnen', + 'common.loading': 'Laden…', + 'common.saving': 'Speichern…', + 'common.close': 'Schließen', + 'common.clear': 'Löschen', + 'a11y.paymentMethods': 'Zahlungsmethoden', + 'a11y.productThumbs': 'Produktminiaturen', + 'footer.shipping': 'Versand', + 'footer.aboutUs': 'Über iLED', + 'footer.secureCheckout': 'Sicher bezahlen mit PayU, BLIK und Karte.', + 'trust.aria': 'Warum bei uns einkaufen', + 'trust.deliveryTitle': 'Lieferung in bis zu 24 Std.', + 'trust.deliveryBody': 'Versand an Werktagen bei Verfügbarkeit.', + 'trust.freeShipTitle': 'Kostenlose Lieferung', + 'trust.freeShipBody': 'Gratis ab {{amount}}', + 'trust.payTitle': 'Sichere Zahlungen', + 'trust.payBody': 'PayU — Karte, BLIK und mehr.', + 'trust.returnsTitle': '14 Tage Rückgabe', + 'trust.returnsBody': 'Einfache Rückgabe für Verbraucher.', + 'cart.freeShipRemaining': 'Noch {{amount}} bis zur kostenlosen Lieferung', + 'cart.freeShipUnlocked': 'Kostenlose Lieferung freigeschaltet!', + 'cart.freeShipShopMore': 'Weiter einkaufen', + 'cart.emptyTitle': 'Ihr Warenkorb ist leer', + 'cart.emptyBody': 'Entdecken Sie unsere Lampen und legen Sie Favoriten hinein.', + 'cart.emptyCta': 'Produkte ansehen', + 'cart.deliveryAtCheckout': 'Wird an der Kasse berechnet', + 'cart.discountCheckoutHint': 'Gültige Codes werden an der Kasse angewendet.', + 'productCard.freeShipChip': 'Kostenlose Lieferung', + 'productCard.removeFavorite': 'Aus Favoriten entfernen', + 'products.outOfStock': 'Nicht auf Lager', + 'products.reviewsEmpty': 'Schreiben Sie die erste Bewertung für diese Lampe.', + 'products.color.czarny': 'Schwarz', + 'products.color.drewno': 'Holz', + 'products.color.srebrny': 'Silber', + 'products.color.złoty': 'Gold', + 'products.color.zloty': 'Gold', + 'products.clearFilters': 'Filter zurücksetzen', + 'products.showFilters': 'Filter', + 'products.noResults': 'Keine Produkte für diese Filter.', + 'products.viewAll': 'Alle ansehen', + 'home.viewAllBestsellers': 'Alle Bestseller', + 'home.viewAllNew': 'Alle Neuheiten', + 'auth.registerTitle': 'Konto erstellen', + 'auth.haveAccountTitle': 'Sie haben bereits ein Konto?', + 'auth.haveAccountBody': + 'Melden Sie sich an für Bestellungen, Favoriten und schnellere Kasse.', + 'auth.showPassword': 'Passwort anzeigen', + 'auth.hidePassword': 'Passwort verbergen', + 'auth.forgotPasswordMailto': 'Schreiben Sie uns zur Passwort-Zurücksetzung', + 'auth.guestCheckout': 'Als Gast in der Kasse fortfahren', + 'auth.secureHint': 'Ihre Daten sind geschützt und werden nicht verkauft.', + 'account.browseCta': 'Produkte ansehen', + 'account.status': 'Status', + 'favorites.browseCta': 'Produkte entdecken', + 'meta.about.title': 'Über uns — iLED', + 'meta.about.description': 'Erfahren Sie mehr über iLED und unsere Beleuchtung.', + 'meta.contact.title': 'Kontakt — iLED', + 'meta.contact.description': 'Kontakt zum iLED-Kundenservice.', + 'meta.shipping.title': 'Versand — iLED', + 'meta.shipping.description': 'Lieferoptionen, Fristen und kostenloser Versand.', + 'meta.returns.title': 'Rückgabe — iLED', + 'meta.returns.description': 'So geben Sie eine Bestellung innerhalb von 14 Tagen zurück.', + 'meta.faq.title': 'FAQ — iLED', + 'meta.faq.description': 'Antworten zu Bestellungen, Lieferung und Zahlung.', + 'meta.cookies.title': 'Cookies — iLED', + 'meta.cookies.description': 'Wie wir Cookies und Analyse nutzen.', + 'legal.privacy.title': 'Datenschutzerklärung', + 'legal.privacy.body': + 'ENTWURF — rechtliche Prüfung ausstehend.\n\n1. Verantwortlicher\niLED betreibt diesen Online-Shop und verarbeitet Daten zur Auftragserfüllung.\n\n2. Zwecke\nBestellungen, Konto, Newsletter (mit Einwilligung), Shop-Verbesserung.\n\n3. Analyse & Monitoring\nMit Einwilligung: selbst gehostetes OpenPanel. API: OpenObserve (serverseitig).\n\n4. Rechte\nAuskunft, Berichtigung, Löschung, Beschwerde bei der Aufsichtsbehörde.\n\n5. Kontakt\nKontaktdaten auf der Website.', + 'legal.terms.title': 'Allgemeine Geschäftsbedingungen', + 'legal.terms.body': + 'ENTWURF — rechtliche Prüfung ausstehend.\n\n1. Geltungsbereich\nDiese AGB gelten für Käufe im iLED-Shop.\n\n2. Bestellungen\nDie Bestellung ist ein Angebot; Vertragsschluss mit Bestätigung.\n\n3. Preise & Zahlung\nPreise in PLN inkl. MwSt., Zahlungsmethoden an der Kasse.\n\n4. Lieferung\nÜber InPost, DHL oder DPD an Adresse oder Abholpunkt.\n\n5. Widerruf\nVerbraucher: 14 Tage, gesetzliche Ausnahmen vorbehalten.\n\n6. Mängel\nNach geltendem Verbraucherschutzrecht.', + 'content.about.title': 'Über iLED', + 'content.about.body': + 'iLED ist ein Fachshop für moderne LED-Beleuchtung für Zuhause und Büro.\n\nWir wählen Produkte nach Effizienz, zuverlässigen Treibern und Design. Jede Bestellung verpacken wir sorgfältig und versenden aus Polen.\n\nOb Wohnzimmer oder Büro — wir helfen bei der passenden Lichtlösung vor und nach dem Kauf.', + 'content.contact.title': 'Kontakt', + 'content.contact.body': + 'Fragen zu Produkten, Bestellungen oder Rückgaben? Wir helfen gern.\n\nSchreiben Sie an kontakt@iled.pl — in der Regel antworten wir innerhalb von 2 Werktagen (Mo–Fr, außer polnische Feiertage).\n\nBitte nennen Sie Ihre Bestellnummer, wenn es um einen konkreten Kauf geht.', + 'content.shipping.title': 'Versand & Lieferung', + 'content.shipping.body': + 'Wir liefern in Polen mit InPost (Paketstation & Kurier), DHL und DPD.\n\nBestellungen an Werktagen mit Lagerbestand versenden wir meist innerhalb von 24 Stunden. Die Lieferzeit hängt von Träger und Option in der Kasse ab.\n\nKostenloser Versand ab 499,99 PLN Warenwert (vor Versand). Tracking erhalten Sie per E-Mail nach Versand.', + 'content.returns.title': 'Rückgabe & Widerruf', + 'content.returns.body': + 'Verbraucher können innerhalb von 14 Tagen nach Erhalt widerrufen (gesetzliche Ausnahmen vorbehalten).\n\nKontakt: kontakt@iled.pl mit Bestellnummer. Produkt sicher verpacken und an die mitgeteilte Adresse senden.\n\nErstattung nach Prüfung der Rücksendung, über die ursprüngliche Zahlungsart.', + 'content.cookies.title': 'Cookies & Analyse', + 'content.cookies.body': + 'Notwendige Cookies für Warenkorb, Kasse und Sprache.\n\nOptional: self-hosted OpenPanel mit Ihrer Einwilligung im Cookie-Banner. Serverseitiges OpenObserve ohne Browser-Tracking-Cookies.', + 'content.faq.title': 'Häufige Fragen', + 'content.faq.body': + 'Kurze Antworten zu Lieferung, Zahlung, Rückgabe und Abholpunkten. Mehr Hilfe auf der Kontaktseite.', + 'content.faq.q1': 'Wie schnell kommt meine Bestellung?', + 'content.faq.a1': + 'Bei Lagerbestand Versand meist innerhalb von 24 Werktagsstunden. Die Zustellzeit hängt von Träger und gewählter Option ab.', + 'content.faq.q2': 'Welche Zahlungsarten gibt es?', + 'content.faq.a2': + 'PayU (Karte, BLIK u. a.), Überweisung und Nachnahme, wenn verfügbar — sichere PayU-Seiten für Online-Zahlungen.', + 'content.faq.q3': 'Ab wann ist Versand kostenlos?', + 'content.faq.a3': + 'Kostenlos ab 499,99 PLN Warenwert vor Versand; der Warenkorb zeigt den Fortschritt.', + 'content.faq.q4': 'Wie gebe ich einen Artikel zurück?', + 'content.faq.a4': + '14 Tage Widerruf für Verbraucher. E-Mail an kontakt@iled.pl mit Bestellnummer für Anleitung und Rücksendeadresse.', + 'content.faq.q5': 'Welche Garantie gilt?', + 'content.faq.a5': + 'Gesetzliche Gewährleistung und ggf. Herstellergarantie. Bei Mängeln Fotos und Bestellnummer senden — wir leiten Reparatur, Ersatz oder Erstattung ein.', + 'content.faq.q6': 'Wie wähle ich einen Abholpunkt?', + 'content.faq.a6': + 'InPost, DHL oder DPD in der Kasse wählen, auf der Karte suchen und Punkt anklicken. Rechnungsadresse separat angeben.', + 'content.faq.q7': 'Bestellung ändern oder stornieren?', + 'content.faq.a7': + 'Vor Versand schnell kontakt@iled.pl schreiben. Nach Versand ggf. 14-Tage-Widerruf nach Lieferung.', + 'content.faq.q8': 'Lieferung außerhalb Polens?', + 'content.faq.a8': + 'Kasse für Polen optimiert. Für andere Länder vor Bestellung anfragen — wir nennen Kosten und Termin.', + 'checkout.billingInvoiceHint': + 'Bei Abholung geben Sie Ihre Rechnungsadresse an (nicht die Packstation).', + 'newsletter.error': 'Anmeldung fehlgeschlagen. Bitte erneut versuchen.', + 'newsletter.submitting': 'Wird angemeldet…', + 'a11y.skipToContent': 'Zum Inhalt springen', + + 'a11y.mainNav': 'Hauptnavigation', + 'a11y.search': 'Produkte suchen', + 'a11y.langSwitcher': 'Sprache wählen', + 'a11y.decreaseQty': 'Menge verringern', + 'a11y.increaseQty': 'Menge erhöhen', + 'a11y.mainContent': 'Hauptinhalt', + 'a11y.promoBanner': 'Werbebanner', + 'nav.home': 'STARTSEITE', + 'nav.bestsellers': 'BESTSELLER', + 'nav.newArrivals': 'NEUHEITEN', + 'nav.searchPlaceholder': 'Suchen...', + 'nav.favorites': 'Favoriten', + 'nav.cart': 'Warenkorb', + 'nav.account': 'Konto', + 'footer.tagline': 'Moderne Lampen. Bewährte Lösungen.', + 'footer.customerService': 'Kundenservice', + 'footer.returnsPolicy': 'Rückgaberichtlinie', + 'footer.about': 'Über uns', + 'footer.contact': 'Kontakt', + 'footer.terms': 'AGB', + 'footer.cookies': 'Cookies', + 'footer.advice': 'Beratung', + 'footer.faq': 'Häufige Fragen', + 'footer.copyright': 'Copyright 2026 © iLED', + 'newsletter.headline': 'Licht, das den Unterschied macht.', + 'newsletter.subtitle': 'Neuheiten und Angebote nur für Abonnenten.', + 'newsletter.emailPlaceholder': 'Ihre E-Mail-Adresse', + 'newsletter.privacyNotice': + "Mit Klick auf 'Anmelden' akzeptieren Sie unsere Datenschutzerklärung.", + 'newsletter.submit': 'Anmelden', + 'newsletter.success': 'Vielen Dank für Ihre Anmeldung!', + 'home.heroTitle': 'Licht, das den Unterschied macht', + 'home.heroSubtitle': + 'Moderne LED-Lampen. Bewährte Lösungen für Ihr Zuhause.', + 'home.cta': 'Produkte ansehen', + 'home.bestsellers': 'Bestseller', + 'home.newArrivals': 'Neuheiten', + 'products.filters': 'Filter', + 'products.priceLabel': 'Preis (PLN)', + 'products.colorLabel': 'Farbe', + 'products.applyFilters': 'Anwenden', + 'products.title': 'Produkte ({{count}})', + 'products.bestsellerBadge': 'Bestseller', + 'products.lowestPrice30Days': 'Niedrigster Preis in den letzten 30 Tagen:', + 'products.addToCart': 'In den Warenkorb', + 'products.quantity': 'Menge', + 'products.inStock': 'Auf Lager', + 'products.deliveryTime': 'Lieferzeit {{hours}} Std.', + 'products.freeDeliveryFrom': 'Kostenlose Lieferung ab 499,99 PLN', + 'products.description': 'Produktbeschreibung', + 'products.specs': 'Technische Daten', + 'products.related': 'Produkte in derselben Kategorie', + 'products.reviews': 'Bewertungen', + 'products.reviewsCount': '({{count}} Bewertungen)', + 'products.yourRating': 'Ihre Bewertung', + 'products.addReview': 'Bewertung hinzufügen', + 'productCard.favorite': 'Zu Favoriten hinzufügen', + 'common.firstName': 'Vorname', + 'common.lastName': 'Nachname', + 'common.email': 'E-Mail', + 'common.password': 'Passwort', + 'common.content': 'Inhalt', + 'common.street': 'Straße', + 'common.city': 'Stadt', + 'common.postalCode': 'Postleitzahl', + 'common.phone': 'Telefon', + 'common.discountCode': 'Rabattcode', + 'common.discountCodeOptional': 'Rabattcode (optional)', + 'common.code': 'Code', + 'common.remove': 'Entfernen', + 'cart.title': 'Warenkorb', + 'cart.addedTitle': 'Zum Warenkorb hinzugefügt', + 'cart.addedMessage': '{{name}} × {{qty}} wurde in den Warenkorb gelegt', + 'cart.goToCart': 'Zum Warenkorb', + 'cart.continueShopping': 'Weiter einkaufen', + 'cart.summary': 'Gesamt ({{count}} Artikel) {{amount}}', + 'cart.deliveryTime24h': 'Lieferzeit 24 Std.', + 'cart.checkout': 'Zur Kasse', + 'cart.confirmDiscount': 'Bestätigen', + 'cart.itemCount': 'Artikelanzahl:', + 'cart.deliveryCost': 'Versandkosten:', + 'cart.total': 'Gesamt:', + 'cart.vatNote': 'Preis inkl. 23 % MwSt.', + 'cart.recommended': 'Empfohlene Produkte', + 'cart.discount': 'Rabatt', + 'cart.discountApplied': 'Code angewendet!', + 'cart.discountInvalid': 'Ungültiger Code', + 'promo.error.Invalid': 'Ungültiger Code.', + 'promo.error.Inactive': 'Dieser Code ist inaktiv.', + 'promo.error.NotYetActive': 'Dieser Code ist noch nicht aktiv.', + 'promo.error.Expired': 'Dieser Code ist abgelaufen oder aufgebraucht.', + 'promo.error.MinOrder': 'Mindestbestellwert: {{amount}}.', + 'promo.error.GlobalCap': 'Das Nutzungslimit dieser Aktion ist erreicht.', + 'promo.error.CustomerCap': 'Sie haben diese Aktion bereits genutzt.', + 'promo.error.WrongEmail': 'Dieser Code ist einer anderen E-Mail zugeordnet.', + 'cart.updateError': 'Warenkorb konnte nicht aktualisiert werden. Bitte erneut versuchen.', + 'checkout.submitting': 'Bestellung wird aufgegeben…', + 'checkout.title': 'Kasse', + 'checkout.subtitle': 'Daten ergänzen und Lieferung sowie Zahlung wählen.', + 'checkout.contactDetails': 'Kontaktdaten', + 'checkout.shippingAddress': 'Lieferadresse', + 'checkout.billingAddress': 'Rechnungsadresse', + 'checkout.billingSameAsShipping': 'Gleich wie Lieferadresse', + 'checkout.deliveryMethod': 'Lieferart', + 'checkout.paymentMethod': 'Zahlungsart', + 'checkout.orderSummary': 'Bestellübersicht', + 'checkout.notes': 'Anmerkungen zur Bestellung', + 'checkout.notesPlaceholder': 'z. B. Türcode, bevorzugte Lieferzeit…', + 'checkout.codFee': 'Nachnahmegebühr:', + 'checkout.placeOrder': 'Bestellung aufgeben', + 'checkout.emptyCart': 'Ihr Warenkorb ist leer.', + 'checkout.amountDue': 'Zu zahlen: {{amount}}', + 'checkout.payWithPayU': 'Mit PayU bezahlen', + 'checkout.orderError': 'Bestellung fehlgeschlagen.', + 'checkout.thankYou': 'Vielen Dank für Ihre Bestellung!', + 'checkout.orderNumber': 'Bestellnummer:', + 'checkout.status': 'Status:', + 'checkout.status.PendingPayment': 'Zahlung ausstehend', + 'checkout.status.Paid': 'Bezahlt', + 'checkout.status.Processing': 'In Bearbeitung', + 'checkout.status.Shipped': 'Versendet', + 'checkout.status.Delivered': 'Zugestellt', + 'checkout.status.Cancelled': 'Storniert', + 'checkout.payuSandbox.badge': 'PayU-Sandbox', + 'checkout.payuSandbox.title': 'Simulierte PayU-Zahlung', + 'checkout.payuSandbox.subtitle': + 'Kein echtes Geld. Bestätigen Sie, um die Bestellung als bezahlt zu markieren.', + 'checkout.payuSandbox.pay': 'Bezahlen (Sandbox)', + 'checkout.payuSandbox.paying': 'Wird verarbeitet…', + 'checkout.payuSandbox.cancel': 'Abbrechen', + 'checkout.backToShop': 'Zurück zum Shop', + 'checkout.country.PL': 'Polen', + 'checkout.country.DE': 'Deutschland', + 'checkout.country.CZ': 'Tschechien', + 'checkout.country.SK': 'Slowakei', + 'checkout.carrier': 'Versanddienst', + 'checkout.parcelPoint': 'Abholpunkt', + 'checkout.parcelPoint.InPost': 'InPost-Paketstation', + 'checkout.parcelPoint.Dhl': 'DHL-Abholpunkt', + 'checkout.parcelPoint.Dpd': 'DPD-Abholpunkt', + 'checkout.parcelSearchPlaceholder': 'Stadt oder PLZ…', + 'checkout.searchParcelPoints': 'Suchen', + 'checkout.parcelPointRequired': 'Wählen Sie einen Abholpunkt.', + 'checkout.parcelSearchError': + 'Abholpunkte konnten nicht geladen werden. Bitte erneut versuchen.', + 'checkout.parcelMapHint': + 'Klicken Sie einen Punkt auf der Karte oder wählen Sie aus der Liste.', + 'checkout.selectedPoint': 'Ausgewählter Punkt', + 'checkout.choosePoint': 'Abholpunkt wählen', + 'checkout.changePoint': 'Ändern', + 'checkout.closePicker': 'Schließen', + 'checkout.nearMe': 'In meiner Nähe', + 'checkout.geoNotSupported': 'Geolokalisierung wird in diesem Browser nicht unterstützt.', + 'checkout.geoDenied': 'Standortzugriff verweigert. Suchen Sie nach Stadt oder PLZ.', + 'checkout.loadingPoints': 'Abholpunkte werden geladen…', + 'checkout.loadingMap': 'Karte wird geladen…', + 'checkout.openingHours': 'Öffnungszeiten', + 'checkout.supportsCod': 'Nachnahme', + 'checkout.sandboxMapHint': + 'Demokarte mit Beispielpunkten. Träger-Widgets in appsettings konfigurieren für Live-Karten.', + 'checkout.parcelNoResults': + 'Keine Abholpunkte gefunden. Versuchen Sie eine andere Suche.', + 'checkout.service.ParcelLocker': 'Abholpunkt', + 'checkout.service.ParcelLocker.InPost': 'Paketstation', + 'checkout.service.ParcelLocker.Dhl': 'DHL-Abholpunkt', + 'checkout.service.ParcelLocker.Dpd': 'DPD-Abholpunkt', + 'checkout.service.Courier': 'Kurier', + 'checkout.carrier.InPost': 'InPost', + 'checkout.carrier.Dhl': 'DHL', + 'checkout.carrier.Dpd': 'DPD', + 'checkout.carrierDesc.InPost': 'InPost Paketstation oder Kurier.', + 'checkout.carrierDesc.Dhl': 'DHL-Abholpunkt oder Kurier.', + 'checkout.carrierDesc.Dpd': 'DPD-Abholpunkt oder Kurier.', + 'checkout.payment.PayU': 'PayU (Karte, BLIK)', + 'checkout.payment.BankTransfer': 'Überweisung', + 'checkout.payment.CashOnDelivery': 'Nachnahme', + 'checkout.paymentDesc.PayU': 'Sichere Online-Zahlung — Weiterleitung zu PayU.', + 'checkout.paymentDesc.BankTransfer': 'Überweisungsdaten erhalten Sie per E-Mail.', + 'checkout.paymentDesc.CashOnDelivery': 'Zahlung beim Kurier (+ Nachnahmegebühr).', + 'checkout.paymentHint.BankTransfer': + 'Überweisen Sie den Betrag mit den Daten aus der Bestätigungs-E-Mail.', + 'checkout.paymentHint.CashOnDelivery': + 'Bitte halten Sie Bargeld für die Zustellung bereit.', + 'auth.loginTitle': 'Anmelden', + 'auth.loginPlaceholder': 'E-Mail', + 'auth.login': 'Anmelden', + 'auth.register': 'Konto erstellen', + 'auth.forgotPassword': 'Login oder Passwort vergessen', + 'auth.noAccountTitle': 'Noch kein Konto?', + 'auth.noAccountBody': + 'Erstellen Sie in wenigen Minuten ein Konto und nutzen Sie Bestellhistorie und Favoriten.', + 'auth.haveAccount': 'Ich habe ein Konto', + 'auth.registerError': 'Registrierung fehlgeschlagen.', + 'auth.loginError': 'Ungültige E-Mail oder Passwort.', + 'account.welcome': 'Willkommen, {{name}}!', + 'account.logout': 'Abmelden', + 'account.myOrders': 'Meine Bestellungen', + 'account.details': 'Details', + 'account.noOrders': 'Keine Bestellungen.', + 'account.loginPrompt': ', um Ihr Konto zu sehen.', + 'favorites.title': 'Lieblingsprodukte', + 'favorites.empty': 'Keine Lieblingsprodukte.', +}; diff --git a/src/shop/src/app/core/i18n/translations/en.ts b/src/shop/src/app/core/i18n/translations/en.ts new file mode 100644 index 0000000..06c6f5c --- /dev/null +++ b/src/shop/src/app/core/i18n/translations/en.ts @@ -0,0 +1,357 @@ +import { TranslationMap } from '../i18n.models'; + +export const EN_TRANSLATIONS: TranslationMap = { + 'meta.pageTitle': 'iLED — Lamp shop', + 'meta.defaultDescription': + 'Modern LED lamps. Trusted solutions for your home.', + 'meta.home.title': 'iLED — Lamp shop', + 'meta.home.description': + 'Modern LED lamps. Trusted solutions for your home.', + 'meta.products.title': 'Products — iLED', + 'meta.products.description': 'Browse our LED lamp collection.', + 'meta.cart.title': 'Cart — iLED', + 'meta.cart.description': 'Your shopping cart.', + 'meta.checkout.title': 'Checkout — iLED', + 'meta.checkout.description': 'Complete your order.', + 'meta.order.title': 'Order confirmation — iLED', + 'meta.order.description': 'Thank you for your order.', + 'meta.login.title': 'Sign in — iLED', + 'meta.login.description': 'Sign in or create an account.', + 'meta.favorites.title': 'Favorites — iLED', + 'meta.favorites.description': 'Your favorite products.', + 'meta.account.title': 'Account — iLED', + 'meta.account.description': 'Your account and orders.', + 'meta.privacy.title': 'Privacy policy — iLED', + 'meta.privacy.description': 'How we process your personal data.', + 'meta.terms.title': 'Terms & conditions — iLED', + 'meta.terms.description': 'Terms of use of the iLED online store.', + 'meta.notFound.title': 'Page not found — iLED', + 'meta.notFound.description': 'The page you requested does not exist.', + 'notFound.title': 'Page not found', + 'notFound.subtitle': 'The link may be outdated or the page was moved.', + 'notFound.home': 'Home', + 'notFound.products': 'Browse products', + 'footer.privacy': 'Privacy policy', + 'cookies.title': 'Cookies & analytics', + 'cookies.body': + 'We use optional analytics to improve the shop. You can change your mind anytime.', + 'cookies.accept': 'Accept analytics', + 'cookies.reject': 'Reject', + 'common.loading': 'Loading…', + 'common.saving': 'Saving…', + 'common.close': 'Close', + 'common.clear': 'Clear', + 'a11y.paymentMethods': 'Payment methods', + 'a11y.productThumbs': 'Product thumbnails', + 'footer.shipping': 'Shipping', + 'footer.aboutUs': 'About iLED', + 'footer.secureCheckout': 'Secure checkout with PayU, BLIK and cards.', + 'trust.aria': 'Why shop with us', + 'trust.deliveryTitle': 'Delivery in up to 24h', + 'trust.deliveryBody': 'Dispatch on business days when in stock.', + 'trust.freeShipTitle': 'Free delivery', + 'trust.freeShipBody': 'Free shipping from {{amount}}', + 'trust.payTitle': 'Secure payments', + 'trust.payBody': 'PayU — card, BLIK and more.', + 'trust.returnsTitle': '14-day returns', + 'trust.returnsBody': 'Easy returns for consumers.', + 'cart.freeShipRemaining': 'Add {{amount}} more for free delivery', + 'cart.freeShipUnlocked': 'You unlocked free delivery!', + 'cart.freeShipShopMore': 'Shop more', + 'cart.emptyTitle': 'Your cart is empty', + 'cart.emptyBody': 'Browse the collection and add lamps you love.', + 'cart.emptyCta': 'Browse products', + 'cart.deliveryAtCheckout': 'Calculated at checkout', + 'cart.discountCheckoutHint': 'Valid codes are applied at checkout.', + 'productCard.freeShipChip': 'Free delivery', + 'productCard.removeFavorite': 'Remove from favorites', + 'products.outOfStock': 'Out of stock', + 'products.reviewsEmpty': 'Be the first to review this lamp.', + 'products.color.czarny': 'Black', + 'products.color.drewno': 'Wood', + 'products.color.srebrny': 'Silver', + 'products.color.złoty': 'Gold', + 'products.color.zloty': 'Gold', + 'products.clearFilters': 'Clear filters', + 'products.showFilters': 'Filters', + 'products.noResults': 'No products match your filters.', + 'products.viewAll': 'View all', + 'home.viewAllBestsellers': 'View all bestsellers', + 'home.viewAllNew': 'View all new arrivals', + 'auth.registerTitle': 'Create account', + 'auth.haveAccountTitle': 'Already have an account?', + 'auth.haveAccountBody': 'Sign in to see orders, favorites and faster checkout.', + 'auth.loginPlaceholder': 'Email', + 'auth.showPassword': 'Show password', + 'auth.hidePassword': 'Hide password', + 'auth.forgotPasswordMailto': 'Email us to reset your password', + 'auth.guestCheckout': 'Continue as guest at checkout', + 'auth.secureHint': 'Your data is protected and never sold.', + 'account.browseCta': 'Browse products', + 'account.status': 'Status', + 'favorites.browseCta': 'Discover products', + 'meta.about.title': 'About us — iLED', + 'meta.about.description': 'Learn about iLED and our lighting.', + 'meta.contact.title': 'Contact — iLED', + 'meta.contact.description': 'Get in touch with iLED support.', + 'meta.shipping.title': 'Shipping — iLED', + 'meta.shipping.description': 'Delivery options, times and free shipping.', + 'meta.returns.title': 'Returns — iLED', + 'meta.returns.description': 'How to return an order within 14 days.', + 'meta.faq.title': 'FAQ — iLED', + 'meta.faq.description': 'Answers about orders, delivery and payments.', + 'meta.cookies.title': 'Cookies — iLED', + 'meta.cookies.description': 'How we use cookies and analytics.', + 'legal.privacy.title': 'Privacy policy', + 'legal.privacy.body': + 'DRAFT — for lawyer review.\n\n1. Controller\niLED operates this online store and processes personal data needed to fulfill orders (name, email, phone, address).\n\n2. Purposes\nOrder processing, account management, newsletter (with consent), shop improvement.\n\n3. Analytics & monitoring\nWith your consent we may use self-hosted OpenPanel for anonymized traffic analytics. The API uses self-hosted OpenObserve for logs, metrics and traces (server-side only).\n\n4. Rights\nYou may request access, rectification, deletion or restriction of processing, and lodge a complaint with the supervisory authority (UODO in Poland).\n\n5. Contact\nContact us via the store contact details published on the website.', + 'legal.terms.title': 'Terms & conditions', + 'legal.terms.body': + 'DRAFT — for lawyer review.\n\n1. Scope\nThese terms govern purchases in the iLED online store.\n\n2. Orders\nPlacing an order constitutes an offer to buy. The contract is concluded when we confirm the order.\n\n3. Prices & payment\nPrices are in PLN and include VAT unless stated otherwise. Available payment methods are shown at checkout (e.g. PayU, bank transfer, COD).\n\n4. Delivery\nDelivery is via selected carriers (InPost, DHL, DPD) to an address or pickup point.\n\n5. Withdrawal\nConsumers may withdraw from a distance contract within 14 days, subject to statutory exceptions.\n\n6. Complaints\nContact us regarding product defects according to applicable warranty and consumer law.', + 'content.about.title': 'About iLED', + 'content.about.body': + 'iLED is a specialist LED lighting shop focused on modern lamps for homes and workspaces.\n\nWe select products for energy efficiency, reliable drivers and clean design. Every order is packed with care and shipped from Poland.\n\nWhether you are refreshing a living room or fitting out an office, our team helps you choose the right light — before and after purchase.', + 'content.contact.title': 'Contact', + 'content.contact.body': + 'Questions about products, orders or returns? We are happy to help.\n\nWrite to us at kontakt@iled.pl — we usually reply within 2 business days (Mon–Fri, excluding public holidays in Poland).\n\nWhen contacting us, include your order number if your message is about a specific purchase. This helps us resolve your case faster.', + 'content.shipping.title': 'Shipping & delivery', + 'content.shipping.body': + 'We ship across Poland using trusted carriers: InPost (parcel lockers and courier), DHL and DPD.\n\nOrders placed on business days with in-stock items are typically dispatched within 24 hours. Delivery time depends on the carrier and destination — you will see available options and cost at checkout.\n\nFree delivery applies from 499.99 PLN (cart subtotal before shipping). Bulky or special-handling items may have individual rules shown on the product page.\n\nYou will receive tracking information by email once the parcel leaves our warehouse.', + 'content.returns.title': 'Returns & withdrawal', + 'content.returns.body': + 'If you are a consumer in the EU, you may withdraw from a distance purchase within 14 days of receiving the goods, without giving a reason (subject to statutory exceptions, e.g. custom-made items).\n\nTo start a return, contact us at kontakt@iled.pl with your order number. Pack the product securely in its original packaging where possible and send it to the address we provide.\n\nRefunds are processed after we receive and inspect the returned goods. Payment is returned using the same method as your original purchase, unless we agree otherwise.', + 'content.cookies.title': 'Cookies & analytics', + 'content.cookies.body': + 'This shop uses essential cookies required for the cart, checkout and language preferences.\n\nWith your consent we may use self-hosted OpenPanel analytics to understand how the site is used — without selling data to ad networks. You can accept or reject optional analytics in the cookie banner; your choice is stored locally and can be changed anytime by clearing site data or revisiting this page.\n\nServer-side monitoring (OpenObserve) helps us keep the API reliable and does not use browser tracking cookies.', + 'content.faq.title': 'Frequently asked questions', + 'content.faq.body': + 'Quick answers about delivery, payments, returns and pickup points. Need more help? See our Contact page.', + 'content.faq.q1': 'How fast will my order arrive?', + 'content.faq.a1': + 'In-stock orders placed on business days are usually dispatched within 24 hours. Final delivery time depends on the carrier and option you choose at checkout (parcel locker, pickup point or courier to your door).', + 'content.faq.q2': 'Which payment methods do you accept?', + 'content.faq.a2': + 'At checkout you can pay via PayU (card, BLIK and other methods supported by PayU), bank transfer or cash on delivery where available. All online payments are processed on secure PayU pages.', + 'content.faq.q3': 'When is delivery free?', + 'content.faq.a3': + 'Standard delivery is free when your cart subtotal reaches 499.99 PLN before shipping. The progress bar in the cart shows how much is left to unlock free delivery.', + 'content.faq.q4': 'How do I return a product?', + 'content.faq.a4': + 'Consumers may withdraw within 14 days of receiving the order. Email kontakt@iled.pl with your order number — we will send return instructions and the delivery address. Products should be unused and returned with original packaging when possible.', + 'content.faq.q5': 'What warranty applies to lamps?', + 'content.faq.a5': + 'Products are covered by statutory warranty and, where applicable, manufacturer warranty. If you notice a defect, contact us with photos and your order number. We will guide you through repair, replacement or refund according to consumer law.', + 'content.faq.q6': 'How do I choose a parcel locker or pickup point?', + 'content.faq.a6': + 'Select InPost, DHL or DPD pickup at checkout, then search by city or postal code on the map. Click your preferred point — the address is saved with your order. For invoice purposes, enter your billing address separately; it is not the locker address.', + 'content.faq.q7': 'Can I change or cancel an order?', + 'content.faq.a7': + 'If your order has not shipped yet, write to us as soon as possible at kontakt@iled.pl. Once dispatched, changes may not be possible — you can still use your 14-day withdrawal right after delivery (see Returns).', + 'content.faq.q8': 'Do you ship outside Poland?', + 'content.faq.a8': + 'The checkout is optimised for Poland. If you need delivery to another country, contact us before ordering — we will confirm availability, shipping cost and estimated delivery time.', + 'checkout.billingInvoiceHint': + 'For pickup delivery, enter your invoice / billing address (it is not the locker address).', + 'newsletter.error': 'Subscription failed. Try again.', + 'newsletter.submitting': 'Subscribing…', + + 'a11y.skipToContent': 'Skip to content', + 'a11y.mainNav': 'Main navigation', + 'a11y.search': 'Search products', + 'a11y.langSwitcher': 'Choose language', + 'a11y.decreaseQty': 'Decrease quantity', + 'a11y.increaseQty': 'Increase quantity', + 'a11y.mainContent': 'Main content', + 'a11y.promoBanner': 'Promotional banner', + 'nav.home': 'HOME', + 'nav.bestsellers': 'BESTSELLERS', + 'nav.newArrivals': 'NEW ARRIVALS', + 'nav.searchPlaceholder': 'Search...', + 'nav.favorites': 'Favorites', + 'nav.cart': 'Cart', + 'nav.account': 'Account', + 'footer.tagline': 'Modern lamps. Trusted solutions.', + 'footer.customerService': 'Customer service', + 'footer.returnsPolicy': 'Returns policy', + 'footer.about': 'About us', + 'footer.contact': 'Contact', + 'footer.terms': 'Terms & conditions', + 'footer.cookies': 'Cookies', + 'footer.advice': 'We advise', + 'footer.faq': 'FAQ', + 'footer.copyright': 'Copyright 2026 © iLED', + 'newsletter.headline': 'Light that makes a difference.', + 'newsletter.subtitle': 'News and offers for subscribers only.', + 'newsletter.emailPlaceholder': 'Your email address', + 'newsletter.privacyNotice': + "By clicking 'subscribe' you accept our privacy policy.", + 'newsletter.submit': 'Subscribe', + 'newsletter.success': 'Thank you for subscribing!', + 'home.heroTitle': 'Light that makes a difference', + 'home.heroSubtitle': + 'Modern LED lamps. Trusted solutions for your home.', + 'home.cta': 'Browse products', + 'home.bestsellers': 'Bestsellers', + 'home.newArrivals': 'New arrivals', + 'products.filters': 'Filters', + 'products.priceLabel': 'Price (PLN)', + 'products.colorLabel': 'Colour', + 'products.applyFilters': 'Apply', + 'products.title': 'Products ({{count}})', + 'products.bestsellerBadge': 'Bestseller', + 'products.lowestPrice30Days': 'Lowest price in the last 30 days:', + 'products.addToCart': 'Add to cart', + 'products.quantity': 'Quantity', + 'products.inStock': 'In stock', + 'products.deliveryTime': 'Delivery time {{hours}}h', + 'products.freeDeliveryFrom': 'Free delivery from 499.99 PLN', + 'products.description': 'Product description', + 'products.specs': 'Technical specifications', + 'products.related': 'Products in the same category', + 'products.reviews': 'Reviews', + 'products.reviewsCount': '({{count}} reviews)', + 'products.yourRating': 'Your rating', + 'products.addReview': 'Add review', + 'productCard.favorite': 'Add to favorites', + 'common.firstName': 'First name', + 'common.lastName': 'Last name', + 'common.email': 'Email', + 'common.password': 'Password', + 'common.content': 'Content', + 'common.street': 'Street', + 'common.city': 'City', + 'common.postalCode': 'Postal code', + 'common.phone': 'Phone', + 'common.discountCode': 'Discount code', + 'common.discountCodeOptional': 'Discount code (optional)', + 'common.code': 'Code', + 'common.remove': 'Remove', + 'cart.title': 'Cart', + 'cart.addedTitle': 'Added to cart', + 'cart.addedMessage': '{{name}} × {{qty}} added to your cart', + 'cart.goToCart': 'Go to cart', + 'cart.continueShopping': 'Continue shopping', + 'cart.summary': 'Total ({{count}} items) {{amount}}', + 'cart.deliveryTime24h': 'Delivery time 24h', + 'cart.checkout': 'Proceed to checkout', + 'cart.confirmDiscount': 'Apply', + 'cart.itemCount': 'Items:', + 'cart.deliveryCost': 'Delivery cost:', + 'cart.total': 'Total:', + 'cart.vatNote': 'Price includes 23% VAT', + 'cart.recommended': 'Recommended products', + 'cart.discount': 'Discount', + 'cart.discountApplied': 'Code applied!', + 'cart.discountInvalid': 'Invalid code', + 'promo.error.Invalid': 'Invalid code.', + 'promo.error.Inactive': 'This code is inactive.', + 'promo.error.NotYetActive': 'This code is not active yet.', + 'promo.error.Expired': 'This code has expired or been used up.', + 'promo.error.MinOrder': 'Minimum order amount: {{amount}}.', + 'promo.error.GlobalCap': 'This promotion has reached its usage limit.', + 'promo.error.CustomerCap': 'You have already used this promotion.', + 'promo.error.WrongEmail': 'This code is assigned to a different email.', + 'cart.updateError': 'Could not update the cart. Try again.', + 'checkout.submitting': 'Placing order…', + 'checkout.title': 'Checkout', + 'checkout.subtitle': 'Complete your details and choose delivery and payment.', + 'checkout.contactDetails': 'Contact details', + 'checkout.shippingAddress': 'Shipping address', + 'checkout.billingAddress': 'Billing address', + 'checkout.billingSameAsShipping': 'Same as shipping address', + 'checkout.deliveryMethod': 'Delivery method', + 'checkout.paymentMethod': 'Payment method', + 'checkout.orderSummary': 'Order summary', + 'checkout.notes': 'Order notes', + 'checkout.notesPlaceholder': 'E.g. door code, preferred delivery time…', + 'checkout.codFee': 'Cash on delivery fee:', + 'checkout.placeOrder': 'Place order', + 'checkout.emptyCart': 'Your cart is empty.', + 'checkout.amountDue': 'Amount due: {{amount}}', + 'checkout.payWithPayU': 'Pay with PayU', + 'checkout.orderError': 'Order placement failed.', + 'checkout.thankYou': 'Thank you for your order!', + 'checkout.orderNumber': 'Order number:', + 'checkout.status': 'Status:', + 'checkout.status.PendingPayment': 'Pending payment', + 'checkout.status.Paid': 'Paid', + 'checkout.status.Processing': 'Processing', + 'checkout.status.Shipped': 'Shipped', + 'checkout.status.Delivered': 'Delivered', + 'checkout.status.Cancelled': 'Cancelled', + 'checkout.payuSandbox.badge': 'PayU sandbox', + 'checkout.payuSandbox.title': 'Simulated PayU payment', + 'checkout.payuSandbox.subtitle': + 'No real money is charged. Confirm to mark the order as paid.', + 'checkout.payuSandbox.pay': 'Pay (sandbox)', + 'checkout.payuSandbox.paying': 'Processing…', + 'checkout.payuSandbox.cancel': 'Cancel', + 'checkout.backToShop': 'Back to shop', + 'checkout.country.PL': 'Poland', + 'checkout.country.DE': 'Germany', + 'checkout.country.CZ': 'Czech Republic', + 'checkout.country.SK': 'Slovakia', + 'checkout.carrier': 'Carrier', + 'checkout.parcelPoint': 'Delivery point', + 'checkout.parcelPoint.InPost': 'InPost parcel locker', + 'checkout.parcelPoint.Dhl': 'DHL pickup point', + 'checkout.parcelPoint.Dpd': 'DPD pickup point', + 'checkout.parcelSearchPlaceholder': 'City or postal code…', + 'checkout.searchParcelPoints': 'Search', + 'checkout.parcelPointRequired': 'Select a delivery point.', + 'checkout.parcelSearchError': 'Could not load delivery points. Try again.', + 'checkout.parcelMapHint': 'Click a point on the map or choose from the list below.', + 'checkout.selectedPoint': 'Selected point', + 'checkout.choosePoint': 'Choose pickup point', + 'checkout.changePoint': 'Change', + 'checkout.closePicker': 'Close', + 'checkout.nearMe': 'Near me', + 'checkout.geoNotSupported': 'Geolocation is not supported in this browser.', + 'checkout.geoDenied': 'Could not access your location. Search by city or postal code.', + 'checkout.loadingPoints': 'Loading pickup points…', + 'checkout.loadingMap': 'Loading map…', + 'checkout.openingHours': 'Opening hours', + 'checkout.supportsCod': 'Cash on delivery', + 'checkout.sandboxMapHint': + 'Demo map with sample points. Used only when the carrier widget is unavailable.', + 'checkout.parcelNoResults': 'No delivery points found. Try a different search.', + 'checkout.service.ParcelLocker': 'Pickup point', + 'checkout.service.ParcelLocker.InPost': 'Parcel locker', + 'checkout.service.ParcelLocker.Dhl': 'DHL pickup point', + 'checkout.service.ParcelLocker.Dpd': 'DPD pickup point', + 'checkout.service.Courier': 'Courier', + 'checkout.carrier.InPost': 'InPost', + 'checkout.carrier.Dhl': 'DHL', + 'checkout.carrier.Dpd': 'DPD', + 'checkout.carrierDesc.InPost': 'InPost parcel locker or courier.', + 'checkout.carrierDesc.Dhl': 'DHL pickup point or courier.', + 'checkout.carrierDesc.Dpd': 'DPD pickup point or courier.', + 'checkout.payment.PayU': 'PayU (card, BLIK)', + 'checkout.payment.BankTransfer': 'Bank transfer', + 'checkout.payment.CashOnDelivery': 'Cash on delivery', + 'checkout.paymentDesc.PayU': 'Secure online payment — redirect to PayU.', + 'checkout.paymentDesc.BankTransfer': 'Transfer details will be emailed after checkout.', + 'checkout.paymentDesc.CashOnDelivery': 'Pay the courier on delivery (+ COD fee).', + 'checkout.paymentHint.BankTransfer': + 'Transfer the order total using the bank details in your confirmation email.', + 'checkout.paymentHint.CashOnDelivery': + 'Please have cash ready when the courier arrives.', + 'auth.loginTitle': 'Sign in', + 'auth.login': 'Sign in', + 'auth.register': 'Create account', + 'auth.forgotPassword': 'Forgot login or password', + 'auth.noAccountTitle': "Don't have an account?", + 'auth.noAccountBody': + 'Create an account in minutes and enjoy easy shopping, order history and favorites.', + 'auth.haveAccount': 'I have an account', + 'auth.registerError': 'Registration failed.', + 'auth.loginError': 'Invalid email or password.', + 'account.welcome': 'Welcome, {{name}}!', + 'account.logout': 'Sign out', + 'account.myOrders': 'My orders', + 'account.details': 'Details', + 'account.noOrders': 'No orders yet.', + 'account.loginPrompt': ' to view your account.', + 'favorites.title': 'Favorite products', + 'favorites.empty': 'No favorite products yet.', +}; diff --git a/src/shop/src/app/core/i18n/translations/index.ts b/src/shop/src/app/core/i18n/translations/index.ts new file mode 100644 index 0000000..4fd23d7 --- /dev/null +++ b/src/shop/src/app/core/i18n/translations/index.ts @@ -0,0 +1,12 @@ +import { LocaleCode, TranslationMap } from '../i18n.models'; +import { CS_TRANSLATIONS } from './cs'; +import { DE_TRANSLATIONS } from './de'; +import { EN_TRANSLATIONS } from './en'; +import { PL_TRANSLATIONS } from './pl'; + +export const TRANSLATIONS: Record = { + pl: PL_TRANSLATIONS, + en: EN_TRANSLATIONS, + de: DE_TRANSLATIONS, + cs: CS_TRANSLATIONS, +}; diff --git a/src/shop/src/app/core/i18n/translations/pl.ts b/src/shop/src/app/core/i18n/translations/pl.ts new file mode 100644 index 0000000..38bbbbc --- /dev/null +++ b/src/shop/src/app/core/i18n/translations/pl.ts @@ -0,0 +1,357 @@ +import { TranslationMap } from '../i18n.models'; + +export const PL_TRANSLATIONS: TranslationMap = { + 'meta.pageTitle': 'iLED — Sklep z lampami', + 'meta.defaultDescription': + 'Nowoczesne lampy LED. Sprawdzone rozwiązania do domu.', + 'meta.home.title': 'iLED — Sklep z lampami', + 'meta.home.description': + 'Nowoczesne lampy LED. Sprawdzone rozwiązania do domu.', + 'meta.products.title': 'Produkty — iLED', + 'meta.products.description': 'Przeglądaj kolekcję lamp LED.', + 'meta.cart.title': 'Koszyk — iLED', + 'meta.cart.description': 'Twój koszyk zakupowy.', + 'meta.checkout.title': 'Kasa — iLED', + 'meta.checkout.description': 'Dokończ zamówienie.', + 'meta.order.title': 'Potwierdzenie zamówienia — iLED', + 'meta.order.description': 'Dziękujemy za zamówienie.', + 'meta.login.title': 'Logowanie — iLED', + 'meta.login.description': 'Zaloguj się lub załóż konto.', + 'meta.favorites.title': 'Ulubione — iLED', + 'meta.favorites.description': 'Twoje ulubione produkty.', + 'meta.account.title': 'Konto — iLED', + 'meta.account.description': 'Twoje konto i zamówienia.', + 'meta.privacy.title': 'Polityka prywatności — iLED', + 'meta.privacy.description': 'Jak przetwarzamy dane osobowe.', + 'meta.terms.title': 'Regulamin — iLED', + 'meta.terms.description': 'Regulamin sklepu internetowego iLED.', + 'meta.notFound.title': 'Nie znaleziono strony — iLED', + 'meta.notFound.description': 'Żądana strona nie istnieje.', + 'notFound.title': 'Nie znaleziono strony', + 'notFound.subtitle': 'Link mógł wygasnąć albo strona została przeniesiona.', + 'notFound.home': 'Strona główna', + 'notFound.products': 'Przeglądaj produkty', + 'footer.privacy': 'Polityka prywatności', + 'cookies.title': 'Cookies i analityka', + 'cookies.body': + 'Używamy opcjonalnej analityki, aby ulepszać sklep. Możesz zmienić decyzję w każdej chwili.', + 'cookies.accept': 'Akceptuję analitykę', + 'cookies.reject': 'Odrzuć', + 'common.loading': 'Ładowanie…', + 'common.saving': 'Zapisywanie…', + 'common.close': 'Zamknij', + 'common.clear': 'Wyczyść', + 'a11y.paymentMethods': 'Metody płatności', + 'a11y.productThumbs': 'Miniatury produktu', + 'footer.shipping': 'Dostawa', + 'footer.aboutUs': 'O iLED', + 'footer.secureCheckout': 'Bezpieczne płatności PayU, BLIK i karty.', + 'trust.aria': 'Dlaczego warto kupować u nas', + 'trust.deliveryTitle': 'Dostawa nawet w 24h', + 'trust.deliveryBody': 'Wysyłka w dni robocze przy dostępności.', + 'trust.freeShipTitle': 'Darmowa dostawa', + 'trust.freeShipBody': 'Gratis od {{amount}}', + 'trust.payTitle': 'Bezpieczne płatności', + 'trust.payBody': 'PayU — karta, BLIK i więcej.', + 'trust.returnsTitle': '14 dni na zwrot', + 'trust.returnsBody': 'Łatwy zwrot dla konsumentów.', + 'cart.freeShipRemaining': 'Dodaj jeszcze {{amount}} do darmowej dostawy', + 'cart.freeShipUnlocked': 'Masz darmową dostawę!', + 'cart.freeShipShopMore': 'Dokup produkty', + 'cart.emptyTitle': 'Koszyk jest pusty', + 'cart.emptyBody': 'Przejrzyj kolekcję i dodaj lampy, które lubisz.', + 'cart.emptyCta': 'Przeglądaj produkty', + 'cart.deliveryAtCheckout': 'Wyliczane w kasie', + 'cart.discountCheckoutHint': 'Kody rabatowe stosujemy w kasie.', + 'productCard.freeShipChip': 'Darmowa dostawa', + 'productCard.removeFavorite': 'Usuń z ulubionych', + 'products.outOfStock': 'Brak w magazynie', + 'products.reviewsEmpty': 'Napisz pierwszą opinię o tej lampie.', + 'products.color.czarny': 'Czarny', + 'products.color.drewno': 'Drewno', + 'products.color.srebrny': 'Srebrny', + 'products.color.złoty': 'Złoty', + 'products.color.zloty': 'Złoty', + 'products.clearFilters': 'Wyczyść filtry', + 'products.showFilters': 'Filtry', + 'products.noResults': 'Brak produktów dla wybranych filtrów.', + 'products.viewAll': 'Zobacz wszystkie', + 'home.viewAllBestsellers': 'Wszystkie bestsellery', + 'home.viewAllNew': 'Wszystkie nowości', + 'auth.registerTitle': 'Utwórz konto', + 'auth.haveAccountTitle': 'Masz już konto?', + 'auth.haveAccountBody': 'Zaloguj się, aby zobaczyć zamówienia, ulubione i szybszą kasę.', + 'auth.loginPlaceholder': 'E-mail', + 'auth.showPassword': 'Pokaż hasło', + 'auth.hidePassword': 'Ukryj hasło', + 'auth.forgotPasswordMailto': 'Napisz do nas, aby zresetować hasło', + 'auth.guestCheckout': 'Kontynuuj jako gość w kasie', + 'auth.secureHint': 'Twoje dane są chronione i nie są sprzedawane.', + 'account.browseCta': 'Przeglądaj produkty', + 'account.status': 'Status', + 'favorites.browseCta': 'Odkryj produkty', + 'meta.about.title': 'O nas — iLED', + 'meta.about.description': 'Poznaj iLED i nasze oświetlenie.', + 'meta.contact.title': 'Kontakt — iLED', + 'meta.contact.description': 'Skontaktuj się z obsługą iLED.', + 'meta.shipping.title': 'Dostawa — iLED', + 'meta.shipping.description': 'Opcje dostawy, terminy i darmowa wysyłka.', + 'meta.returns.title': 'Zwroty — iLED', + 'meta.returns.description': 'Jak zwrócić zamówienie w 14 dni.', + 'meta.faq.title': 'FAQ — iLED', + 'meta.faq.description': 'Odpowiedzi o zamówieniach, dostawie i płatnościach.', + 'meta.cookies.title': 'Cookies — iLED', + 'meta.cookies.description': 'Jak używamy cookies i analityki.', + 'legal.privacy.title': 'Polityka prywatności', + 'legal.privacy.body': + 'SZKIC — do weryfikacji prawnej.\n\n1. Administrator\niLED prowadzi sklep internetowy i przetwarza dane niezbędne do realizacji zamówień (imię, e-mail, telefon, adres).\n\n2. Cele\nRealizacja zamówień, konto, newsletter (za zgodą), ulepszanie sklepu.\n\n3. Analityka i monitoring\nZa zgodą możemy używać self-hosted OpenPanel do anonimizowanej analityki ruchu. API korzysta z self-hosted OpenObserve (logi, metryki, trace) wyłącznie po stronie serwera.\n\n4. Prawa\nMasz prawo dostępu, sprostowania, usunięcia, ograniczenia przetwarzania oraz skargi do UODO.\n\n5. Kontakt\nDane kontaktowe znajdują się na stronie sklepu.', + 'legal.terms.title': 'Regulamin', + 'legal.terms.body': + 'SZKIC — do weryfikacji prawnej.\n\n1. Zakres\nRegulamin określa zasady zakupów w sklepie iLED.\n\n2. Zamówienia\nZłożenie zamówienia stanowi ofertę nabycia. Umowa zostaje zawarta po potwierdzeniu zamówienia.\n\n3. Ceny i płatność\nCeny w PLN, z VAT, o ile nie wskazano inaczej. Metody płatności widoczne w kasie.\n\n4. Dostawa\nDostawa przez wybranego przewoźnika (InPost, DHL, DPD) pod adres lub do punktu.\n\n5. Odstąpienie\nKonsument może odstąpić od umowy w 14 dni, z wyjątkami ustawowymi.\n\n6. Reklamacje\nReklamacje zgodnie z obowiązującym prawem konsumenckim.', + 'content.about.title': 'O iLED', + 'content.about.body': + 'iLED to sklep specjalizujący się w nowoczesnym oświetleniu LED do domu i biura.\n\nWybieramy produkty pod kątem energooszczędności, niezawodnych driverów i estetyki. Każde zamówienie pakujemy starannie i wysyłamy z Polski.\n\nNiezależnie od tego, czy odświeżasz salon, czy wyposażasz biuro — pomożemy dobrać odpowiednie światło przed zakupem i po nim.', + 'content.contact.title': 'Kontakt', + 'content.contact.body': + 'Masz pytania o produkty, zamówienia lub zwroty? Chętnie pomożemy.\n\nNapisz na kontakt@iled.pl — zwykle odpowiadamy w ciągu 2 dni roboczych (pn.–pt., z wyłączeniem świąt w Polsce).\n\nPodaj numer zamówienia, jeśli sprawa dotyczy konkretnego zakupu — przyspieszy to rozwiązanie.', + 'content.shipping.title': 'Dostawa', + 'content.shipping.body': + 'Wysyłamy na terenie Polski przez sprawdzonych przewoźników: InPost (paczkomaty i kurier), DHL oraz DPD.\n\nZamówienia złożone w dni robocze przy dostępności towaru zwykle wysyłamy w ciągu 24 godzin. Czas doręczenia zależy od przewoźnika i wybranej opcji — koszt i terminy zobaczysz w kasie.\n\nDarmowa dostawa od 499,99 zł (wartość produktów przed kosztem wysyłki). Dla gabarytów lub produktów specjalnych mogą obowiązywać indywidualne zasady — informacja na karcie produktu.\n\nPo nadaniu paczki wyślemy e-mail z numerem śledzenia.', + 'content.returns.title': 'Zwroty i odstąpienie', + 'content.returns.body': + 'Jako konsument możesz odstąpić od umowy zawartej na odległość w ciągu 14 dni od otrzymania towaru, bez podawania przyczyny (z wyjątkami ustawowymi, np. rzeczy na zamówienie).\n\nAby rozpocząć zwrot, napisz na kontakt@iled.pl podając numer zamówienia. Zapakuj produkt starannie, najlepiej w oryginalnym opakowaniu, i wyślij na adres, który podamy.\n\nZwrot środków następuje po otrzymaniu i sprawdzeniu przesyłki — tą samą metodą płatności, chyba że ustalimy inaczej.', + 'content.cookies.title': 'Cookies i analityka', + 'content.cookies.body': + 'Sklep używa niezbędnych plików cookies do koszyka, kasy i zapisu języka.\n\nZa Twoją zgodą możemy korzystać z self-hosted OpenPanel, aby lepiej rozumieć ruch na stronie — bez sprzedaży danych sieciom reklamowym. Wybór akceptujesz w banerze cookies; możesz go zmienić, czyszcząc dane strony lub wracając na tę podstronę.\n\nMonitoring po stronie serwera (OpenObserve) służy niezawodności API i nie ustawia cookies śledzących w przeglądarce.', + 'content.faq.title': 'Często zadawane pytania', + 'content.faq.body': + 'Krótkie odpowiedzi o dostawie, płatnościach, zwrotach i punktach odbioru. Potrzebujesz więcej informacji? Zobacz stronę Kontakt.', + 'content.faq.q1': 'Jak szybko otrzymam zamówienie?', + 'content.faq.a1': + 'Przy dostępności towaru zamówienia złożone w dni robocze zwykle wysyłamy w ciągu 24 godzin. Termin doręczenia zależy od przewoźnika i opcji wybranej w kasie (paczkomat, punkt odbioru lub kurier pod adres).', + 'content.faq.q2': 'Jakie formy płatności są dostępne?', + 'content.faq.a2': + 'W kasie możesz zapłacić przez PayU (karta, BLIK i inne metody PayU), przelewem bankowym lub za pobraniem, jeśli jest dostępne. Płatności online realizowane są na bezpiecznych stronach PayU.', + 'content.faq.q3': 'Od kiedy dostawa jest darmowa?', + 'content.faq.a3': + 'Darmowa dostawa obowiązuje, gdy wartość produktów w koszyku wyniesie co najmniej 499,99 zł przed kosztem wysyłki. Pasek w koszyku pokazuje, ile brakuje do darmowej dostawy.', + 'content.faq.q4': 'Jak zwrócić produkt?', + 'content.faq.a4': + 'Konsument może odstąpić w ciągu 14 dni od otrzymania zamówienia. Napisz na kontakt@iled.pl z numerem zamówienia — prześlemy instrukcję i adres zwrotu. Towar najlepiej zwrócić nieużywany, w oryginalnym opakowaniu.', + 'content.faq.q5': 'Jaka gwarancja obowiązuje na lampy?', + 'content.faq.a5': + 'Produkty objęte są ustawową rękojmią oraz — jeśli dotyczy — gwarancją producenta. W razie wady skontaktuj się z nami, dołącz zdjęcia i numer zamówienia. Prowadzimy Cię przez naprawę, wymianę lub zwrot zgodnie z prawem konsumenckim.', + 'content.faq.q6': 'Jak wybrać paczkomat lub punkt odbioru?', + 'content.faq.a6': + 'W kasie wybierz odbiór InPost, DHL lub DPD, następnie wyszukaj miejscowość lub kod pocztowy na mapie i kliknij punkt — adres zapisze się do zamówienia. Adres do faktury podaj osobno; to nie jest adres paczkomatu.', + 'content.faq.q7': 'Czy mogę zmienić lub anulować zamówienie?', + 'content.faq.a7': + 'Jeśli paczka jeszcze nie wyszła, napisz jak najszybciej na kontakt@iled.pl. Po nadaniu zmiana może być niemożliwa — nadal możesz skorzystać z 14-dniowego prawa odstąpienia po doręczeniu (patrz Zwroty).', + 'content.faq.q8': 'Czy wysyłacie za granicę?', + 'content.faq.a8': + 'Kasa jest zoptymalizowana pod Polskę. Jeśli potrzebujesz dostawy do innego kraju, skontaktuj się przed zakupem — potwierdzimy możliwość, koszt i orientacyjny termin.', + 'checkout.billingInvoiceHint': + 'Przy odbiorze w punkcie podaj adres do faktury (to nie jest adres paczkomatu).', + 'newsletter.error': 'Nie udało się zapisać. Spróbuj ponownie.', + 'newsletter.submitting': 'Zapisywanie…', + + 'a11y.skipToContent': 'Przejdź do treści', + 'a11y.mainNav': 'Nawigacja główna', + 'a11y.search': 'Szukaj produktów', + 'a11y.langSwitcher': 'Wybierz język', + 'a11y.decreaseQty': 'Zmniejsz ilość', + 'a11y.increaseQty': 'Zwiększ ilość', + 'a11y.mainContent': 'Treść główna', + 'a11y.promoBanner': 'Baner promocyjny', + 'nav.home': 'STRONA GŁÓWNA', + 'nav.bestsellers': 'BESTSELLERY', + 'nav.newArrivals': 'NOWOŚCI', + 'nav.searchPlaceholder': 'Szukaj...', + 'nav.favorites': 'Ulubione', + 'nav.cart': 'Koszyk', + 'nav.account': 'Konto', + 'footer.tagline': 'Nowoczesne lampy. Sprawdzone rozwiązania.', + 'footer.customerService': 'Obsługa Klienta', + 'footer.returnsPolicy': 'Polityka zwrotów', + 'footer.about': 'O firmie', + 'footer.contact': 'Kontakt', + 'footer.terms': 'Regulamin', + 'footer.cookies': 'Pliki Cookies', + 'footer.advice': 'Doradzamy', + 'footer.faq': 'Częste pytania', + 'footer.copyright': 'Copyright 2026 © iLED', + 'newsletter.headline': 'Światło, które robi różnicę.', + 'newsletter.subtitle': 'Nowości i oferty tylko dla subskrybentów.', + 'newsletter.emailPlaceholder': 'Twój adres e-mail', + 'newsletter.privacyNotice': + "Klikając 'zapisz się' akceptujesz naszą politykę prywatności.", + 'newsletter.submit': 'Zapisz się', + 'newsletter.success': 'Dziękujemy za zapisanie się!', + 'home.heroTitle': 'Światło, które robi różnicę', + 'home.heroSubtitle': + 'Nowoczesne lampy LED. Sprawdzone rozwiązania dla Twojego domu.', + 'home.cta': 'Zobacz produkty', + 'home.bestsellers': 'Bestsellery', + 'home.newArrivals': 'Nowości', + 'products.filters': 'Filtry', + 'products.priceLabel': 'Cena (zł)', + 'products.colorLabel': 'Kolor', + 'products.applyFilters': 'Zastosuj', + 'products.title': 'Produkty ({{count}})', + 'products.bestsellerBadge': 'Bestseller', + 'products.lowestPrice30Days': 'Najniższa cena w przeciągu ostatnich 30 dni:', + 'products.addToCart': 'Dodaj do koszyka', + 'products.quantity': 'Ilość', + 'products.inStock': 'W magazynie', + 'products.deliveryTime': 'Czas dostawy {{hours}}h', + 'products.freeDeliveryFrom': 'Darmowa dostawa od 499,99 zł', + 'products.description': 'Opis produktu', + 'products.specs': 'Dane techniczne', + 'products.related': 'Produkty z tej samej kategorii', + 'products.reviews': 'Opinie', + 'products.reviewsCount': '({{count}} opinii)', + 'products.yourRating': 'Twoja ocena', + 'products.addReview': 'Dodaj opinię', + 'productCard.favorite': 'Dodaj do ulubionych', + 'common.firstName': 'Imię', + 'common.lastName': 'Nazwisko', + 'common.email': 'Email', + 'common.password': 'Hasło', + 'common.content': 'Treść', + 'common.street': 'Ulica', + 'common.city': 'Miasto', + 'common.postalCode': 'Kod pocztowy', + 'common.phone': 'Telefon', + 'common.discountCode': 'Kod rabatowy', + 'common.discountCodeOptional': 'Kod rabatowy (opcjonalnie)', + 'common.code': 'Kod', + 'common.remove': 'Usuń', + 'cart.title': 'Koszyk', + 'cart.addedTitle': 'Dodano do koszyka', + 'cart.addedMessage': '{{name}} × {{qty}} dodano do koszyka', + 'cart.goToCart': 'Przejdź do koszyka', + 'cart.continueShopping': 'Kontynuuj zakupy', + 'cart.summary': 'Razem ({{count}} artykułów) {{amount}}', + 'cart.deliveryTime24h': 'Czas dostawy 24h', + 'cart.checkout': 'Przejdź do kasy', + 'cart.confirmDiscount': 'Zatwierdź', + 'cart.itemCount': 'Ilość produktów:', + 'cart.deliveryCost': 'Koszt dostawy:', + 'cart.total': 'Razem:', + 'cart.vatNote': 'Cena zawiera 23% VAT', + 'cart.recommended': 'Rekomendowane produkty', + 'cart.discount': 'Rabat', + 'cart.discountApplied': 'Kod zastosowany!', + 'cart.discountInvalid': 'Błędny kod', + 'promo.error.Invalid': 'Nieprawidłowy kod.', + 'promo.error.Inactive': 'Kod jest nieaktywny.', + 'promo.error.NotYetActive': 'Kod jeszcze nieaktywny.', + 'promo.error.Expired': 'Kod wygasł lub został wykorzystany.', + 'promo.error.MinOrder': 'Minimalna wartość zamówienia: {{amount}}.', + 'promo.error.GlobalCap': 'Limit użyć promocji został wyczerpany.', + 'promo.error.CustomerCap': 'Wykorzystano już limit użyć dla tego klienta.', + 'promo.error.WrongEmail': 'Ten kod jest przypisany do innego adresu e-mail.', + 'cart.updateError': 'Nie udało się zaktualizować koszyka. Spróbuj ponownie.', + 'checkout.submitting': 'Składanie zamówienia…', + 'checkout.title': 'Kasa', + 'checkout.subtitle': 'Uzupełnij dane i wybierz dostawę oraz płatność.', + 'checkout.contactDetails': 'Dane kontaktowe', + 'checkout.shippingAddress': 'Adres dostawy', + 'checkout.billingAddress': 'Adres rozliczeniowy', + 'checkout.billingSameAsShipping': 'Taki sam jak adres dostawy', + 'checkout.deliveryMethod': 'Sposób dostawy', + 'checkout.paymentMethod': 'Sposób płatności', + 'checkout.orderSummary': 'Podsumowanie zamówienia', + 'checkout.notes': 'Uwagi do zamówienia', + 'checkout.notesPlaceholder': 'Np. kod domofonu, preferowana godzina dostawy…', + 'checkout.codFee': 'Opłata za pobranie:', + 'checkout.placeOrder': 'Złóż zamówienie', + 'checkout.emptyCart': 'Koszyk jest pusty.', + 'checkout.amountDue': 'Do zapłaty: {{amount}}', + 'checkout.payWithPayU': 'Zapłać przez PayU', + 'checkout.orderError': 'Błąd składania zamówienia.', + 'checkout.thankYou': 'Dziękujemy za zamówienie!', + 'checkout.orderNumber': 'Numer zamówienia:', + 'checkout.status': 'Status:', + 'checkout.status.PendingPayment': 'Oczekuje na płatność', + 'checkout.status.Paid': 'Opłacone', + 'checkout.status.Processing': 'W realizacji', + 'checkout.status.Shipped': 'Wysłane', + 'checkout.status.Delivered': 'Dostarczone', + 'checkout.status.Cancelled': 'Anulowane', + 'checkout.payuSandbox.badge': 'PayU sandbox', + 'checkout.payuSandbox.title': 'Symulacja płatności PayU', + 'checkout.payuSandbox.subtitle': + 'To nie jest prawdziwa płatność. Potwierdź, aby oznaczyć zamówienie jako opłacone.', + 'checkout.payuSandbox.pay': 'Zapłać (sandbox)', + 'checkout.payuSandbox.paying': 'Przetwarzanie…', + 'checkout.payuSandbox.cancel': 'Anuluj', + 'checkout.backToShop': 'Wróć do sklepu', + 'checkout.country.PL': 'Polska', + 'checkout.country.DE': 'Niemcy', + 'checkout.country.CZ': 'Czechy', + 'checkout.country.SK': 'Słowacja', + 'checkout.carrier': 'Przewoźnik', + 'checkout.parcelPoint': 'Punkt odbioru', + 'checkout.parcelPoint.InPost': 'Paczkomat InPost', + 'checkout.parcelPoint.Dhl': 'Punkt odbioru DHL', + 'checkout.parcelPoint.Dpd': 'Punkt odbioru DPD', + 'checkout.parcelSearchPlaceholder': 'Miasto lub kod pocztowy…', + 'checkout.searchParcelPoints': 'Szukaj', + 'checkout.parcelPointRequired': 'Wybierz punkt odbioru.', + 'checkout.parcelSearchError': 'Nie udało się wczytać punktów odbioru. Spróbuj ponownie.', + 'checkout.parcelMapHint': 'Kliknij punkt na mapie lub wybierz z listy poniżej.', + 'checkout.selectedPoint': 'Wybrany punkt', + 'checkout.choosePoint': 'Wybierz punkt odbioru', + 'checkout.changePoint': 'Zmień', + 'checkout.closePicker': 'Zamknij', + 'checkout.nearMe': 'Blisko mnie', + 'checkout.geoNotSupported': 'Geolokalizacja nie jest obsługiwana w tej przeglądarce.', + 'checkout.geoDenied': 'Brak dostępu do lokalizacji. Wyszukaj po mieście lub kodzie pocztowym.', + 'checkout.loadingPoints': 'Wczytywanie punktów odbioru…', + 'checkout.loadingMap': 'Wczytywanie mapy…', + 'checkout.openingHours': 'Godziny otwarcia', + 'checkout.supportsCod': 'Pobranie', + 'checkout.sandboxMapHint': + 'Mapa demonstracyjna z przykładowymi punktami. Tylko gdy widget przewoźnika jest niedostępny.', + 'checkout.parcelNoResults': 'Nie znaleziono punktów odbioru. Spróbuj innego wyszukiwania.', + 'checkout.service.ParcelLocker': 'Punkt odbioru', + 'checkout.service.ParcelLocker.InPost': 'Paczkomat', + 'checkout.service.ParcelLocker.Dhl': 'Punkt DHL', + 'checkout.service.ParcelLocker.Dpd': 'Punkt DPD', + 'checkout.service.Courier': 'Kurier', + 'checkout.carrier.InPost': 'InPost', + 'checkout.carrier.Dhl': 'DHL', + 'checkout.carrier.Dpd': 'DPD', + 'checkout.carrierDesc.InPost': 'Paczkomat lub kurier InPost.', + 'checkout.carrierDesc.Dhl': 'Punkt odbioru lub kurier DHL.', + 'checkout.carrierDesc.Dpd': 'Punkt odbioru lub kurier DPD.', + 'checkout.payment.PayU': 'PayU (karta, BLIK)', + 'checkout.payment.BankTransfer': 'Przelew bankowy', + 'checkout.payment.CashOnDelivery': 'Płatność przy odbiorze', + 'checkout.paymentDesc.PayU': 'Bezpieczna płatność online — przekierowanie do PayU.', + 'checkout.paymentDesc.BankTransfer': 'Dane do przelewu otrzymasz w e-mailu po złożeniu zamówienia.', + 'checkout.paymentDesc.CashOnDelivery': 'Zapłacisz kurierowi przy dostawie (+ opłata za pobranie).', + 'checkout.paymentHint.BankTransfer': + 'Przelej kwotę zamówienia na konto podane w e-mailu. Realizacja po zaksięgowaniu wpłaty.', + 'checkout.paymentHint.CashOnDelivery': + 'Przygotuj gotówkę na moment dostawy. Kurier wystawi dokument sprzedaży.', + 'auth.loginTitle': 'Logowanie', + 'auth.login': 'Zaloguj się', + 'auth.register': 'Utwórz konto', + 'auth.forgotPassword': 'Nie pamiętam loginu lub hasła', + 'auth.noAccountTitle': 'Nie masz konta?', + 'auth.noAccountBody': + 'Załóż konto w kilka chwil i korzystaj z wygodnych zakupów, historii zamówień i listy ulubionych.', + 'auth.haveAccount': 'Mam konto', + 'auth.registerError': 'Błąd rejestracji.', + 'auth.loginError': 'Nieprawidłowy email lub hasło.', + 'account.welcome': 'Witaj, {{name}}!', + 'account.logout': 'Wyloguj', + 'account.myOrders': 'Moje zamówienia', + 'account.details': 'Szczegóły', + 'account.noOrders': 'Brak zamówień.', + 'account.loginPrompt': ', aby zobaczyć konto.', + 'favorites.title': 'Ulubione produkty', + 'favorites.empty': 'Brak ulubionych produktów.', +}; diff --git a/src/shop/src/app/core/models.ts b/src/shop/src/app/core/models.ts new file mode 100644 index 0000000..42ab7e4 --- /dev/null +++ b/src/shop/src/app/core/models.ts @@ -0,0 +1,197 @@ +export interface PaginatedList { + items: T[]; + page: number; + pageSize: number; + totalCount: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; +} + +export interface ProductListItem { + id: string; + name: string; + slug: string; + shortDescription: string; + price: number; + averageRating: number | null; + reviewCount: number; + primaryImageUrl: string | null; + isBestseller: boolean; + isNewArrival: boolean; + isInStock: boolean; +} + +export interface ProductDetail { + id: string; + name: string; + slug: string; + shortDescription: string; + description: string; + price: number; + lowestPrice30Days: number; + productCode: string; + manufacturer: string; + isBestseller: boolean; + isInStock: boolean; + deliveryHours: number; + averageRating: number | null; + reviewCount: number; + categoryName: string; + variants: ProductVariant[]; + images: ProductImage[]; + attributes: ProductAttribute[]; +} + +export interface ProductVariant { + id: string; + sku: string; + colorName: string; + colorHex: string; + price: number; + isDefault: boolean; + isInStock: boolean; + stockQuantity: number; +} + +export interface ProductImage { + id: string; + url: string; + altText: string | null; + sortOrder: number; + isPrimary: boolean; +} + +export interface ProductAttribute { + name: string; + value: string; +} + +export interface ProductFilterOptions { + minPrice: number; + maxPrice: number; + colors: string[]; + filterGroups: FilterGroup[]; +} + +export interface FilterGroup { + name: string; + values: string[]; +} + +export interface CartDto { + id: string; + items: CartItemDto[]; + subtotal: number; + itemCount: number; +} + +export interface CartItemDto { + id: string; + productId: string; + variantId: string; + productName: string; + productSlug: string; + variantColor: string; + unitPrice: number; + quantity: number; + imageUrl: string | null; + isInStock: boolean; + lineTotal: number; +} + +export interface AuthResponse { + accessToken: string; + refreshToken: string; + expiresAt: string; + userId: string; + email: string; + firstName: string; + lastName: string; + roles: string[]; +} + +export interface UserProfile { + userId: string; + email: string; + firstName: string; + lastName: string; + roles: string[]; +} + +export interface OrderDto { + id: string; + orderNumber: string; + status: string; + subtotal: number; + deliveryCost: number; + discountAmount: number; + vatAmount: number; + total: number; + customerEmail: string; + customerPhone: string; + shippingCarrier: string; + shippingServiceType: string; + parcelPointId: string | null; + parcelPointName: string | null; + parcelPointAddress: string | null; + paymentMethod: string; + lines: OrderLineDto[]; + paymentRedirectUrl: string | null; +} + +export interface OrderLineDto { + productName: string; + variantColor: string; + unitPrice: number; + quantity: number; + lineTotal: number; +} + +export interface OrderSummary { + id: string; + orderNumber: string; + status: string; + total: number; + createdAt: string; + itemCount: number; +} + +export interface FavoriteDto { + id: string; + productId: string; + productName: string; + productSlug: string; + shortDescription: string; + price: number; + imageUrl: string | null; + isInStock: boolean; +} + +export interface PromoBanner { + text: string; + highlightedCode: string | null; +} + +export interface ProductReviews { + averageRating: number; + totalCount: number; + ratingDistribution: Record; + reviews: ReviewDto[]; +} + +export interface ReviewDto { + id: string; + authorName: string; + rating: number; + content: string; + createdAt: string; +} + +export interface DiscountValidation { + isValid: boolean; + errorCode: string | null; + minOrderAmount: number | null; + discountAmount: number | null; + freeShipping: boolean; +} diff --git a/src/shop/src/app/core/product-image.pipe.ts b/src/shop/src/app/core/product-image.pipe.ts new file mode 100644 index 0000000..42b559b --- /dev/null +++ b/src/shop/src/app/core/product-image.pipe.ts @@ -0,0 +1,9 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { resolveProductImageUrl } from './product-image.util'; + +@Pipe({ name: 'productImage', standalone: true }) +export class ProductImagePipe implements PipeTransform { + transform(url: string | null | undefined): string { + return resolveProductImageUrl(url); + } +} diff --git a/src/shop/src/app/core/product-image.util.ts b/src/shop/src/app/core/product-image.util.ts new file mode 100644 index 0000000..ac63d5a --- /dev/null +++ b/src/shop/src/app/core/product-image.util.ts @@ -0,0 +1,21 @@ +import { API_BASE_URL } from './constants'; + +/** Resolves product image paths from API uploads or shop assets. */ +export function resolveProductImageUrl( + url: string | null | undefined, +): string { + if (!url) { + return ''; + } + + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + if (url.startsWith('/uploads/')) { + return `${API_BASE_URL}${url}`; + } + + const path = url.startsWith('/') ? url : `/${url}`; + return path; +} diff --git a/src/shop/src/app/core/promo-errors.ts b/src/shop/src/app/core/promo-errors.ts new file mode 100644 index 0000000..623af57 --- /dev/null +++ b/src/shop/src/app/core/promo-errors.ts @@ -0,0 +1,37 @@ +import { I18nService } from './i18n/i18n.service'; +import { DiscountValidation } from './models'; + +/** Maps API promotion error codes to shop i18n strings. */ +export function promoErrorMessage( + i18n: I18nService, + res: Pick, +): string { + const code = res.errorCode; + if (!code) { + return i18n.t('cart.discountInvalid'); + } + + const key = `promo.error.${code}`; + if (code === 'MinOrder' && res.minOrderAmount != null) { + return i18n.t(key, { + amount: i18n.formatCurrency(res.minOrderAmount), + }); + } + + const translated = i18n.t(key); + return translated === key ? i18n.t('cart.discountInvalid') : translated; +} + +/** Reads validation error payload from HTTP 400 body. */ +export function promoErrorFromHttp(i18n: I18nService, err: unknown): string { + const body = (err as { error?: { errorCode?: string; minOrderAmount?: number } }) + ?.error; + if (body?.errorCode) { + return promoErrorMessage(i18n, { + errorCode: body.errorCode, + minOrderAmount: body.minOrderAmount ?? null, + }); + } + + return i18n.t('checkout.orderError'); +} diff --git a/src/shop/src/app/core/seo.initializer.ts b/src/shop/src/app/core/seo.initializer.ts new file mode 100644 index 0000000..fe079ae --- /dev/null +++ b/src/shop/src/app/core/seo.initializer.ts @@ -0,0 +1,57 @@ +import { inject, Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs'; +import { I18nService } from './i18n/i18n.service'; +import { SeoService } from './seo.service'; + +@Injectable({ providedIn: 'root' }) +export class SeoInitializer { + readonly #router = inject(Router); + readonly #seo = inject(SeoService); + readonly #i18n = inject(I18nService); + + init(): void { + this.#router.events + .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) + .subscribe(() => this.#applyFromRoute()); + + // Re-apply when locale changes (translations for title/description). + // Locale is a signal; poll via effect in app root instead if needed. + this.#applyFromRoute(); + } + + refresh(): void { + this.#applyFromRoute(); + } + + #applyFromRoute(): void { + let route = this.#router.routerState.snapshot.root; + while (route.firstChild) { + route = route.firstChild; + } + + // Product detail sets SEO itself after load. + if (route.paramMap.has('slug')) { + return; + } + + const titleKey = this.#data(route, 'titleKey') as string | undefined; + const descriptionKey = this.#data(route, 'descriptionKey') as + | string + | undefined; + this.#seo.applyRouteDefaults(titleKey, descriptionKey); + } + + #data(route: ActivatedRouteSnapshot, key: string): unknown { + let current: ActivatedRouteSnapshot | null = route; + while (current) { + if (current.data[key] != null) { + return current.data[key]; + } + + current = current.parent; + } + + return undefined; + } +} diff --git a/src/shop/src/app/core/seo.service.ts b/src/shop/src/app/core/seo.service.ts new file mode 100644 index 0000000..44b1c2e --- /dev/null +++ b/src/shop/src/app/core/seo.service.ts @@ -0,0 +1,83 @@ +import { DOCUMENT } from '@angular/common'; +import { Injectable, inject } from '@angular/core'; +import { Meta, Title } from '@angular/platform-browser'; +import { I18nService } from './i18n/i18n.service'; + +export interface SeoPayload { + title?: string; + titleKey?: string; + description?: string; + descriptionKey?: string; + imageUrl?: string; + url?: string; + type?: string; +} + +@Injectable({ providedIn: 'root' }) +export class SeoService { + readonly #title = inject(Title); + readonly #meta = inject(Meta); + readonly #i18n = inject(I18nService); + readonly #document = inject(DOCUMENT); + + readonly #defaultImage = '/og-default.svg'; + readonly #siteName = 'iLED'; + + apply(payload: SeoPayload): void { + const title = + payload.title ?? + (payload.titleKey ? this.#i18n.t(payload.titleKey) : this.#i18n.t('meta.pageTitle')); + const description = + payload.description ?? + (payload.descriptionKey + ? this.#i18n.t(payload.descriptionKey) + : this.#i18n.t('meta.defaultDescription')); + const image = this.#absoluteUrl(payload.imageUrl ?? this.#defaultImage); + const url = this.#absoluteUrl(payload.url ?? this.#document.location?.pathname ?? '/'); + const type = payload.type ?? 'website'; + + this.#title.setTitle(title); + this.#setMeta('description', description); + this.#setMeta('og:title', title, true); + this.#setMeta('og:description', description, true); + this.#setMeta('og:image', image, true); + this.#setMeta('og:url', url, true); + this.#setMeta('og:type', type, true); + this.#setMeta('og:site_name', this.#siteName, true); + this.#setMeta('twitter:card', 'summary_large_image'); + this.#setMeta('twitter:title', title); + this.#setMeta('twitter:description', description); + this.#setMeta('twitter:image', image); + } + + applyRouteDefaults(titleKey?: string, descriptionKey?: string): void { + this.apply({ + titleKey: titleKey ?? 'meta.pageTitle', + descriptionKey: descriptionKey ?? 'meta.defaultDescription', + url: this.#document.location?.pathname ?? '/', + }); + } + + #setMeta(name: string, content: string, isProperty = false): void { + const selector = isProperty ? `property='${name}'` : `name='${name}'`; + if (this.#meta.getTag(selector)) { + this.#meta.updateTag( + isProperty ? { property: name, content } : { name, content }, + ); + return; + } + + this.#meta.addTag( + isProperty ? { property: name, content } : { name, content }, + ); + } + + #absoluteUrl(path: string): string { + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + + const origin = this.#document.location?.origin ?? 'http://localhost:4200'; + return `${origin}${path.startsWith('/') ? path : `/${path}`}`; + } +} diff --git a/src/shop/src/app/core/shipping-widget.service.ts b/src/shop/src/app/core/shipping-widget.service.ts new file mode 100644 index 0000000..f362841 --- /dev/null +++ b/src/shop/src/app/core/shipping-widget.service.ts @@ -0,0 +1,53 @@ +import { inject, Injectable } from '@angular/core'; +import { Observable, shareReplay } from 'rxjs'; +import { ApiService } from './api.service'; +import { ShippingWidgetConfig } from './checkout.utils'; + +const DEFAULT_INPOST_SDK = 'https://geowidget.inpost.pl'; + +@Injectable({ providedIn: 'root' }) +export class ShippingWidgetService { + readonly #api = inject(ApiService); + readonly #config$ = this.#api + .get('/shipping/config') + .pipe(shareReplay(1)); + + #loadedSdkBaseUrl: string | null = null; + #loadPromise: Promise | null = null; + + getConfig(): Observable { + return this.#config$; + } + + loadInPostGeowidget(sdkBaseUrl?: string): Promise { + const base = (sdkBaseUrl || DEFAULT_INPOST_SDK).replace(/\/$/, ''); + + if (this.#loadedSdkBaseUrl === base && this.#loadPromise) { + return this.#loadPromise; + } + + // Reload if switching sandbox ↔ production SDK. + document.getElementById('inpost-geowidget-script')?.remove(); + document.getElementById('inpost-geowidget-css')?.remove(); + + this.#loadedSdkBaseUrl = base; + this.#loadPromise = new Promise((resolve, reject) => { + const link = document.createElement('link'); + link.id = 'inpost-geowidget-css'; + link.rel = 'stylesheet'; + link.href = `${base}/inpost-geowidget.css`; + document.head.appendChild(link); + + const script = document.createElement('script'); + script.id = 'inpost-geowidget-script'; + script.src = `${base}/inpost-geowidget.js`; + script.defer = true; + script.onload = () => resolve(); + script.onerror = () => + reject(new Error('InPost Geowidget failed to load')); + document.head.appendChild(script); + }); + + return this.#loadPromise; + } +} diff --git a/src/shop/src/app/core/shop.services.ts b/src/shop/src/app/core/shop.services.ts new file mode 100644 index 0000000..47521e8 --- /dev/null +++ b/src/shop/src/app/core/shop.services.ts @@ -0,0 +1,215 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { map, tap } from 'rxjs'; +import { ApiService } from './api.service'; +import { CartDto } from './models'; +import { normalizeParcelPoint } from './checkout.utils'; + +@Injectable({ providedIn: 'root' }) +export class CartService { + readonly #api = inject(ApiService); + + readonly #cart = signal(null); + readonly cart = this.#cart.asReadonly(); + + readonly itemCount = signal(0); + + load(): void { + this.#api.get('/cart').subscribe({ + next: (cart) => this.#update(cart), + }); + } + + addItem(productId: string, variantId: string, quantity = 1) { + return this.#api + .post('/cart/items', { productId, variantId, quantity }) + .pipe(tap((cart) => this.#update(cart))); + } + + updateItem(itemId: string, quantity: number) { + return this.#api + .put(`/cart/items/${itemId}`, { quantity }) + .pipe(tap((cart) => this.#update(cart))); + } + + removeItem(itemId: string) { + return this.#api + .delete(`/cart/items/${itemId}`) + .pipe(tap((cart) => this.#update(cart))); + } + + #update(cart: CartDto): void { + this.#cart.set(cart); + this.itemCount.set(cart.itemCount); + } +} + +@Injectable({ providedIn: 'root' }) +export class CatalogService { + readonly #api = inject(ApiService); + + getProducts(params: Record) { + return this.#api.get>( + '/products', + params, + ); + } + + getProduct(slug: string) { + return this.#api.get(`/products/${slug}`); + } + + getFilters() { + return this.#api.get('/products/filters'); + } + + getBanner() { + return this.#api.get('/marketing/banner'); + } + + subscribeNewsletter(email: string) { + return this.#api.post('/marketing/newsletter/subscribe', { email }); + } + + validateDiscount( + code: string, + subtotal = 0, + customerEmail?: string, + ) { + return this.#api.post( + '/marketing/discount/validate', + { code, subtotal, customerEmail: customerEmail || null }, + ); + } + + searchParcelPoints( + carrier: string, + query = '', + latitude?: number, + longitude?: number, + ) { + const params: Record = { carrier, query }; + if (latitude != null) { + params['latitude'] = latitude; + } + + if (longitude != null) { + params['longitude'] = longitude; + } + + return this.#api + .get( + '/shipping/parcel-points', + params, + ) + .pipe(map((points) => points.map(normalizeParcelPoint))); + } + + getShippingConfig() { + return this.#api.get( + '/shipping/config', + ); + } +} + +@Injectable({ providedIn: 'root' }) +export class OrderService { + readonly #api = inject(ApiService); + + create(data: { + customerEmail: string; + customerPhone: string; + customerFirstName: string; + customerLastName: string; + shippingStreet: string; + shippingCity: string; + shippingPostalCode: string; + shippingCountry: string; + billingStreet?: string; + billingCity?: string; + billingPostalCode?: string; + billingCountry?: string; + shippingCarrier: string; + shippingServiceType: string; + parcelPointId?: string; + parcelPointName?: string; + parcelPointAddress?: string; + paymentMethod: string; + customerNotes?: string; + discountCode?: string; + locale?: string; + }) { + return this.#api.post('/orders', data); + } + + getOrder(id: string) { + return this.#api.get(`/orders/${id}`); + } + + getMyOrders() { + return this.#api.get('/orders/my'); + } + + initiatePayment(orderId: string) { + return this.#api.post<{ + success: boolean; + redirectUrl: string | null; + errorMessage?: string | null; + }>(`/payments/${orderId}/initiate`, {}); + } + + completeSandboxPayment(orderId: string) { + return this.#api.post<{ + success: boolean; + redirectUrl: string | null; + errorMessage?: string | null; + }>(`/payments/${orderId}/sandbox-complete`, {}); + } + + /** Pulls latest PayU status into our DB (webhook alternative for local/dev). */ + syncPayment(orderId: string) { + return this.#api.post<{ + success: boolean; + payUStatus: string | null; + orderStatus: string | null; + errorMessage?: string | null; + }>(`/payments/${orderId}/sync`, {}); + } +} + +@Injectable({ providedIn: 'root' }) +export class FavoritesService { + readonly #api = inject(ApiService); + + getAll() { + return this.#api.get('/favorites'); + } + + add(productId: string) { + return this.#api.post(`/favorites/${productId}`, {}); + } + + remove(productId: string) { + return this.#api.delete(`/favorites/${productId}`); + } +} + +@Injectable({ providedIn: 'root' }) +export class ReviewService { + readonly #api = inject(ApiService); + + getForProduct(slug: string) { + return this.#api.get( + `/reviews/product/${slug}`, + ); + } + + create(data: { + productSlug: string; + authorName: string; + authorEmail: string; + rating: number; + content: string; + }) { + return this.#api.post('/reviews', data); + } +} diff --git a/src/shop/src/app/features/account/account.component.html b/src/shop/src/app/features/account/account.component.html new file mode 100644 index 0000000..b86f67a --- /dev/null +++ b/src/shop/src/app/features/account/account.component.html @@ -0,0 +1,97 @@ + + +
+ + @if (auth.user(); as user) { + +
+ +
+ +

+ + {{ 'account.welcome' | translate: { name: user.firstName } }} + +

+ +

{{ user.email }}

+ +
+ + + +
+ + + +

{{ 'account.myOrders' | translate }}

+ + @for (order of orders(); track order.id) { + +
+ + {{ order.orderNumber }} + + + {{ ('checkout.status.' + order.status) | translate }} + + + {{ order.total | i18nCurrency }} + + + + {{ 'account.details' | translate }} + + + +
+ + } @empty { + +
+

{{ 'account.noOrders' | translate }}

+ + {{ 'account.browseCta' | translate }} + +
+ + } + + } @else { + +

+ + + + {{ 'auth.login' | translate }} + + + + {{ 'account.loginPrompt' | translate }} + +

+ + } + +
+ +
+ diff --git a/src/shop/src/app/features/account/account.component.scss b/src/shop/src/app/features/account/account.component.scss new file mode 100644 index 0000000..8a34b49 --- /dev/null +++ b/src/shop/src/app/features/account/account.component.scss @@ -0,0 +1,9 @@ +@use '../../../styles/tokens' as *; + +.order-row { + display: grid; + grid-template-columns: 2fr 1fr 1fr auto; + gap: 16px; + padding: 16px 0; + border-bottom: 1px solid $color-grey-light; +} diff --git a/src/shop/src/app/features/account/account.components.ts b/src/shop/src/app/features/account/account.components.ts new file mode 100644 index 0000000..a292b69 --- /dev/null +++ b/src/shop/src/app/features/account/account.components.ts @@ -0,0 +1,173 @@ +import { + + ChangeDetectionStrategy, + + Component, + + inject, + + OnInit, + + signal, + +} from '@angular/core'; + +import { RouterLink } from '@angular/router'; + +import { + + LucideLightbulb, + + LucideLogOut, + + LucideShoppingBag, + + LucideX, + +} from '@lucide/angular'; + +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; + +import { TranslatePipe } from '../../core/i18n/translate.pipe'; + +import { ShellComponent } from '../../shared/layout/layout.components'; + +import { AuthService } from '../../core/auth.service'; + +import { CartService, FavoritesService, OrderService } from '../../core/shop.services'; + +import { FavoriteDto, OrderSummary } from '../../core/models'; + + + +@Component({ + + selector: 'app-favorites', + + standalone: true, + + imports: [ + ShellComponent, + I18nCurrencyPipe, + TranslatePipe, + RouterLink, + LucideX, + LucideLightbulb, + LucideShoppingBag, + ], + + templateUrl: './favorites.component.html', + + changeDetection: ChangeDetectionStrategy.OnPush, + +}) + +export class FavoritesComponent implements OnInit { + + readonly #favorites = inject(FavoritesService); + + readonly #cart = inject(CartService); + + readonly items = signal([]); + + + + ngOnInit(): void { + + this.#favorites.getAll().subscribe({ + + next: (items) => this.items.set(items), + + }); + + } + + + + remove(productId: string): void { + + this.#favorites.remove(productId).subscribe({ + + next: () => + + this.items.update((list) => list.filter((i) => i.productId !== productId)), + + }); + + } + + + + addToCart(productId: string): void { + + this.#cart.addItem(productId, productId).subscribe(); + + } + +} + + + +@Component({ + + selector: 'app-account', + + standalone: true, + + imports: [ + + ShellComponent, + + I18nCurrencyPipe, + + TranslatePipe, + + RouterLink, + + LucideLogOut, + + ], + + templateUrl: './account.component.html', + + changeDetection: ChangeDetectionStrategy.OnPush, + +}) + +export class AccountComponent implements OnInit { + + readonly auth = inject(AuthService); + + readonly #orders = inject(OrderService); + + readonly orders = signal([]); + + + + ngOnInit(): void { + + this.auth.loadProfile(); + + if (this.auth.isLoggedIn()) { + + this.#orders.getMyOrders().subscribe({ + + next: (o) => this.orders.set(o), + + }); + + } + + } + + + + logout(): void { + + this.auth.logout(); + + } + +} + + diff --git a/src/shop/src/app/features/account/favorites.component.html b/src/shop/src/app/features/account/favorites.component.html new file mode 100644 index 0000000..829b69d --- /dev/null +++ b/src/shop/src/app/features/account/favorites.component.html @@ -0,0 +1,79 @@ + + +
+ +

{{ 'favorites.title' | translate }}

+ + @for (item of items(); track item.id) { + +
+ + + + + +
+ +

{{ item.productName }}

+ +

{{ item.shortDescription }}

+ + {{ item.price | i18nCurrency }} + +
+ + + +
+ + } @empty { + +
+

{{ 'favorites.empty' | translate }}

+ + {{ 'favorites.browseCta' | translate }} + +
+ + } + +
+ +
+ diff --git a/src/shop/src/app/features/account/favorites.component.scss b/src/shop/src/app/features/account/favorites.component.scss new file mode 100644 index 0000000..6a83e75 --- /dev/null +++ b/src/shop/src/app/features/account/favorites.component.scss @@ -0,0 +1,22 @@ +@use '../../../styles/tokens' as *; + +.fav-item { + display: grid; + grid-template-columns: auto 120px 1fr auto; + gap: 24px; + align-items: center; + padding: 24px; + background: $color-white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + margin-bottom: 16px; +} + +.fav-item__image { + width: 120px; + height: 120px; + background: $color-bg; + display: flex; + align-items: center; + justify-content: center; + font-size: 48px; +} diff --git a/src/shop/src/app/features/auth/login.component.html b/src/shop/src/app/features/auth/login.component.html new file mode 100644 index 0000000..2cefd5d --- /dev/null +++ b/src/shop/src/app/features/auth/login.component.html @@ -0,0 +1,144 @@ + +
+
+
+

+ {{ + (isRegister() ? 'auth.registerTitle' : 'auth.loginTitle') | translate + }} +

+ +
+
+ + +
+ +
+ +
+ + +
+
+ + @if (isRegister()) { +
+ + +
+
+ + +
+ } + + @if (error()) { + + } + + +
+ + @if (!isRegister()) { + + {{ 'auth.forgotPasswordMailto' | translate }} + + } + +

+ {{ 'auth.secureHint' | translate }} +

+ + {{ 'auth.guestCheckout' | translate }} + +
+ +
+

+ {{ + (isRegister() ? 'auth.haveAccountTitle' : 'auth.noAccountTitle') + | translate + }} +

+

+ {{ + (isRegister() ? 'auth.haveAccountBody' : 'auth.noAccountBody') + | translate + }} +

+ +
+
+
+
diff --git a/src/shop/src/app/features/auth/login.component.ts b/src/shop/src/app/features/auth/login.component.ts new file mode 100644 index 0000000..dc6bc24 --- /dev/null +++ b/src/shop/src/app/features/auth/login.component.ts @@ -0,0 +1,76 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + signal, +} from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { I18nService } from '../../core/i18n/i18n.service'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ShellComponent } from '../../shared/layout/layout.components'; +import { AuthService } from '../../core/auth.service'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [ShellComponent, FormsModule, TranslatePipe, RouterLink], + templateUrl: './login.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoginComponent { + readonly #auth = inject(AuthService); + readonly #router = inject(Router); + readonly #i18n = inject(I18nService); + + readonly error = signal(''); + readonly submitting = signal(false); + readonly isRegister = signal(false); + readonly showPassword = signal(false); + + email = ''; + password = ''; + firstName = ''; + lastName = ''; + + submit(): void { + if (this.submitting()) { + return; + } + + this.error.set(''); + this.submitting.set(true); + + if (this.isRegister()) { + this.#auth + .register({ + email: this.email, + password: this.password, + firstName: this.firstName, + lastName: this.lastName, + }) + .subscribe({ + next: () => { + this.submitting.set(false); + this.#router.navigate(['/account']); + }, + error: () => { + this.submitting.set(false); + this.error.set(this.#i18n.t('auth.registerError')); + }, + }); + return; + } + + this.#auth.login(this.email, this.password).subscribe({ + next: () => { + this.submitting.set(false); + this.#router.navigate(['/account']); + }, + error: () => { + this.submitting.set(false); + this.error.set(this.#i18n.t('auth.loginError')); + }, + }); + } +} diff --git a/src/shop/src/app/features/cart/cart.component.html b/src/shop/src/app/features/cart/cart.component.html new file mode 100644 index 0000000..04b0fac --- /dev/null +++ b/src/shop/src/app/features/cart/cart.component.html @@ -0,0 +1,256 @@ + +
+

+ {{ 'cart.title' | translate }} +

+ + @if (cart(); as c) { + @if (c.itemCount === 0) { +
+

+ {{ 'cart.emptyTitle' | translate }} +

+

{{ 'cart.emptyBody' | translate }}

+ + {{ 'cart.emptyCta' | translate }} + +
+ } @else { +

+ {{ + 'cart.summary' + | translate: { + count: c.itemCount, + amount: i18n.formatCurrency(c.subtotal), + } + }} +

+ +
+ +
+ +
+
+ @for (item of c.items; track item.id) { +
+ + + + @if (item.imageUrl) { + + } @else { + iLED + } + + +
+

+ + {{ item.productName }} + +

+

{{ item.variantColor }}

+ @if (item.isInStock) { + + } + + +
+
+ + + {{ item.quantity }} + + +
+
+ {{ item.lineTotal | i18nCurrency }} +
+
+
+ + + + +
+ } +
+ + +
+ +
+
+ {{ 'cart.total' | translate }} + {{ c.subtotal | i18nCurrency }} +
+ + {{ 'cart.checkout' | translate }} + +
+ + } + } @else { +

{{ 'common.loading' | translate }}

+ } + + @if (recommended().length > 0) { +
+

+ {{ 'cart.recommended' | translate }} +

+
+ @for (product of recommended(); track product.id) { + + } +
+
+ } +
+
diff --git a/src/shop/src/app/features/cart/cart.component.scss b/src/shop/src/app/features/cart/cart.component.scss new file mode 100644 index 0000000..7a0ef51 --- /dev/null +++ b/src/shop/src/app/features/cart/cart.component.scss @@ -0,0 +1,91 @@ +@use '../../../styles/tokens' as *; + +.cart__layout { + display: grid; + grid-template-columns: 1fr 340px; + gap: 40px; + margin-top: 24px; +} + +.cart__item { + display: grid; + grid-template-columns: auto 100px 1fr auto auto; + gap: 16px; + align-items: center; + padding: 24px 0; + border-bottom: 1px solid $color-grey-light; +} + +.cart__image { + width: 100px; + height: 100px; + background: $color-bg; + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; +} + +.cart__stock { color: $color-success; font-size: $font-size-small; } + +.cart__qty { + display: flex; + align-items: center; + gap: 8px; + + button { + width: 32px; + height: 32px; + border: 1px solid $color-grey-light; + background: $color-white; + cursor: pointer; + } +} + +.cart__sidebar { + background: $color-bg; + padding: 24px; + height: fit-content; +} + +.cart__checkout { + display: block; + width: 100%; + margin-bottom: 24px; + text-align: center; +} + +.cart__discount-row { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.cart__totals div { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + font-size: $font-size-small; +} + +.cart__total { + font-weight: 700; + font-size: $font-size-body !important; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid $color-grey-light; +} + +.cart__grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; + margin-top: 24px; +} + +.cart__recommended { margin-top: 64px; } + +@media (max-width: 900px) { + .cart__layout { grid-template-columns: 1fr; } + .cart__grid { grid-template-columns: repeat(2, 1fr); } +} diff --git a/src/shop/src/app/features/cart/cart.component.ts b/src/shop/src/app/features/cart/cart.component.ts new file mode 100644 index 0000000..c92c46a --- /dev/null +++ b/src/shop/src/app/features/cart/cart.component.ts @@ -0,0 +1,141 @@ +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + signal, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { + LucideCheck, + LucideClock, + LucideMinus, + LucidePlus, + LucideTrash2, +} from '@lucide/angular'; +import { FREE_DELIVERY_THRESHOLD } from '../../core/constants'; +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; +import { I18nService } from '../../core/i18n/i18n.service'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { promoErrorMessage } from '../../core/promo-errors'; +import { ProductImagePipe } from '../../core/product-image.pipe'; +import { + ShellComponent, + ProductCardComponent, +} from '../../shared/layout/layout.components'; +import { FreeShippingBarComponent } from '../../shared/free-shipping-bar.component'; +import { CartService, CatalogService } from '../../core/shop.services'; +import { ProductListItem } from '../../core/models'; + +@Component({ + selector: 'app-cart-page', + standalone: true, + imports: [ + ShellComponent, + ProductCardComponent, + FreeShippingBarComponent, + FormsModule, + I18nCurrencyPipe, + ProductImagePipe, + TranslatePipe, + RouterLink, + LucideTrash2, + LucideCheck, + LucideClock, + LucideMinus, + LucidePlus, + ], + templateUrl: './cart.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CartPageComponent { + readonly #cart = inject(CartService); + readonly #catalog = inject(CatalogService); + readonly #i18n = inject(I18nService); + readonly i18n = this.#i18n; + readonly cart = this.#cart.cart; + readonly recommended = signal([]); + discountCode = ''; + readonly discountMessage = signal(''); + readonly error = signal(''); + readonly updating = signal(false); + readonly discountSubmitting = signal(false); + readonly freeShipThreshold = FREE_DELIVERY_THRESHOLD; + + constructor() { + effect(() => { + this.#i18n.locale(); + this.#cart.load(); + this.#catalog.getProducts({ pageSize: 8 }).subscribe({ + next: (res) => + this.recommended.set( + res.items.filter((p) => p.isInStock).slice(0, 4), + ), + }); + }); + } + + updateQty(itemId: string, qty: number): void { + if (this.updating()) { + return; + } + + if (qty < 1) { + this.remove(itemId); + return; + } + + this.updating.set(true); + this.error.set(''); + this.#cart.updateItem(itemId, qty).subscribe({ + next: () => this.updating.set(false), + error: () => { + this.updating.set(false); + this.error.set(this.#i18n.t('cart.updateError')); + }, + }); + } + + remove(itemId: string): void { + if (this.updating()) { + return; + } + + this.updating.set(true); + this.error.set(''); + this.#cart.removeItem(itemId).subscribe({ + next: () => this.updating.set(false), + error: () => { + this.updating.set(false); + this.error.set(this.#i18n.t('cart.updateError')); + }, + }); + } + + applyDiscount(): void { + if (this.discountSubmitting()) { + return; + } + + this.discountSubmitting.set(true); + this.error.set(''); + const cart = this.#cart.cart(); + this.#catalog + .validateDiscount(this.discountCode, cart?.subtotal ?? 0) + .subscribe({ + next: (res) => { + this.discountSubmitting.set(false); + this.discountMessage.set( + res.isValid + ? this.#i18n.t('cart.discountCheckoutHint') + : promoErrorMessage(this.#i18n, res), + ); + }, + error: () => { + this.discountSubmitting.set(false); + this.error.set(this.#i18n.t('cart.discountInvalid')); + }, + }); + } +} diff --git a/src/shop/src/app/features/checkout/checkout.component.html b/src/shop/src/app/features/checkout/checkout.component.html new file mode 100644 index 0000000..c07b344 --- /dev/null +++ b/src/shop/src/app/features/checkout/checkout.component.html @@ -0,0 +1,407 @@ + +
+

{{ 'checkout.title' | translate }}

+

{{ 'checkout.subtitle' | translate }}

+
+ +
+ + @if (cart(); as c) { +
+
+
+ + {{ 'checkout.contactDetails' | translate }} + +
+ + + + +
+
+ +
+ + {{ 'checkout.shippingAddress' | translate }} + +
+ + + + +
+
+ +
+ + {{ 'checkout.billingAddress' | translate }} + + @if (isPointDelivery()) { +

+ {{ 'checkout.billingInvoiceHint' | translate }} +

+ } + + @if (!billingSameAsShipping || isPointDelivery()) { +
+ + + + +
+ } +
+ +
+ + {{ 'checkout.carrier' | translate }} + +
+ @for (carrier of shippingCarriers; track carrier) { + + } +
+ +
+ + +
+ + @if (isPointDelivery()) { + + } +
+ +
+ + {{ 'checkout.paymentMethod' | translate }} + +
+ @for (method of paymentMethods; track method) { + + } +
+
+ +
+ + +
+
+ + +
+ } @else { +

{{ 'checkout.emptyCart' | translate }}

+ } +
+
diff --git a/src/shop/src/app/features/checkout/checkout.component.scss b/src/shop/src/app/features/checkout/checkout.component.scss new file mode 100644 index 0000000..d96fd0e --- /dev/null +++ b/src/shop/src/app/features/checkout/checkout.component.scss @@ -0,0 +1,30 @@ +@use '../../../styles/tokens' as *; + +.checkout__form { + max-width: 500px; + display: flex; + flex-direction: column; + gap: 24px; + + fieldset { + border: 1px solid $color-grey-light; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + } + + legend { + font-weight: 600; + padding: 0 8px; + } +} + +.checkout__total { + font-size: $font-size-h4; + font-weight: 700; +} + +.checkout__error { + color: #dc2626; +} diff --git a/src/shop/src/app/features/checkout/checkout.component.ts b/src/shop/src/app/features/checkout/checkout.component.ts new file mode 100644 index 0000000..026debb --- /dev/null +++ b/src/shop/src/app/features/checkout/checkout.component.ts @@ -0,0 +1,412 @@ +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + OnInit, + signal, +} from '@angular/core'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { firstValueFrom } from 'rxjs'; +import { + LucideBanknote, + LucideCreditCard, + LucidePackage, + LucideTruck, + LucideWallet, +} from '@lucide/angular'; +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; +import { I18nService } from '../../core/i18n/i18n.service'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { + promoErrorFromHttp, + promoErrorMessage, +} from '../../core/promo-errors'; +import { ShellComponent } from '../../shared/layout/layout.components'; +import { DeliveryPointPickerComponent } from '../../shared/delivery-point-picker.component'; +import { TrustStripComponent } from '../../shared/trust-strip.component'; +import { + CartService, + CatalogService, + OrderService, +} from '../../core/shop.services'; +import { OrderDto } from '../../core/models'; +import { AuthService } from '../../core/auth.service'; +import { FREE_DELIVERY_THRESHOLD } from '../../core/constants'; +import { + defaultServiceType, + enumKey, + getCodFee, + getDeliveryCost, + getOrderTotal, + ParcelPoint, + PAYMENT_METHODS, + PaymentMethod, + SHIPPING_CARRIERS, + ShippingCarrier, + ShippingServiceType, +} from '../../core/checkout.utils'; + +@Component({ + selector: 'app-checkout', + standalone: true, + imports: [ + ShellComponent, + DeliveryPointPickerComponent, + TrustStripComponent, + FormsModule, + TranslatePipe, + I18nCurrencyPipe, + LucideCreditCard, + LucideTruck, + LucidePackage, + LucideBanknote, + LucideWallet, + ], + templateUrl: './checkout.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CheckoutComponent implements OnInit { + readonly #orders = inject(OrderService); + readonly #cart = inject(CartService); + readonly #catalog = inject(CatalogService); + readonly #auth = inject(AuthService); + readonly #router = inject(Router); + readonly #i18n = inject(I18nService); + + readonly i18n = this.#i18n; + readonly cart = this.#cart.cart; + readonly error = signal(''); + readonly submitting = signal(false); + readonly discountValid = signal(false); + readonly freeShipping = signal(false); + readonly discountAmount = signal(0); + readonly discountMessage = signal(''); + readonly selectedParcelPoint = signal(null); + + readonly shippingCarriers = SHIPPING_CARRIERS; + readonly paymentMethods = PAYMENT_METHODS; + readonly freeShipThreshold = FREE_DELIVERY_THRESHOLD; + + freeShipRemaining(subtotal: number): number { + return Math.max(0, FREE_DELIVERY_THRESHOLD - subtotal); + } + + email = ''; + phone = ''; + firstName = ''; + lastName = ''; + street = ''; + city = ''; + postalCode = ''; + country = 'PL'; + billingSameAsShipping = true; + billingStreet = ''; + billingCity = ''; + billingPostalCode = ''; + billingCountry = 'PL'; + customerNotes = ''; + discountCode = ''; + shippingCarrier: ShippingCarrier = 'InPost'; + shippingServiceType: ShippingServiceType = 'ParcelLocker'; + paymentMethod: PaymentMethod = 'PayU'; + + constructor() { + effect(() => { + if (this.cart()) { + this.refreshSummary(); + } + }); + } + + readonly summary = signal<{ + delivery: number; + codFee: number; + discount: number; + total: number; + } | null>(null); + + ngOnInit(): void { + this.#cart.load(); + this.#auth.loadProfile(); + + const user = this.#auth.user(); + if (user) { + this.email = user.email; + this.firstName = user.firstName; + this.lastName = user.lastName; + } + + this.refreshSummary(); + } + + isPointDelivery(): boolean { + return this.shippingServiceType === 'ParcelLocker'; + } + + pickupServiceLabelKey(): string { + return `checkout.service.ParcelLocker.${this.shippingCarrier}`; + } + + handleCarrierChange(carrier: ShippingCarrier): void { + this.shippingCarrier = carrier; + this.shippingServiceType = defaultServiceType(carrier); + this.selectedParcelPoint.set(null); + this.syncBillingForPointDelivery(); + this.refreshSummary(); + } + + handleServiceTypeChange(type: ShippingServiceType): void { + this.shippingServiceType = type; + if (!this.isPointDelivery()) { + this.selectedParcelPoint.set(null); + } + + this.syncBillingForPointDelivery(); + this.refreshSummary(); + } + + syncBillingForPointDelivery(): void { + if (this.isPointDelivery()) { + this.billingSameAsShipping = false; + } + } + + handlePointSelected(point: ParcelPoint): void { + this.selectedParcelPoint.set(point); + this.error.set(''); + } + + refreshSummary(): void { + const cart = this.cart(); + if (!cart) { + this.summary.set(null); + return; + } + + const discount = this.discountAmount(); + const afterDiscount = Math.max(0, cart.subtotal - discount); + const delivery = getDeliveryCost( + this.shippingCarrier, + this.shippingServiceType, + afterDiscount, + this.freeShipping(), + ); + const codFee = getCodFee(this.paymentMethod); + const total = getOrderTotal( + cart.subtotal, + this.shippingCarrier, + this.shippingServiceType, + this.paymentMethod, + discount, + this.freeShipping(), + ); + + this.summary.set({ delivery, codFee, discount, total }); + } + + applyDiscount(): void { + if (!this.discountCode.trim()) { + return; + } + + const cart = this.cart(); + this.#catalog + .validateDiscount( + this.discountCode, + cart?.subtotal ?? 0, + this.email || undefined, + ) + .subscribe({ + next: (res) => { + this.discountValid.set(res.isValid); + this.freeShipping.set(res.isValid && res.freeShipping); + this.discountAmount.set( + res.isValid ? (res.discountAmount ?? 0) : 0, + ); + this.discountMessage.set( + res.isValid ? '' : promoErrorMessage(this.#i18n, res), + ); + this.refreshSummary(); + }, + error: () => { + this.discountValid.set(false); + this.discountAmount.set(0); + this.discountMessage.set(this.#i18n.t('cart.discountInvalid')); + this.refreshSummary(); + }, + }); + } + + submit(): void { + if (this.submitting()) { + return; + } + + if (this.isPointDelivery() && !this.selectedParcelPoint()) { + this.error.set(this.#i18n.t('checkout.parcelPointRequired')); + return; + } + + this.error.set(''); + this.submitting.set(true); + + const point = this.selectedParcelPoint(); + const pointDelivery = this.isPointDelivery() && point; + + const payload = { + customerEmail: this.email, + customerPhone: this.phone, + customerFirstName: this.firstName, + customerLastName: this.lastName, + shippingStreet: pointDelivery ? point.address : this.street, + shippingCity: pointDelivery ? point.city : this.city, + shippingPostalCode: pointDelivery ? point.postalCode : this.postalCode, + shippingCountry: this.country, + billingStreet: this.billingSameAsShipping ? undefined : this.billingStreet, + billingCity: this.billingSameAsShipping ? undefined : this.billingCity, + billingPostalCode: this.billingSameAsShipping + ? undefined + : this.billingPostalCode, + billingCountry: this.billingSameAsShipping + ? undefined + : this.billingCountry, + shippingCarrier: this.shippingCarrier, + shippingServiceType: this.shippingServiceType, + parcelPointId: point?.id, + parcelPointName: point?.name, + parcelPointAddress: point + ? `${point.address}, ${point.postalCode} ${point.city}` + : undefined, + paymentMethod: this.paymentMethod, + customerNotes: this.customerNotes || undefined, + discountCode: this.discountCode || undefined, + locale: this.#i18n.locale(), + }; + + this.#orders.create(payload).subscribe({ + next: (order) => { + if (this.paymentMethod === 'PayU') { + this.#orders.initiatePayment(order.id).subscribe({ + next: (pay) => { + this.submitting.set(false); + if (pay.success && pay.redirectUrl) { + this.#goToPayment(pay.redirectUrl, order.id); + return; + } + + this.error.set( + pay.errorMessage || this.#i18n.t('checkout.orderError'), + ); + void this.#router.navigate(['/order', order.id], { + queryParams: { payment: 'failed' }, + }); + }, + error: () => { + this.submitting.set(false); + this.error.set(this.#i18n.t('checkout.orderError')); + }, + }); + return; + } + + this.submitting.set(false); + this.#router.navigate(['/order', order.id]); + }, + error: (err) => { + this.submitting.set(false); + this.error.set(promoErrorFromHttp(this.#i18n, err)); + }, + }); + } + + /** Internal sandbox paths stay same-origin; live PayU uses absolute URL. */ + #goToPayment(redirectUrl: string, orderId: string): void { + if ( + redirectUrl.startsWith('/') || + redirectUrl.includes('/checkout/payu-sandbox/') + ) { + try { + const url = new URL(redirectUrl, window.location.origin); + void this.#router.navigateByUrl(url.pathname + url.search); + return; + } catch { + void this.#router.navigate(['/order', orderId]); + return; + } + } + + window.location.href = redirectUrl; + } +} + +@Component({ + selector: 'app-order-confirmation', + standalone: true, + imports: [ShellComponent, I18nCurrencyPipe, TranslatePipe, RouterLink], + templateUrl: './order-confirmation.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrderConfirmationComponent implements OnInit { + readonly #orders = inject(OrderService); + readonly #route = inject(ActivatedRoute); + readonly #i18n = inject(I18nService); + + readonly order = signal(null); + readonly syncing = signal(false); + + ngOnInit(): void { + this.#route.paramMap.subscribe((params) => { + const id = params.get('id'); + if (id) { + void this.#loadAndSync(id); + } + }); + } + + async #loadAndSync(orderId: string): Promise { + this.syncing.set(true); + try { + // PayU continueUrl lands here before/without webhook — pull status. + for (let attempt = 0; attempt < 4; attempt++) { + try { + await firstValueFrom(this.#orders.syncPayment(orderId)); + } catch { + // Sync may fail if payment row missing; still show order. + } + + const order = await firstValueFrom(this.#orders.getOrder(orderId)); + this.order.set(order); + + if (order.status === 'Paid' || order.paymentMethod !== 'PayU') { + break; + } + + if (attempt < 3) { + await new Promise((r) => setTimeout(r, 2000)); + } + } + } finally { + this.syncing.set(false); + } + } + + paymentHint(method: string): string { + const key = `checkout.paymentHint.${enumKey(method)}`; + const translated = this.#i18n.t(key); + return translated === key ? '' : translated; + } + + statusLabel(status: string): string { + return `checkout.status.${enumKey(status)}`; + } + + carrierLabel(carrier: string): string { + return `checkout.carrier.${enumKey(carrier)}`; + } + + paymentLabel(method: string): string { + return `checkout.payment.${enumKey(method)}`; + } +} diff --git a/src/shop/src/app/features/checkout/order-confirmation.component.html b/src/shop/src/app/features/checkout/order-confirmation.component.html new file mode 100644 index 0000000..8ad8986 --- /dev/null +++ b/src/shop/src/app/features/checkout/order-confirmation.component.html @@ -0,0 +1,46 @@ + +
+ @if (syncing() && !order()) { +

{{ 'common.loading' | translate }}

+ } + @if (order(); as o) { +

+ {{ 'checkout.thankYou' | translate }} +

+

+ {{ 'checkout.orderNumber' | translate }} + {{ o.orderNumber }} +

+

+ {{ 'checkout.status' | translate }} + {{ statusLabel(o.status) | translate }} + @if (syncing()) { + (…) + } +

+

+ {{ 'checkout.carrier' | translate }}: + {{ carrierLabel(o.shippingCarrier) | translate }} + @if (o.parcelPointName) { + — {{ o.parcelPointName }} + } +

+

+ {{ 'checkout.paymentMethod' | translate }}: + {{ paymentLabel(o.paymentMethod) | translate }} +

+ @if (paymentHint(o.paymentMethod)) { +

+ {{ paymentHint(o.paymentMethod) }} +

+ } +

{{ o.total | i18nCurrency }}

+ + {{ 'checkout.backToShop' | translate }} + + } +
+
diff --git a/src/shop/src/app/features/checkout/payu-sandbox.component.html b/src/shop/src/app/features/checkout/payu-sandbox.component.html new file mode 100644 index 0000000..1c7e8db --- /dev/null +++ b/src/shop/src/app/features/checkout/payu-sandbox.component.html @@ -0,0 +1,52 @@ + +
+
+

+ {{ 'checkout.payuSandbox.badge' | translate }} +

+

+ {{ 'checkout.payuSandbox.title' | translate }} +

+

+ {{ 'checkout.payuSandbox.subtitle' | translate }} +

+ + @if (order(); as o) { +

+ {{ 'checkout.orderNumber' | translate }} + {{ o.orderNumber }} +

+

{{ o.total | i18nCurrency }}

+ + @if (error()) { + + } + + + + } @else if (!error()) { +

{{ 'common.loading' | translate }}

+ } +
+
+
diff --git a/src/shop/src/app/features/checkout/payu-sandbox.component.ts b/src/shop/src/app/features/checkout/payu-sandbox.component.ts new file mode 100644 index 0000000..ab6777d --- /dev/null +++ b/src/shop/src/app/features/checkout/payu-sandbox.component.ts @@ -0,0 +1,79 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, + signal, +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ShellComponent } from '../../shared/layout/layout.components'; +import { OrderService } from '../../core/shop.services'; +import { OrderDto } from '../../core/models'; + +@Component({ + selector: 'app-payu-sandbox', + standalone: true, + imports: [ShellComponent, TranslatePipe, I18nCurrencyPipe], + templateUrl: './payu-sandbox.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PayuSandboxComponent implements OnInit { + readonly #orders = inject(OrderService); + readonly #route = inject(ActivatedRoute); + readonly #router = inject(Router); + + readonly order = signal(null); + readonly submitting = signal(false); + readonly error = signal(''); + + ngOnInit(): void { + const id = this.#route.snapshot.paramMap.get('orderId'); + if (!id) { + return; + } + + this.#orders.getOrder(id).subscribe({ + next: (o) => this.order.set(o), + error: () => this.error.set('checkout.orderError'), + }); + } + + handlePay(): void { + const o = this.order(); + if (!o || this.submitting()) { + return; + } + + this.submitting.set(true); + this.error.set(''); + this.#orders.completeSandboxPayment(o.id).subscribe({ + next: (res) => { + this.submitting.set(false); + if (res.success && res.redirectUrl) { + if (res.redirectUrl.startsWith('/')) { + void this.#router.navigateByUrl(res.redirectUrl); + return; + } + + window.location.href = res.redirectUrl; + return; + } + + void this.#router.navigate(['/order', o.id]); + }, + error: () => { + this.submitting.set(false); + this.error.set('checkout.orderError'); + }, + }); + } + + handleCancel(): void { + const o = this.order(); + if (o) { + void this.#router.navigate(['/order', o.id]); + } + } +} diff --git a/src/shop/src/app/features/home/home.component.html b/src/shop/src/app/features/home/home.component.html new file mode 100644 index 0000000..61e3db1 --- /dev/null +++ b/src/shop/src/app/features/home/home.component.html @@ -0,0 +1,90 @@ + +
+
+
+

+ iLED +

+

+ {{ 'home.heroTitle' | translate }} +

+

+ {{ 'home.heroSubtitle' | translate }} +

+ + {{ 'home.cta' | translate }} + +
+ +
+
+ +
+
+ +
+
+ +
+
+

+ {{ 'home.bestsellers' | translate }} +

+ + {{ 'home.viewAllBestsellers' | translate }} + +
+ @if (bestsellers().length === 0) { +

{{ 'common.loading' | translate }}

+ } @else { +
+ @for (product of bestsellers(); track product.id) { + + } +
+ } +
+ +
+
+

+ {{ 'home.newArrivals' | translate }} +

+ + {{ 'home.viewAllNew' | translate }} + +
+ @if (newArrivals().length === 0) { +

{{ 'common.loading' | translate }}

+ } @else { +
+ @for (product of newArrivals(); track product.id) { + + } +
+ } +
+
diff --git a/src/shop/src/app/features/home/home.component.ts b/src/shop/src/app/features/home/home.component.ts new file mode 100644 index 0000000..0847100 --- /dev/null +++ b/src/shop/src/app/features/home/home.component.ts @@ -0,0 +1,52 @@ +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + signal, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { LucideLightbulb } from '@lucide/angular'; +import { I18nService } from '../../core/i18n/i18n.service'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ShellComponent, ProductCardComponent } from '../../shared/layout/layout.components'; +import { TrustStripComponent } from '../../shared/trust-strip.component'; +import { CatalogService } from '../../core/shop.services'; +import { ProductListItem } from '../../core/models'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [ + ShellComponent, + ProductCardComponent, + TrustStripComponent, + RouterLink, + LucideLightbulb, + TranslatePipe, + ], + templateUrl: './home.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HomeComponent { + readonly #catalog = inject(CatalogService); + readonly #i18n = inject(I18nService); + readonly bestsellers = signal([]); + readonly newArrivals = signal([]); + + constructor() { + effect(() => { + this.#i18n.locale(); + this.#load(); + }); + } + + #load(): void { + this.#catalog.getProducts({ isBestseller: true, pageSize: 8 }).subscribe({ + next: (res) => this.bestsellers.set(res.items.slice(0, 4)), + }); + this.#catalog.getProducts({ isNewArrival: true, pageSize: 8 }).subscribe({ + next: (res) => this.newArrivals.set(res.items.slice(0, 4)), + }); + } +} diff --git a/src/shop/src/app/features/legal/legal-page.component.html b/src/shop/src/app/features/legal/legal-page.component.html new file mode 100644 index 0000000..94606d5 --- /dev/null +++ b/src/shop/src/app/features/legal/legal-page.component.html @@ -0,0 +1,100 @@ + + +
+ +

{{ titleKey() | translate }}

+ + + + @if (isContact()) { + +

+ + + + kontakt@iled.pl + + + +

+ + } + + + + @if (isFaq()) { + +

+ + {{ bodyKey() | translate }} + +

+ +
+ + @for (i of faqItems; track i) { + +
+ + + + {{ faqQuestionKey(i) | translate }} + + + +

+ + {{ faqAnswerKey(i) | translate }} + +

+ +
+ + } + +
+ + } @else { + +
+ + {{ bodyKey() | translate }} + +
+ + } + + + +

+ + {{ 'notFound.home' | translate }} + +

+ +
+ +
+ diff --git a/src/shop/src/app/features/legal/legal-page.component.ts b/src/shop/src/app/features/legal/legal-page.component.ts new file mode 100644 index 0000000..fd18419 --- /dev/null +++ b/src/shop/src/app/features/legal/legal-page.component.ts @@ -0,0 +1,108 @@ +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; + +import { toSignal } from '@angular/core/rxjs-interop'; + +import { ActivatedRoute, RouterLink } from '@angular/router'; + +import { map } from 'rxjs'; + +import { TranslatePipe } from '../../core/i18n/translate.pipe'; + +import { ShellComponent } from '../../shared/layout/layout.components'; + + + +const LEGAL_PAGES = new Set(['privacy', 'terms']); + +const FAQ_INDICES = [1, 2, 3, 4, 5, 6, 7, 8] as const; + + + +@Component({ + + selector: 'app-legal-page', + + standalone: true, + + imports: [ShellComponent, TranslatePipe, RouterLink], + + templateUrl: './legal-page.component.html', + + changeDetection: ChangeDetectionStrategy.OnPush, + +}) + +export class LegalPageComponent { + + readonly #route = inject(ActivatedRoute); + + readonly page = toSignal( + + this.#route.data.pipe(map((d) => (d['page'] as string) ?? 'privacy')), + + { initialValue: 'privacy' }, + + ); + + + + readonly faqItems = FAQ_INDICES; + + + + readonly titleKey = computed(() => { + + const p = this.page(); + + const prefix = LEGAL_PAGES.has(p) ? 'legal' : 'content'; + + return `${prefix}.${p}.title`; + + }); + + + + readonly bodyKey = computed(() => { + + const p = this.page(); + + const prefix = LEGAL_PAGES.has(p) ? 'legal' : 'content'; + + return `${prefix}.${p}.body`; + + }); + + + + readonly isFaq = computed(() => this.page() === 'faq'); + + + + readonly isContact = computed(() => this.page() === 'contact'); + + + + #faqKey(kind: 'q' | 'a', index: number): string { + + return `content.faq.${kind}${index}`; + + } + + + + faqQuestionKey(index: number): string { + + return this.#faqKey('q', index); + + } + + + + faqAnswerKey(index: number): string { + + return this.#faqKey('a', index); + + } + +} + diff --git a/src/shop/src/app/features/not-found/not-found.component.html b/src/shop/src/app/features/not-found/not-found.component.html new file mode 100644 index 0000000..2fc8345 --- /dev/null +++ b/src/shop/src/app/features/not-found/not-found.component.html @@ -0,0 +1,17 @@ + +
+

404

+

+ {{ 'notFound.title' | translate }} +

+

+ {{ 'notFound.subtitle' | translate }} +

+ +
+
diff --git a/src/shop/src/app/features/not-found/not-found.component.ts b/src/shop/src/app/features/not-found/not-found.component.ts new file mode 100644 index 0000000..1c88e43 --- /dev/null +++ b/src/shop/src/app/features/not-found/not-found.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { ShellComponent } from '../../shared/layout/layout.components'; + +@Component({ + selector: 'app-not-found', + standalone: true, + imports: [ShellComponent, RouterLink, TranslatePipe], + templateUrl: './not-found.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NotFoundComponent {} diff --git a/src/shop/src/app/features/products/product-detail.component.html b/src/shop/src/app/features/products/product-detail.component.html new file mode 100644 index 0000000..efbf413 --- /dev/null +++ b/src/shop/src/app/features/products/product-detail.component.html @@ -0,0 +1,409 @@ + + + @if (product(); as p) { + +
+ +
+ +
+ +
+ + @for (img of p.images; track img.id) { + + + + } + +
+ +
+ + @if (activeImage()) { + + + + } @else { + + + + } + +
+ +
+ + + +
+ + @if (p.isBestseller) { + + + + {{ 'products.bestsellerBadge' | translate }} + + + + } + + @if (p.averageRating) { + + + + } + +

{{ p.name }}

+ +
+ + {{ (selectedVariant()?.price ?? p.price) | i18nCurrency }} + +
+ +

+ + {{ 'products.lowestPrice30Days' | translate }} + + {{ p.lowestPrice30Days | i18nCurrency }} + +

+ + + +
+ + @for (v of p.variants; track v.id) { + + + + } + +
+ + + +
+ + {{ 'products.quantity' | translate }} + +
+ + + {{ quantity() }} + + +
+
+ + + + + + @if (!selectedVariant()?.isInStock) { +

+ {{ 'products.outOfStock' | translate }} +

+ } + +
+ @if (selectedVariant()?.isInStock) { + + + {{ 'products.inStock' | translate }} + + } + + + {{ 'products.deliveryTime' | translate: { hours: p.deliveryHours } }} + + + + {{ 'products.freeDeliveryFrom' | translate }} + +
+ +
+ +
+ +
+ +
+ + + +
+ +

{{ 'products.description' | translate }}

+ +

{{ p.description }}

+ +
+ + + +
+ +

{{ 'products.specs' | translate }}

+ + + + @for (attr of p.attributes; track attr.name) { + + + + + + + + + + } + +
{{ attr.name }}{{ attr.value }}
+ +
+ + + +
+ +

{{ 'products.related' | translate }}

+ +
+ + @for (item of related(); track item.id) { + + + + } + +
+ +
+ + + +
+ +

{{ 'products.reviews' | translate }}

+ + @if (reviews(); as r) { +
+ {{ r.averageRating | number: '1.0-1' }}/5 + + {{ 'products.reviewsCount' | translate: { count: r.totalCount } }} + +
+ @if (r.totalCount === 0) { +

+ {{ 'products.reviewsEmpty' | translate }} +

+ } + @for (review of r.reviews; track review.id) { +
+ + {{ review.authorName }} +

{{ review.content }}

+
+ } + } + +
+ +

{{ 'products.yourRating' | translate }}

+ + + + + + + + + + + +
+ +
+ +
+ + } + + @if (product(); as stickyProduct) { +
+
+ + {{ stickyProduct.name }} — + {{ (selectedVariant()?.price ?? stickyProduct.price) | i18nCurrency }} + + +
+
+ } + + + +
+ diff --git a/src/shop/src/app/features/products/product-detail.component.scss b/src/shop/src/app/features/products/product-detail.component.scss new file mode 100644 index 0000000..87b9ba4 --- /dev/null +++ b/src/shop/src/app/features/products/product-detail.component.scss @@ -0,0 +1,129 @@ +@use '../../../styles/tokens' as *; + +.detail { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; +} + +.detail__gallery { + display: flex; + gap: 16px; +} + +.detail__thumbs { + display: flex; + flex-direction: column; + gap: 8px; + + button { + border: 2px solid transparent; + padding: 0; + cursor: pointer; + width: 80px; + height: 80px; + overflow: hidden; + background: $color-bg; + + img { width: 100%; height: 100%; object-fit: cover; } + } +} + +.detail__main-image { + flex: 1; + aspect-ratio: 1; + background: $color-bg; + + img { width: 100%; height: 100%; object-fit: cover; } +} + +.detail__placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + font-size: 120px; + opacity: 0.3; +} + +.detail__badge { + font-size: $font-size-small; + color: $color-grey-dark; +} + +.detail__price { + font-size: 32px; + font-weight: 700; + margin: 8px 0; +} + +.detail__omnibus { + font-size: $font-size-small; + color: $color-grey-dark; +} + +.detail__variants { + display: flex; + gap: 12px; + margin: 24px 0; +} + +.detail__swatch { + width: 32px; + height: 32px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + + &.active { border-color: $color-black; } +} + +.detail__cta { + width: 100%; + margin-bottom: 24px; +} + +.detail__meta { + display: flex; + flex-direction: column; + gap: 8px; + font-size: $font-size-small; +} + +.detail__stock { color: $color-success; } + +.detail__section { + grid-column: 1 / -1; + margin-top: 32px; +} + +.detail__specs { + width: 100%; + border-collapse: collapse; + + td { + padding: 12px; + border-bottom: 1px solid $color-grey-light; + + &:first-child { font-weight: 500; width: 40%; } + } +} + +.detail__related { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; +} + +.detail__review-form { + display: flex; + flex-direction: column; + gap: 12px; + max-width: 400px; + margin-top: 24px; +} + +@media (max-width: 900px) { + .detail { grid-template-columns: 1fr; } + .detail__related { grid-template-columns: repeat(2, 1fr); } +} diff --git a/src/shop/src/app/features/products/product-detail.component.ts b/src/shop/src/app/features/products/product-detail.component.ts new file mode 100644 index 0000000..9794e2e --- /dev/null +++ b/src/shop/src/app/features/products/product-detail.component.ts @@ -0,0 +1,218 @@ +import { DecimalPipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + OnInit, + signal, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { + LucideCheck, + LucideClock, + LucideLightbulb, + LucideMinus, + LucidePlus, + LucideTruck, +} from '@lucide/angular'; +import { CartAddedModalComponent } from '../../shared/cart-added-modal.component'; +import { TrustStripComponent } from '../../shared/trust-strip.component'; +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; +import { I18nService } from '../../core/i18n/i18n.service'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { + ShellComponent, + StarRatingComponent, + ProductCardComponent, +} from '../../shared/layout/layout.components'; +import { + CatalogService, + CartService, + ReviewService, +} from '../../core/shop.services'; +import { + ProductDetail, + ProductListItem, + ProductReviews, + ProductVariant, +} from '../../core/models'; +import { SeoService } from '../../core/seo.service'; +import { ProductImagePipe } from '../../core/product-image.pipe'; + +@Component({ + selector: 'app-product-detail', + standalone: true, + imports: [ + ShellComponent, + StarRatingComponent, + ProductCardComponent, + FormsModule, + DecimalPipe, + I18nCurrencyPipe, + ProductImagePipe, + TranslatePipe, + CartAddedModalComponent, + TrustStripComponent, + LucideLightbulb, + LucideCheck, + LucideTruck, + LucideClock, + LucideMinus, + LucidePlus, + ], + templateUrl: './product-detail.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductDetailComponent implements OnInit { + readonly #route = inject(ActivatedRoute); + readonly #catalog = inject(CatalogService); + readonly #cart = inject(CartService); + readonly #reviews = inject(ReviewService); + readonly #seo = inject(SeoService); + readonly #i18n = inject(I18nService); + + readonly product = signal(null); + readonly related = signal([]); + readonly reviews = signal(null); + readonly selectedVariant = signal(null); + readonly activeImage = signal(null); + readonly quantity = signal(1); + readonly cartModalOpen = signal(false); + #slug = ''; + + reviewName = ''; + reviewEmail = ''; + reviewContent = ''; + reviewRating = 3; + + constructor() { + effect(() => { + this.#i18n.locale(); + if (this.#slug) { + this.#loadProduct(this.#slug); + } + }); + } + + ngOnInit(): void { + this.#route.paramMap.subscribe((params) => { + const slug = params.get('slug'); + if (!slug) { + return; + } + this.#slug = slug; + this.#loadProduct(slug); + this.#reviews.getForProduct(slug).subscribe({ + next: (r) => this.reviews.set(r), + }); + }); + } + + #loadProduct(slug: string): void { + this.#catalog.getProduct(slug).subscribe({ + next: (p) => { + this.product.set(p); + const def = p.variants.find((v) => v.isDefault) ?? p.variants[0]; + this.selectedVariant.set(def ?? null); + this.activeImage.set(p.images[0]?.url ?? null); + this.#seo.apply({ + title: `${p.name} — iLED`, + description: p.shortDescription || p.description, + imageUrl: p.images[0]?.url ?? undefined, + url: `/products/${p.slug}`, + type: 'product', + }); + }, + }); + this.#catalog.getProducts({ pageSize: 4 }).subscribe({ + next: (res) => this.related.set(res.items), + }); + } + + selectVariant(variant: ProductVariant): void { + this.selectedVariant.set(variant); + this.quantity.set(1); + } + + maxQuantity(): number { + const variant = this.selectedVariant(); + if (!variant?.isInStock) { + return 1; + } + + const stock = variant.stockQuantity; + if (stock <= 0) { + return 99; + } + + return Math.min(stock, 99); + } + + decreaseQuantity(): void { + if (this.quantity() > 1) { + this.quantity.update((q) => q - 1); + } + } + + increaseQuantity(): void { + if (this.quantity() < this.maxQuantity()) { + this.quantity.update((q) => q + 1); + } + } + + closeCartModal(): void { + this.cartModalOpen.set(false); + } + + selectImage(url: string): void { + this.activeImage.set(url); + } + + activeImageAlt(): string { + const p = this.product(); + if (!p) { + return ''; + } + + const url = this.activeImage(); + const match = p.images.find((img) => img.url === url); + return match?.altText || p.name; + } + + addToCart(): void { + const p = this.product(); + const v = this.selectedVariant(); + if (!p || !v || !v.isInStock) { + return; + } + + this.#cart.addItem(p.id, v.id, this.quantity()).subscribe({ + next: () => this.cartModalOpen.set(true), + }); + } + + submitReview(): void { + const p = this.product(); + if (!p) { + return; + } + this.#reviews + .create({ + productSlug: p.slug, + authorName: this.reviewName, + authorEmail: this.reviewEmail, + rating: this.reviewRating, + content: this.reviewContent, + }) + .subscribe({ + next: () => { + this.reviewContent = ''; + this.#reviews.getForProduct(p.slug).subscribe({ + next: (r) => this.reviews.set(r), + }); + }, + }); + } +} diff --git a/src/shop/src/app/features/products/product-list.component.html b/src/shop/src/app/features/products/product-list.component.html new file mode 100644 index 0000000..7ed3363 --- /dev/null +++ b/src/shop/src/app/features/products/product-list.component.html @@ -0,0 +1,118 @@ + +
+ + + + +
+

+ {{ 'products.title' | translate: { count: totalCount() } }} +

+ + @if (products().length === 0) { +
+

+ {{ 'products.noResults' | translate }} +

+ +
+ } @else { +
+ @for (product of products(); track product.id) { + + } +
+ } +
+
+
diff --git a/src/shop/src/app/features/products/product-list.component.scss b/src/shop/src/app/features/products/product-list.component.scss new file mode 100644 index 0000000..96681b1 --- /dev/null +++ b/src/shop/src/app/features/products/product-list.component.scss @@ -0,0 +1,66 @@ +@use '../../../styles/tokens' as *; + +.product-list { + display: grid; + grid-template-columns: 260px 1fr; + gap: 40px; +} + +.filters { + background: $color-bg; + padding: 24px; + + h2 { + font-size: $font-size-h4; + margin-bottom: 24px; + } +} + +.filters__group { + margin-bottom: 24px; + + label { + display: block; + font-weight: 500; + margin-bottom: 8px; + font-size: $font-size-small; + } +} + +.filters__range { + display: flex; + align-items: center; + gap: 8px; + + input { width: 80px; } +} + +.filters__colors { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.filters__swatch { + padding: 6px 12px; + border: 1px solid $color-grey-light; + background: $color-white; + cursor: pointer; + font-size: 13px; + + &.active { + border-color: $color-black; + font-weight: 600; + } +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +@media (max-width: 900px) { + .product-list { grid-template-columns: 1fr; } + .grid { grid-template-columns: repeat(2, 1fr); } +} diff --git a/src/shop/src/app/features/products/product-list.component.ts b/src/shop/src/app/features/products/product-list.component.ts new file mode 100644 index 0000000..1bec185 --- /dev/null +++ b/src/shop/src/app/features/products/product-list.component.ts @@ -0,0 +1,157 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + OnInit, + signal, +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { I18nService } from '../../core/i18n/i18n.service'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { + ShellComponent, + ProductCardComponent, +} from '../../shared/layout/layout.components'; +import { CatalogService } from '../../core/shop.services'; +import { ProductFilterOptions, ProductListItem } from '../../core/models'; + +@Component({ + selector: 'app-product-list', + standalone: true, + imports: [ShellComponent, ProductCardComponent, FormsModule, TranslatePipe], + templateUrl: './product-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductListComponent implements OnInit { + readonly #catalog = inject(CatalogService); + readonly #route = inject(ActivatedRoute); + readonly #router = inject(Router); + readonly #i18n = inject(I18nService); + + readonly products = signal([]); + readonly filters = signal(null); + readonly totalCount = signal(0); + readonly filtersOpen = signal(false); + + minPrice = 0; + maxPrice = 1000; + selectedColor = ''; + search = ''; + page = 1; + isBestseller = false; + isNewArrival = false; + + readonly activeFilterChips = computed(() => { + const chips: string[] = []; + if (this.selectedColor) { + chips.push(this.colorLabel(this.selectedColor)); + } + if (this.search) { + chips.push(this.search); + } + return chips; + }); + + constructor() { + effect(() => { + this.#i18n.locale(); + this.#loadFilters(); + this.loadProducts(); + }); + } + + ngOnInit(): void { + this.#route.queryParams.subscribe((params) => { + this.search = params['search'] ?? ''; + this.selectedColor = params['color'] ?? ''; + this.isBestseller = params['isBestseller'] === 'true'; + this.isNewArrival = params['isNewArrival'] === 'true'; + if (params['minPrice']) { + this.minPrice = Number(params['minPrice']); + } + if (params['maxPrice']) { + this.maxPrice = Number(params['maxPrice']); + } + this.loadProducts(); + }); + } + + #loadFilters(): void { + this.#catalog.getFilters().subscribe({ + next: (f) => { + this.filters.set(f); + if (!this.#route.snapshot.queryParams['minPrice']) { + this.minPrice = f.minPrice; + } + if (!this.#route.snapshot.queryParams['maxPrice']) { + this.maxPrice = f.maxPrice; + } + }, + }); + } + + colorLabel(color: string): string { + const key = `products.color.${color.toLowerCase()}`; + const translated = this.#i18n.t(key); + return translated === key ? color : translated; + } + + loadProducts(): void { + if (this.minPrice > this.maxPrice) { + const swap = this.minPrice; + this.minPrice = this.maxPrice; + this.maxPrice = swap; + } + + this.#catalog + .getProducts({ + page: this.page, + pageSize: 12, + minPrice: this.minPrice, + maxPrice: this.maxPrice, + color: this.selectedColor, + search: this.search, + isBestseller: this.isBestseller, + isNewArrival: this.isNewArrival, + }) + .subscribe({ + next: (res) => { + this.products.set(res.items); + this.totalCount.set(res.totalCount); + }, + }); + } + + applyFilters(): void { + this.page = 1; + this.filtersOpen.set(false); + void this.#router.navigate([], { + relativeTo: this.#route, + queryParams: { + search: this.search || null, + color: this.selectedColor || null, + minPrice: this.minPrice || null, + maxPrice: this.maxPrice || null, + isBestseller: this.isBestseller ? true : null, + isNewArrival: this.isNewArrival ? true : null, + }, + queryParamsHandling: 'merge', + }); + } + + clearFilters(): void { + this.selectedColor = ''; + this.search = ''; + this.isBestseller = false; + this.isNewArrival = false; + const f = this.filters(); + if (f) { + this.minPrice = f.minPrice; + this.maxPrice = f.maxPrice; + } + void this.#router.navigate(['/products']); + } +} diff --git a/src/shop/src/app/shared/cart-added-modal.component.html b/src/shop/src/app/shared/cart-added-modal.component.html new file mode 100644 index 0000000..b0a2c43 --- /dev/null +++ b/src/shop/src/app/shared/cart-added-modal.component.html @@ -0,0 +1,40 @@ +@if (open()) { + +} diff --git a/src/shop/src/app/shared/cart-added-modal.component.ts b/src/shop/src/app/shared/cart-added-modal.component.ts new file mode 100644 index 0000000..c6177a4 --- /dev/null +++ b/src/shop/src/app/shared/cart-added-modal.component.ts @@ -0,0 +1,41 @@ +import { + ChangeDetectionStrategy, + Component, + HostListener, + input, + output, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; + +@Component({ + selector: 'app-cart-added-modal', + standalone: true, + imports: [RouterLink, TranslatePipe], + templateUrl: './cart-added-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CartAddedModalComponent { + readonly open = input(false); + readonly productName = input(''); + readonly quantity = input(1); + + readonly closed = output(); + + @HostListener('document:keydown.escape') + handleEscape(): void { + if (this.open()) { + this.closed.emit(); + } + } + + handleBackdrop(event: MouseEvent): void { + if (event.target === event.currentTarget) { + this.closed.emit(); + } + } + + handleContinue(): void { + this.closed.emit(); + } +} diff --git a/src/shop/src/app/shared/cookie-banner.component.html b/src/shop/src/app/shared/cookie-banner.component.html new file mode 100644 index 0000000..3631f7e --- /dev/null +++ b/src/shop/src/app/shared/cookie-banner.component.html @@ -0,0 +1,34 @@ +@if (ready() && consent.bannerVisible()) { + +} diff --git a/src/shop/src/app/shared/cookie-banner.component.scss b/src/shop/src/app/shared/cookie-banner.component.scss new file mode 100644 index 0000000..f5bf9c9 --- /dev/null +++ b/src/shop/src/app/shared/cookie-banner.component.scss @@ -0,0 +1,71 @@ +.cookie-banner { + position: fixed; + inset-inline: 0.75rem; + bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px)); + z-index: 60; + max-width: 36rem; + margin-inline: auto; + padding: 1rem 1.125rem; + background: rgb(17 17 17 / 96%); + color: #fff; + box-shadow: 0 12px 40px rgb(0 0 0 / 28%); +} + +.cookie-banner__inner { + display: flex; + flex-direction: column; + gap: 0.875rem; +} + +.cookie-banner__title { + margin: 0 0 0.25rem; + font-size: 0.9375rem; + font-weight: 600; +} + +.cookie-banner__body { + margin: 0; + font-size: 0.8125rem; + line-height: 1.45; + color: #d9d9d9; +} + +.cookie-banner__body a { + color: #ffbc39; +} + +.cookie-banner__actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.cookie-banner__btn { + border: 0; + min-height: 2.5rem; + padding: 0.5rem 1rem; + font-size: 0.875rem; + cursor: pointer; +} + +.cookie-banner__btn--ghost { + background: transparent; + color: #fff; + border: 1px solid #8c8c8c; +} + +.cookie-banner__btn--solid { + background: #ffbc39; + color: #111; + font-weight: 600; +} + +@media (min-width: 640px) { + .cookie-banner { + inset-inline: auto; + right: 1rem; + left: auto; + bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + margin-inline: 0; + } +} diff --git a/src/shop/src/app/shared/cookie-banner.component.ts b/src/shop/src/app/shared/cookie-banner.component.ts new file mode 100644 index 0000000..2cd6af3 --- /dev/null +++ b/src/shop/src/app/shared/cookie-banner.component.ts @@ -0,0 +1,53 @@ +import { + ChangeDetectionStrategy, + Component, + HostListener, + inject, + OnInit, + signal, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { CookieConsentService } from '../core/cookie-consent.service'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; + +@Component({ + selector: 'app-cookie-banner', + standalone: true, + imports: [TranslatePipe, RouterLink], + templateUrl: './cookie-banner.component.html', + styleUrl: './cookie-banner.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.cookie-banner-host--open]': 'ready() && consent.bannerVisible()', + }, +}) +export class CookieBannerComponent implements OnInit { + readonly consent = inject(CookieConsentService); + readonly ready = signal(false); + + ngOnInit(): void { + if (!this.consent.bannerVisible()) { + return; + } + + const reveal = (): void => this.ready.set(true); + const ric = ( + window as Window & { + requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => void; + } + ).requestIdleCallback; + + if (ric) { + ric(reveal, { timeout: 2500 }); + } else { + setTimeout(reveal, 1200); + } + } + + @HostListener('document:keydown.escape') + handleEscape(): void { + if (this.ready() && this.consent.bannerVisible()) { + this.consent.reject(); + } + } +} diff --git a/src/shop/src/app/shared/delivery-point-picker.component.html b/src/shop/src/app/shared/delivery-point-picker.component.html new file mode 100644 index 0000000..215771a --- /dev/null +++ b/src/shop/src/app/shared/delivery-point-picker.component.html @@ -0,0 +1,254 @@ +
+ @if (selectedPoint(); as point) { +
+ +
+

+ {{ 'checkout.selectedPoint' | translate }} +

+

{{ point.name }}

+

+ {{ point.address }}, {{ point.postalCode }} {{ point.city }} +

+ @if (point.openingHours) { +

+ {{ 'checkout.openingHours' | translate }}: {{ point.openingHours }} +

+ } +
+ +
+ } @else { + + } +
+ +@if (isOpen()) { + +} diff --git a/src/shop/src/app/shared/delivery-point-picker.component.scss b/src/shop/src/app/shared/delivery-point-picker.component.scss new file mode 100644 index 0000000..edd96e8 --- /dev/null +++ b/src/shop/src/app/shared/delivery-point-picker.component.scss @@ -0,0 +1,292 @@ +.delivery-point-summary { + margin-top: 1rem; +} + +.delivery-point-card { + display: flex; + align-items: flex-start; + gap: 0.75rem; + width: 100%; + border: 1px solid var(--color-iled-grey-light, #d9d9d9); + background: #fff; + text-align: left; +} + +.delivery-point-card--empty { + padding: 1rem; + cursor: pointer; + transition: border-color 0.2s ease, background 0.2s ease; +} + +.delivery-point-card--empty:hover { + border-color: #111; + background: var(--color-iled-bg, #f5f5f5); +} + +.delivery-point-card--selected { + padding: 1rem; +} + +.delivery-point-card__icon { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + flex-shrink: 0; + background: var(--color-iled-bg, #f5f5f5); +} + +.delivery-point-card__body { + flex: 1; + min-width: 0; +} + +.delivery-point-card__label, +.delivery-point-card__cta { + display: block; +} + +.delivery-point-card__label { + margin: 0 0 0.25rem; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-iled-grey-dark, #8c8c8c); +} + +.delivery-point-card__name { + margin: 0; + font-weight: 600; +} + +.delivery-point-card__address, +.delivery-point-card__meta { + margin: 0.25rem 0 0; + font-size: 0.875rem; + color: var(--color-iled-grey-dark, #8c8c8c); +} + +.delivery-point-card__action { + flex-shrink: 0; + border: 0; + background: transparent; + font-size: 0.875rem; + font-weight: 600; + text-decoration: underline; + cursor: pointer; +} + +.delivery-point-modal { + position: fixed; + inset: 0; + z-index: 50; + display: flex; + align-items: stretch; + justify-content: center; + padding: 0; + background: rgb(0 0 0 / 55%); +} + +.delivery-point-modal__panel { + display: flex; + flex-direction: column; + width: 100%; + max-width: 72rem; + height: 100%; + background: #fff; +} + +.delivery-point-modal__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--color-iled-grey-light, #d9d9d9); +} + +.delivery-point-modal__title { + margin: 0; + font-size: 1.125rem; + font-weight: 700; +} + +.delivery-point-modal__subtitle { + margin: 0.25rem 0 0; + font-size: 0.875rem; + color: var(--color-iled-grey-dark, #8c8c8c); +} + +.delivery-point-modal__close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border: 0; + background: var(--color-iled-bg, #f5f5f5); + cursor: pointer; +} + +.delivery-point-modal__toolbar { + display: flex; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + border-bottom: 1px solid var(--color-iled-grey-light, #d9d9d9); +} + +.delivery-point-modal__search { + flex: 1; + border: 0; + background: var(--color-iled-bg, #f5f5f5); + padding: 0.75rem 1rem; + font-size: 0.875rem; +} + +.delivery-point-modal__nearby { + display: inline-flex; + align-items: center; + gap: 0.375rem; + border: 0; + background: #111; + color: #fff; + padding: 0.75rem 1rem; + font-size: 0.875rem; + white-space: nowrap; + cursor: pointer; +} + +.delivery-point-modal__error { + margin: 0; + padding: 0.5rem 1.25rem; + font-size: 0.875rem; + color: #b42318; +} + +.delivery-point-modal__content { + display: grid; + grid-template-columns: 1fr; + flex: 1; + min-height: 0; +} + +.delivery-point-modal__map { + min-height: 16rem; + border-bottom: 1px solid var(--color-iled-grey-light, #d9d9d9); +} + +.delivery-point-modal__map app-parcel-point-map, +.delivery-point-modal__map app-parcel-point-map .parcel-point-map { + height: 100%; + min-height: 16rem; +} + +.delivery-point-modal__list { + overflow: auto; + min-height: 0; +} + +.delivery-point-modal__status { + margin: 0; + padding: 1.25rem; + font-size: 0.875rem; + color: var(--color-iled-grey-dark, #8c8c8c); +} + +.delivery-point-modal__items { + margin: 0; + padding: 0; + list-style: none; +} + +.delivery-point-item { + display: block; + width: 100%; + padding: 1rem 1.25rem; + border: 0; + border-bottom: 1px solid var(--color-iled-grey-light, #d9d9d9); + background: #fff; + text-align: left; + cursor: pointer; +} + +.delivery-point-item:hover, +.delivery-point-item--active { + background: var(--color-iled-bg, #f5f5f5); +} + +.delivery-point-item__name { + display: block; + font-weight: 600; +} + +.delivery-point-item__address, +.delivery-point-item__meta { + display: block; + margin-top: 0.25rem; + font-size: 0.875rem; + color: var(--color-iled-grey-dark, #8c8c8c); +} + +.delivery-point-item__meta { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.delivery-point-modal__sandbox { + margin: 0; + padding: 0.75rem 1.25rem; + font-size: 0.875rem; + color: var(--color-iled-grey-dark, #8c8c8c); + background: var(--color-iled-bg, #f5f5f5); + border-bottom: 1px solid var(--color-iled-grey-light, #d9d9d9); +} + +.delivery-point-modal__widget { + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + min-height: 20rem; +} + +.delivery-point-modal__iframe { + flex: 1; + width: 100%; + min-height: 20rem; + border: 0; +} + +@media (min-width: 768px) { + .delivery-point-modal { + padding: 2rem; + } + + .delivery-point-modal__panel { + height: min(90vh, 52rem); + border: 1px solid var(--color-iled-grey-light, #d9d9d9); + } + + .delivery-point-modal__content { + grid-template-columns: minmax(0, 1.2fr) minmax(18rem, 0.8fr); + } + + .delivery-point-modal__map { + border-bottom: 0; + border-right: 1px solid var(--color-iled-grey-light, #d9d9d9); + } + + .delivery-point-modal__map, + .delivery-point-modal__map app-parcel-point-map, + .delivery-point-modal__map app-parcel-point-map .parcel-point-map { + min-height: 100%; + } +} + +@media (max-width: 767px) { + .delivery-point-modal__toolbar { + flex-direction: column; + } +} diff --git a/src/shop/src/app/shared/delivery-point-picker.component.ts b/src/shop/src/app/shared/delivery-point-picker.component.ts new file mode 100644 index 0000000..424402b --- /dev/null +++ b/src/shop/src/app/shared/delivery-point-picker.component.ts @@ -0,0 +1,402 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + effect, + inject, + input, + OnInit, + output, + signal, +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { LucideMapPin, LucideNavigation, LucideX } from '@lucide/angular'; +import { + debounceTime, + distinctUntilChanged, + switchMap, + tap, +} from 'rxjs'; +import { + DeliveryWidgetMode, + inPostGeowidgetConfig, + ParcelPoint, + PaymentMethod, + ShippingCarrier, + ShippingWidgetConfig, +} from '../core/checkout.utils'; +import { I18nService } from '../core/i18n/i18n.service'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; +import { CatalogService } from '../core/shop.services'; +import { InpostGeowidgetComponent } from './inpost-geowidget.component'; +import { ParcelPointMapComponent } from './parcel-point-map.component'; + +@Component({ + selector: 'app-delivery-point-picker', + standalone: true, + imports: [ + FormsModule, + TranslatePipe, + ParcelPointMapComponent, + InpostGeowidgetComponent, + LucideMapPin, + LucideNavigation, + LucideX, + ], + templateUrl: './delivery-point-picker.component.html', + styleUrl: './delivery-point-picker.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeliveryPointPickerComponent implements OnInit { + readonly carrier = input.required(); + readonly paymentMethod = input('PayU'); + readonly selectedPoint = input(null); + readonly autoOpen = input(false); + + readonly pointSelected = output(); + + readonly #catalog = inject(CatalogService); + readonly #i18n = inject(I18nService); + readonly #destroyRef = inject(DestroyRef); + readonly #sanitizer = inject(DomSanitizer); + + readonly isOpen = signal(false); + readonly points = signal([]); + readonly loading = signal(false); + readonly searchQuery = signal(''); + readonly highlightId = signal(''); + readonly widgetConfig = signal(null); + readonly configLoaded = signal(false); + readonly userCoords = signal<{ lat: number; lng: number } | null>(null); + readonly geoError = signal(''); + + searchInput = ''; + #previousCarrier: ShippingCarrier | null = null; + + readonly safeDhlWidgetUrl = computed(() => { + const url = + this.widgetConfig()?.dhl?.widgetUrl ?? + 'https://parcelshop.dhl.pl/mapa'; + return this.#sanitizer.bypassSecurityTrustResourceUrl(url); + }); + + readonly widgetMode = computed((): DeliveryWidgetMode => { + const config = this.widgetConfig(); + const carrier = this.carrier(); + if (!config) { + return 'sandbox'; + } + + // UseSandbox only mocks label APIs — point-picking widgets stay live. + // InPost: prefer Geowidget whenever a token is set (sandbox or prod SDK). + if (carrier === 'InPost' && config.inPost?.publicToken?.trim()) { + return 'inpost'; + } + + if (carrier === 'Dhl' && config.dhl?.enabled !== false) { + return 'dhl'; + } + + if (carrier === 'Dpd' && config.dpd?.enabled) { + return 'dpd'; + } + + return 'sandbox'; + }); + + readonly #messageHandler = (event: MessageEvent): void => { + if (!this.isOpen()) { + return; + } + + const mode = this.widgetMode(); + if (mode === 'dhl') { + const point = this.#parseDhlMessage(event); + if (point) { + this.selectPoint(point); + } + + return; + } + + if (mode === 'dpd') { + const point = this.#parseDpdMessage(event); + if (point) { + this.selectPoint(point); + } + } + }; + + constructor() { + window.addEventListener('message', this.#messageHandler); + this.#destroyRef.onDestroy(() => { + window.removeEventListener('message', this.#messageHandler); + }); + + toObservable(this.searchQuery) + .pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => { + if (this.isOpen()) { + this.loading.set(true); + this.highlightId.set(''); + } + }), + switchMap((query) => + this.#catalog.searchParcelPoints( + this.carrier(), + query, + this.userCoords()?.lat, + this.userCoords()?.lng, + ), + ), + takeUntilDestroyed(this.#destroyRef), + ) + .subscribe({ + next: (points) => { + if (!this.isOpen()) { + return; + } + + this.points.set(points); + this.loading.set(false); + }, + error: () => { + if (!this.isOpen()) { + return; + } + + this.points.set([]); + this.loading.set(false); + }, + }); + + effect(() => { + const carrier = this.carrier(); + if (this.#previousCarrier && this.#previousCarrier !== carrier) { + this.searchQuery.set(''); + this.searchInput = ''; + this.points.set([]); + this.highlightId.set(''); + this.userCoords.set(null); + } + + this.#previousCarrier = carrier; + + if (this.isOpen()) { + this.highlightId.set(this.selectedPoint()?.id ?? ''); + } + }); + } + + ngOnInit(): void { + this.#catalog.getShippingConfig().subscribe({ + next: (config) => { + this.widgetConfig.set(config); + this.configLoaded.set(true); + if (this.autoOpen() && !this.selectedPoint() && !this.isOpen()) { + this.openPicker(); + } + }, + error: () => this.configLoaded.set(true), + }); + } + + useOfficialInPostWidget(): boolean { + return this.widgetMode() === 'inpost'; + } + + useOfficialDhlWidget(): boolean { + return this.widgetMode() === 'dhl'; + } + + useOfficialDpdWidget(): boolean { + return this.widgetMode() === 'dpd'; + } + + inPostConfig(): string { + const config = this.widgetConfig(); + return ( + config?.inPost?.config ?? + inPostGeowidgetConfig(this.paymentMethod()) + ); + } + + inPostToken(): string { + return this.widgetConfig()?.inPost?.publicToken ?? ''; + } + + inPostSdkBaseUrl(): string { + return ( + this.widgetConfig()?.inPost?.sdkBaseUrl ?? + 'https://geowidget.inpost.pl' + ); + } + + pointLabelKey(): string { + return `checkout.parcelPoint.${this.carrier()}`; + } + + openPicker(): void { + this.isOpen.set(true); + document.body.style.overflow = 'hidden'; + this.highlightId.set(this.selectedPoint()?.id ?? ''); + + const mode = this.widgetMode(); + if (mode === 'inpost' || mode === 'dhl') { + return; + } + + const query = this.searchInput.trim(); + if (query === this.searchQuery()) { + this.#loadPoints(query, this.userCoords()); + return; + } + + this.searchQuery.set(query); + } + + closePicker(): void { + this.isOpen.set(false); + document.body.style.overflow = ''; + } + + handleSearchInput(value: string): void { + this.searchInput = value; + this.searchQuery.set(value.trim()); + } + + handleNearMe(): void { + if (!navigator.geolocation) { + this.geoError.set(this.#i18n.t('checkout.geoNotSupported')); + return; + } + + this.geoError.set(''); + this.loading.set(true); + navigator.geolocation.getCurrentPosition( + (pos) => { + this.userCoords.set({ + lat: pos.coords.latitude, + lng: pos.coords.longitude, + }); + this.#loadPoints(this.searchQuery(), this.userCoords()); + }, + () => { + this.loading.set(false); + this.geoError.set(this.#i18n.t('checkout.geoDenied')); + }, + { enableHighAccuracy: true, timeout: 10000 }, + ); + } + + selectPoint(point: ParcelPoint): void { + this.highlightId.set(point.id); + this.pointSelected.emit(point); + this.closePicker(); + } + + handleBackdrop(event: MouseEvent): void { + if (event.target === event.currentTarget) { + this.closePicker(); + } + } + + #loadPoints( + query: string, + coords: { lat: number; lng: number } | null, + ): void { + this.loading.set(true); + this.#catalog + .searchParcelPoints( + this.carrier(), + query, + coords?.lat, + coords?.lng, + ) + .subscribe({ + next: (points) => { + this.points.set(points); + this.loading.set(false); + }, + error: () => { + this.points.set([]); + this.loading.set(false); + }, + }); + } + + #parseDhlMessage(event: MessageEvent): ParcelPoint | null { + try { + const raw = typeof event.data === 'string' + ? JSON.parse(event.data) + : event.data; + + if (!raw?.sap && !raw?.name) { + return null; + } + + const street = [raw.street, raw.streetNo, raw.houseNo] + .filter(Boolean) + .join(' ') + .trim(); + + return { + id: String(raw.sap ?? raw.name), + name: String(raw.name ?? raw.sap), + address: street, + city: String(raw.city ?? ''), + postalCode: String(raw.zip ?? ''), + latitude: Number(raw.latitude ?? 0), + longitude: Number(raw.longitude ?? 0), + }; + } catch { + return null; + } + } + + #parseDpdMessage(event: MessageEvent): ParcelPoint | null { + const data = event.data as { + dpdWidget?: Record; + point_id?: string; + }; + + // Official PL pudofinder posts { point_id }. + if (typeof data?.point_id === 'string' && data.point_id.trim()) { + const id = data.point_id.trim(); + return { + id, + name: id, + address: '', + city: '', + postalCode: '', + latitude: 0, + longitude: 0, + }; + } + + const raw = data?.dpdWidget; + if (!raw) { + return null; + } + + const id = String(raw['id'] ?? raw['pudoId'] ?? raw['code'] ?? ''); + if (!id) { + return null; + } + + return { + id, + name: String(raw['name'] ?? raw['company'] ?? id), + address: String(raw['street'] ?? raw['address'] ?? ''), + city: String(raw['city'] ?? ''), + postalCode: String(raw['postcode'] ?? raw['postalCode'] ?? raw['zip'] ?? ''), + latitude: Number(raw['latitude'] ?? raw['lat'] ?? 0), + longitude: Number(raw['longitude'] ?? raw['lon'] ?? 0), + }; + } +} diff --git a/src/shop/src/app/shared/free-shipping-bar.component.ts b/src/shop/src/app/shared/free-shipping-bar.component.ts new file mode 100644 index 0000000..0315303 --- /dev/null +++ b/src/shop/src/app/shared/free-shipping-bar.component.ts @@ -0,0 +1,96 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + input, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { FREE_DELIVERY_THRESHOLD } from '../core/constants'; +import { I18nCurrencyPipe } from '../core/i18n/i18n-currency.pipe'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; +import { CartService } from '../core/shop.services'; + +/** + * Inline free-shipping progress (cart / checkout only). + * Do not mount in the global shell — that caused homepage layout shift. + */ +@Component({ + selector: 'app-free-shipping-bar', + standalone: true, + imports: [TranslatePipe, I18nCurrencyPipe, RouterLink], + template: ` + @if (visible()) { +
+

+ @if (remaining() > 0) { + {{ + 'cart.freeShipRemaining' + | translate: { amount: remaining() | i18nCurrency } + }} + } @else { + {{ 'cart.freeShipUnlocked' | translate }} + } +

+ @if (remaining() > 0) { +
+
+
+
+ @if (showShopLink()) { + + {{ 'cart.freeShipShopMore' | translate }} + + } +
+ } +
+ } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FreeShippingBarComponent { + readonly #cart = inject(CartService); + readonly showShopLink = input(true); + + readonly visible = computed(() => { + const cart = this.#cart.cart(); + return (cart?.itemCount ?? 0) > 0; + }); + + readonly remaining = computed(() => { + const cart = this.#cart.cart(); + if (!cart || cart.itemCount <= 0) { + return FREE_DELIVERY_THRESHOLD; + } + + return Math.max(0, FREE_DELIVERY_THRESHOLD - cart.subtotal); + }); + + readonly progress = computed(() => { + const cart = this.#cart.cart(); + if (!cart || cart.itemCount <= 0) { + return 0; + } + + return Math.min( + 100, + Math.round((cart.subtotal / FREE_DELIVERY_THRESHOLD) * 100), + ); + }); +} diff --git a/src/shop/src/app/shared/inpost-geowidget.component.ts b/src/shop/src/app/shared/inpost-geowidget.component.ts new file mode 100644 index 0000000..23b680d --- /dev/null +++ b/src/shop/src/app/shared/inpost-geowidget.component.ts @@ -0,0 +1,142 @@ +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA, + DestroyRef, + effect, + ElementRef, + inject, + input, + output, + signal, + viewChild, +} from '@angular/core'; +import { ParcelPoint } from '../core/checkout.utils'; +import { ShippingWidgetService } from '../core/shipping-widget.service'; + +interface InPostGeowidgetPoint { + name?: string; + address?: { line1?: string; line2?: string }; + address_details?: { + city?: string; + post_code?: string; + street?: string; + building_number?: string; + }; + location?: { latitude?: number; longitude?: number }; + opening_hours?: string; + payment_point_descr?: string; +} + +const INPOST_CALLBACK = 'iledInpostPointSelect'; + +@Component({ + selector: 'app-inpost-geowidget', + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + @if (!ready()) { +

{{ loadingLabel() }}

+ } +
+ `, + styles: [ + ` + :host { + display: block; + height: 100%; + min-height: 20rem; + } + + .inpost-geowidget-mount { + display: block; + width: 100%; + height: 100%; + min-height: 20rem; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InpostGeowidgetComponent { + readonly token = input.required(); + readonly config = input('parcelCollect'); + readonly sdkBaseUrl = input('https://geowidget.inpost.pl'); + readonly language = input('pl'); + readonly loadingLabel = input('Loading map…'); + + readonly pointSelected = output(); + + readonly #widgets = inject(ShippingWidgetService); + readonly #destroyRef = inject(DestroyRef); + readonly ready = signal(false); + readonly mount = viewChild>('mount'); + + constructor() { + const handler = (raw: InPostGeowidgetPoint) => { + const point = this.#mapPoint(raw); + if (point) { + this.pointSelected.emit(point); + } + }; + + (window as unknown as Record)[INPOST_CALLBACK] = handler; + this.#destroyRef.onDestroy(() => { + delete (window as unknown as Record)[INPOST_CALLBACK]; + }); + + effect(() => { + const token = this.token().trim(); + const config = this.config(); + const language = this.language(); + const sdkBaseUrl = this.sdkBaseUrl(); + const host = this.mount()?.nativeElement; + if (!token || !host) { + return; + } + + void this.#widgets.loadInPostGeowidget(sdkBaseUrl).then(() => { + this.ready.set(true); + host.replaceChildren(); + + const el = document.createElement('inpost-geowidget'); + el.setAttribute('token', token); + el.setAttribute('config', config); + el.setAttribute('language', language); + el.setAttribute('onpoint', INPOST_CALLBACK); + el.style.display = 'block'; + el.style.width = '100%'; + el.style.height = '100%'; + el.style.minHeight = '20rem'; + host.appendChild(el); + }); + }); + } + + #mapPoint(raw: InPostGeowidgetPoint | undefined): ParcelPoint | null { + if (!raw?.name) { + return null; + } + + const details = raw.address_details; + const street = details?.street + ? `${details.street} ${details.building_number ?? ''}`.trim() + : (raw.address?.line1 ?? ''); + const city = details?.city ?? ''; + const postalCode = details?.post_code ?? ''; + const lat = raw.location?.latitude ?? 0; + const lng = raw.location?.longitude ?? 0; + + return { + id: raw.name, + name: raw.name, + address: street, + city, + postalCode, + latitude: lat, + longitude: lng, + openingHours: raw.opening_hours, + supportsCod: Boolean(raw.payment_point_descr), + }; + } +} diff --git a/src/shop/src/app/shared/layout/footer.component.html b/src/shop/src/app/shared/layout/footer.component.html new file mode 100644 index 0000000..86c1a9f --- /dev/null +++ b/src/shop/src/app/shared/layout/footer.component.html @@ -0,0 +1,72 @@ + diff --git a/src/shop/src/app/shared/layout/footer.component.scss b/src/shop/src/app/shared/layout/footer.component.scss new file mode 100644 index 0000000..0ce580e --- /dev/null +++ b/src/shop/src/app/shared/layout/footer.component.scss @@ -0,0 +1,55 @@ +@use '../../../styles/tokens' as *; + +.footer { + background: $color-black; + color: $color-white; + padding-top: 48px; +} + +.footer__inner { + @include container; + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 32px; + padding-bottom: 48px; +} + +.footer__logo { + font-size: 24px; + font-weight: 900; + margin-bottom: 12px; +} + +.footer__col h4 { + font-size: $font-size-body; + margin-bottom: 16px; +} + +.footer__col a { + display: block; + color: $color-grey-dark; + margin-bottom: 8px; + font-size: $font-size-small; +} + +.footer__bottom { + border-top: 1px solid #333; + padding: 16px 0; +} + +.footer__bottom-inner { + @include container; + display: flex; + justify-content: space-between; + font-size: $font-size-small; + color: $color-grey-dark; +} + +.footer__payments { + display: flex; + gap: 16px; +} + +@media (max-width: 768px) { + .footer__inner { grid-template-columns: 1fr 1fr; } +} diff --git a/src/shop/src/app/shared/layout/header.component.html b/src/shop/src/app/shared/layout/header.component.html new file mode 100644 index 0000000..51d7ce9 --- /dev/null +++ b/src/shop/src/app/shared/layout/header.component.html @@ -0,0 +1,189 @@ +
+ + + @if (searchOpen()) { +
+
+ + + +
+
+ } + + @if (mobileOpen()) { + + } +
diff --git a/src/shop/src/app/shared/layout/header.component.scss b/src/shop/src/app/shared/layout/header.component.scss new file mode 100644 index 0000000..b1b9757 --- /dev/null +++ b/src/shop/src/app/shared/layout/header.component.scss @@ -0,0 +1,94 @@ +@use '../../../styles/tokens' as *; + +.header { + border-bottom: 1px solid $color-grey-light; + background: $color-white; +} + +.header__inner { + @include container; + display: flex; + align-items: center; + justify-content: space-between; + height: $header-height; + gap: 24px; +} + +.header__logo { + font-size: 28px; + font-weight: 900; + display: flex; + align-items: center; + gap: 4px; +} + +.header__bulb { + color: $color-primary; +} + +.header__nav { + display: flex; + gap: 32px; + + a { + font-size: $font-size-small; + font-weight: 500; + letter-spacing: 0.5px; + + &.active { + text-decoration: underline; + text-underline-offset: 6px; + } + } +} + +.header__actions { + display: flex; + align-items: center; + gap: 16px; +} + +.header__search { + position: relative; + + input { + width: 200px; + padding-right: 36px; + background: $color-grey-light; + border: none; + } +} + +.header__search-icon { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + font-size: 14px; +} + +.header__icon { + font-size: 20px; + position: relative; +} + +.header__badge { + position: absolute; + top: -6px; + right: -8px; + background: $color-primary; + color: $color-black; + font-size: 11px; + font-weight: 700; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +@media (max-width: 900px) { + .header__nav { display: none; } + .header__search input { width: 140px; } +} diff --git a/src/shop/src/app/shared/layout/layout.components.ts b/src/shop/src/app/shared/layout/layout.components.ts new file mode 100644 index 0000000..c6d6a36 --- /dev/null +++ b/src/shop/src/app/shared/layout/layout.components.ts @@ -0,0 +1,346 @@ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + effect, + HostListener, + inject, + input, + OnInit, + signal, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Router, RouterLink, RouterLinkActive } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { + LucideHeart, + LucideLightbulb, + LucideSearch, + LucideShoppingBag, + LucideShoppingCart, + LucideStar, + LucideUser, +} from '@lucide/angular'; +import { AuthService } from '../../core/auth.service'; +import { FREE_DELIVERY_THRESHOLD } from '../../core/constants'; +import { I18nService } from '../../core/i18n/i18n.service'; +import { ProductImagePipe } from '../../core/product-image.pipe'; +import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe'; +import { LocaleSwitcherComponent } from '../../core/i18n/locale-switcher.component'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { + CartService, + CatalogService, + FavoritesService, +} from '../../core/shop.services'; +import { ProductListItem, PromoBanner } from '../../core/models'; +import { CartAddedModalComponent } from '../cart-added-modal.component'; + +@Component({ + selector: 'app-promo-banner', + standalone: true, + imports: [TranslatePipe], + template: ` + @if (banner() && !dismissed()) { +
+ {{ banner()!.text }} + @if (banner()!.highlightedCode) { + {{ banner()!.highlightedCode }} + } + +
+ } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PromoBannerComponent { + readonly #catalog = inject(CatalogService); + readonly #i18n = inject(I18nService); + readonly banner = signal(null); + readonly dismissed = signal( + sessionStorage.getItem('iled_promo_dismissed') === '1', + ); + + constructor() { + effect(() => { + this.#i18n.locale(); + this.#catalog.getBanner().subscribe({ + next: (b) => this.banner.set(b), + }); + }); + } + + dismiss(): void { + sessionStorage.setItem('iled_promo_dismissed', '1'); + this.dismissed.set(true); + } +} + +@Component({ + selector: 'app-header', + standalone: true, + imports: [ + RouterLink, + RouterLinkActive, + FormsModule, + TranslatePipe, + LocaleSwitcherComponent, + LucideSearch, + LucideHeart, + LucideShoppingCart, + LucideUser, + LucideLightbulb, + ], + templateUrl: './header.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HeaderComponent { + readonly #router = inject(Router); + readonly auth = inject(AuthService); + readonly cart = inject(CartService); + readonly scrolled = signal(false); + readonly mobileOpen = signal(false); + readonly searchOpen = signal(false); + searchQuery = ''; + + @HostListener('window:scroll') + handleScroll(): void { + this.scrolled.set(window.scrollY > 8); + } + + @HostListener('document:keydown.escape') + handleEscape(): void { + this.searchOpen.set(false); + } + + handleToggleSearch(): void { + this.searchOpen.update((open) => !open); + this.mobileOpen.set(false); + } + + handleSearch(): void { + const q = this.searchQuery.trim(); + this.searchOpen.set(false); + this.mobileOpen.set(false); + this.#router.navigate(['/products'], { + queryParams: q ? { search: q } : {}, + }); + } +} + +@Component({ + selector: 'app-footer', + standalone: true, + imports: [LucideLightbulb, TranslatePipe, RouterLink], + templateUrl: './footer.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FooterComponent {} + +@Component({ + selector: 'app-newsletter', + standalone: true, + imports: [FormsModule, TranslatePipe, RouterLink], + templateUrl: './newsletter.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NewsletterComponent { + readonly #catalog = inject(CatalogService); + readonly #i18n = inject(I18nService); + email = ''; + readonly message = signal(''); + readonly error = signal(''); + readonly submitting = signal(false); + + handleSubmit(): void { + if (!this.email || this.submitting()) { + return; + } + + this.submitting.set(true); + this.error.set(''); + this.#catalog.subscribeNewsletter(this.email).subscribe({ + next: () => { + this.message.set(this.#i18n.t('newsletter.success')); + this.email = ''; + this.submitting.set(false); + }, + error: () => { + this.error.set(this.#i18n.t('newsletter.error')); + this.submitting.set(false); + }, + }); + } +} + +@Component({ + selector: 'app-star-rating', + standalone: true, + imports: [LucideStar], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StarRatingComponent { + readonly rating = input(0); + readonly stars = [1, 2, 3, 4, 5]; +} + +@Component({ + selector: 'app-product-card', + standalone: true, + imports: [ + RouterLink, + StarRatingComponent, + I18nCurrencyPipe, + ProductImagePipe, + LucideHeart, + LucideLightbulb, + LucideShoppingBag, + TranslatePipe, + CartAddedModalComponent, + ], + templateUrl: './product-card.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductCardComponent implements OnInit { + readonly product = input.required(); + readonly #cart = inject(CartService); + readonly #catalog = inject(CatalogService); + readonly #favorites = inject(FavoritesService); + readonly #auth = inject(AuthService); + readonly #router = inject(Router); + readonly #destroyRef = inject(DestroyRef); + + readonly favorited = signal(false); + readonly adding = signal(false); + readonly modalOpen = signal(false); + readonly freeShipThreshold = FREE_DELIVERY_THRESHOLD; + + ngOnInit(): void { + if (!this.#auth.isLoggedIn()) { + return; + } + + this.#favorites + .getAll() + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe({ + next: (items) => { + this.favorited.set( + items.some((i) => i.productId === this.product().id), + ); + }, + }); + } + + handleFavorite(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + if (!this.#auth.isLoggedIn()) { + void this.#router.navigate(['/login']); + return; + } + + const id = this.product().id; + if (this.favorited()) { + this.#favorites.remove(id).subscribe({ + next: () => this.favorited.set(false), + }); + return; + } + + this.#favorites.add(id).subscribe({ + next: () => this.favorited.set(true), + }); + } + + handleAddToCart(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + if (this.adding() || !this.product().isInStock) { + return; + } + + this.adding.set(true); + this.#catalog.getProduct(this.product().slug).subscribe({ + next: (detail) => { + const variant = + detail.variants.find((v) => v.isDefault) ?? detail.variants[0]; + if (!variant) { + this.adding.set(false); + return; + } + + this.#cart.addItem(detail.id, variant.id, 1).subscribe({ + next: () => { + this.adding.set(false); + this.modalOpen.set(true); + }, + error: () => this.adding.set(false), + }); + }, + error: () => this.adding.set(false), + }); + } +} + +@Component({ + selector: 'app-shell', + standalone: true, + imports: [ + PromoBannerComponent, + HeaderComponent, + FooterComponent, + NewsletterComponent, + TranslatePipe, + ], + template: ` + + {{ 'a11y.skipToContent' | translate }} + +
+ + +
+ +
+ + +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ShellComponent {} diff --git a/src/shop/src/app/shared/layout/newsletter.component.html b/src/shop/src/app/shared/layout/newsletter.component.html new file mode 100644 index 0000000..b098df4 --- /dev/null +++ b/src/shop/src/app/shared/layout/newsletter.component.html @@ -0,0 +1,52 @@ +
+
+
+ + {{ 'newsletter.headline' | translate }} + +

{{ 'newsletter.subtitle' | translate }}

+
+ +
+
+ + + +
+ + {{ 'newsletter.privacyNotice' | translate }} + {{ 'footer.privacy' | translate }} + + @if (error()) { + + } + @if (message()) { +

{{ message() }}

+ } +
+
+
diff --git a/src/shop/src/app/shared/layout/newsletter.component.scss b/src/shop/src/app/shared/layout/newsletter.component.scss new file mode 100644 index 0000000..920b847 --- /dev/null +++ b/src/shop/src/app/shared/layout/newsletter.component.scss @@ -0,0 +1,54 @@ +@use '../../../styles/tokens' as *; + +.newsletter { + background: $color-primary; + padding: 40px 0; +} + +.newsletter__inner { + @include container; + display: flex; + align-items: center; + justify-content: space-between; + gap: 32px; +} + +.newsletter__text strong { + font-size: $font-size-h4; + display: block; +} + +.newsletter__text p { + margin: 4px 0 0; +} + +.newsletter__form { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + flex: 1; + max-width: 500px; + + input { + flex: 1; + min-width: 200px; + background: rgba(255, 255, 255, 0.6); + border: none; + } + + small { + width: 100%; + font-size: 12px; + color: rgba(0, 0, 0, 0.6); + } +} + +.newsletter__msg { + width: 100%; + font-weight: 500; +} + +@media (max-width: 768px) { + .newsletter__inner { flex-direction: column; text-align: center; } +} diff --git a/src/shop/src/app/shared/layout/product-card.component.html b/src/shop/src/app/shared/layout/product-card.component.html new file mode 100644 index 0000000..ac8b26b --- /dev/null +++ b/src/shop/src/app/shared/layout/product-card.component.html @@ -0,0 +1,105 @@ + + + diff --git a/src/shop/src/app/shared/parcel-point-map.component.html b/src/shop/src/app/shared/parcel-point-map.component.html new file mode 100644 index 0000000..b831ddc --- /dev/null +++ b/src/shop/src/app/shared/parcel-point-map.component.html @@ -0,0 +1,8 @@ +
+ @if (showHint()) { +

+ {{ 'checkout.parcelMapHint' | translate }} +

+ } +
+
diff --git a/src/shop/src/app/shared/parcel-point-map.component.scss b/src/shop/src/app/shared/parcel-point-map.component.scss new file mode 100644 index 0000000..1b05459 --- /dev/null +++ b/src/shop/src/app/shared/parcel-point-map.component.scss @@ -0,0 +1,48 @@ +:host { + display: block; + height: 100%; +} + +.parcel-point-map-wrap { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.parcel-point-map-hint { + margin: 0; + font-size: 0.875rem; + color: var(--color-iled-grey-dark, #666); +} + +.parcel-point-map { + z-index: 1; + position: relative; + height: 100%; + min-height: 16rem; + width: 100%; + border: 1px solid var(--color-iled-grey-light, #e5e5e5); + background: #f5f5f5; +} + +:host ::ng-deep .parcel-point-marker { + background: transparent; + border: none; +} + +:host ::ng-deep .parcel-point-marker__dot { + display: block; + width: 1rem; + height: 1rem; + border: 2px solid #fff; + border-radius: 50%; + background: #111; + box-shadow: 0 2px 6px rgb(0 0 0 / 25%); +} + +:host ::ng-deep .parcel-point-marker__dot.selected { + width: 1.25rem; + height: 1.25rem; + margin: -0.125rem 0 0 -0.125rem; + background: #e85d04; +} diff --git a/src/shop/src/app/shared/parcel-point-map.component.ts b/src/shop/src/app/shared/parcel-point-map.component.ts new file mode 100644 index 0000000..6d984b9 --- /dev/null +++ b/src/shop/src/app/shared/parcel-point-map.component.ts @@ -0,0 +1,159 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + DestroyRef, + ElementRef, + effect, + inject, + input, + output, + signal, + untracked, + viewChild, +} from '@angular/core'; +import * as L from 'leaflet'; +import { ParcelPoint } from '../core/checkout.utils'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; + +const DEFAULT_CENTER: L.LatLngExpression = [52.1, 19.4]; +const DEFAULT_ZOOM = 6; +const SELECTED_ZOOM = 15; + +@Component({ + selector: 'app-parcel-point-map', + standalone: true, + imports: [TranslatePipe], + templateUrl: './parcel-point-map.component.html', + styleUrl: './parcel-point-map.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ParcelPointMapComponent implements AfterViewInit { + readonly points = input([]); + readonly selectedId = input(''); + readonly showHint = input(true); + readonly pointSelected = output(); + + readonly mapHost = viewChild.required>('mapHost'); + readonly #destroyRef = inject(DestroyRef); + readonly #ready = signal(false); + + #map: L.Map | null = null; + #layer: L.LayerGroup | null = null; + #lastPointIds = ''; + + constructor() { + effect(() => { + if (!this.#ready()) { + return; + } + + const points = this.points(); + const selectedId = this.selectedId(); + untracked(() => this.#renderMarkers(points, selectedId)); + }); + } + + ngAfterViewInit(): void { + const el = this.mapHost().nativeElement; + this.#map = L.map(el, { scrollWheelZoom: true }).setView( + DEFAULT_CENTER, + DEFAULT_ZOOM, + ); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap contributors', + }).addTo(this.#map); + + this.#layer = L.layerGroup().addTo(this.#map); + this.#ready.set(true); + + this.#destroyRef.onDestroy(() => { + this.#map?.remove(); + this.#map = null; + this.#layer = null; + }); + + this.#refreshMapSize(); + } + + #renderMarkers(points: ParcelPoint[], selectedId: string): void { + if (!this.#map || !this.#layer) { + return; + } + + const pointIds = points.map((p) => p.id).join(','); + const pointsChanged = pointIds !== this.#lastPointIds; + this.#lastPointIds = pointIds; + + this.#layer.clearLayers(); + + if (points.length === 0) { + this.#map.setView(DEFAULT_CENTER, DEFAULT_ZOOM); + this.#refreshMapSize(); + return; + } + + const bounds = L.latLngBounds([]); + for (const point of points) { + const lat = point.latitude; + const lng = point.longitude; + if (lat == null || lng == null) { + continue; + } + + const latLng: L.LatLngExpression = [lat, lng]; + bounds.extend(latLng); + + const selected = point.id === selectedId; + const marker = L.marker(latLng, { icon: this.#createIcon(selected) }).bindPopup( + `${this.#escape(point.name)}
${this.#escape(point.address)}, ${this.#escape(point.postalCode)} ${this.#escape(point.city)}`, + ); + + marker.on('click', () => this.pointSelected.emit(point)); + marker.addTo(this.#layer); + } + + if (pointsChanged && bounds.isValid()) { + this.#map.fitBounds(bounds, { padding: [32, 32], maxZoom: 14 }); + } + + const selected = points.find((p) => p.id === selectedId); + if (selected?.latitude != null && selected.longitude != null && !pointsChanged) { + this.#map.flyTo( + [selected.latitude, selected.longitude], + SELECTED_ZOOM, + { duration: 0.4 }, + ); + } + + this.#refreshMapSize(); + } + + #refreshMapSize(): void { + requestAnimationFrame(() => { + this.#map?.invalidateSize(); + requestAnimationFrame(() => this.#map?.invalidateSize()); + }); + } + + #createIcon(selected: boolean): L.DivIcon { + const state = selected ? ' selected' : ''; + return L.divIcon({ + className: 'parcel-point-marker', + html: ``, + iconSize: [24, 24], + iconAnchor: [12, 24], + popupAnchor: [0, -24], + }); + } + + #escape(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } +} diff --git a/src/shop/src/app/shared/trust-strip.component.ts b/src/shop/src/app/shared/trust-strip.component.ts new file mode 100644 index 0000000..e1cbf5c --- /dev/null +++ b/src/shop/src/app/shared/trust-strip.component.ts @@ -0,0 +1,91 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + LucideClock, + LucidePackage, + LucideShieldCheck, + LucideTruck, +} from '@lucide/angular'; +import { FREE_DELIVERY_THRESHOLD } from '../core/constants'; +import { TranslatePipe } from '../core/i18n/translate.pipe'; +import { I18nCurrencyPipe } from '../core/i18n/i18n-currency.pipe'; + +@Component({ + selector: 'app-trust-strip', + standalone: true, + imports: [ + TranslatePipe, + I18nCurrencyPipe, + LucideTruck, + LucideClock, + LucideShieldCheck, + LucidePackage, + ], + template: ` +
    +
  • + +
    +

    {{ 'trust.deliveryTitle' | translate }}

    +

    + {{ 'trust.deliveryBody' | translate }} +

    +
    +
  • +
  • + +
    +

    {{ 'trust.freeShipTitle' | translate }}

    +

    + {{ + 'trust.freeShipBody' + | translate: { amount: threshold | i18nCurrency } + }} +

    +
    +
  • +
  • + +
    +

    {{ 'trust.payTitle' | translate }}

    +

    {{ 'trust.payBody' | translate }}

    +
    +
  • +
  • + +
    +

    {{ 'trust.returnsTitle' | translate }}

    +

    + {{ 'trust.returnsBody' | translate }} +

    +
    +
  • +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TrustStripComponent { + readonly threshold = FREE_DELIVERY_THRESHOLD; +} diff --git a/src/shop/src/environments/environment.production.ts b/src/shop/src/environments/environment.production.ts new file mode 100644 index 0000000..885b19c --- /dev/null +++ b/src/shop/src/environments/environment.production.ts @@ -0,0 +1,6 @@ +export const environment = { + production: true, + apiUrl: '/api/v1', + openPanelApiUrl: '', + openPanelClientId: '', +}; diff --git a/src/shop/src/environments/environment.ts b/src/shop/src/environments/environment.ts new file mode 100644 index 0000000..181fd67 --- /dev/null +++ b/src/shop/src/environments/environment.ts @@ -0,0 +1,8 @@ +export const environment = { + production: false, + apiUrl: 'http://localhost:5000/api/v1', + // Set after deploying OpenPanel (self-host) and creating a client: + // openPanelApiUrl: 'http://localhost:3000/api', + openPanelApiUrl: '', + openPanelClientId: '', +}; diff --git a/src/shop/src/index.html b/src/shop/src/index.html new file mode 100644 index 0000000..9b807cb --- /dev/null +++ b/src/shop/src/index.html @@ -0,0 +1,33 @@ + + + + + iLED — Sklep z lampami + + + + + + + + + + + + + + + + + + + diff --git a/src/shop/src/main.ts b/src/shop/src/main.ts new file mode 100644 index 0000000..5df75f9 --- /dev/null +++ b/src/shop/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig) + .catch((err) => console.error(err)); diff --git a/src/shop/src/styles.css b/src/shop/src/styles.css new file mode 100644 index 0000000..ea4a5c1 --- /dev/null +++ b/src/shop/src/styles.css @@ -0,0 +1,135 @@ +@import 'leaflet/dist/leaflet.css'; + +@import 'tailwindcss'; +@source './**/*.{html,ts}'; + +@theme { + --color-iled-primary: #ffbc39; + --color-iled-black: #111111; + --color-iled-grey-dark: #6b6b6b; + --color-iled-grey-light: #d9d9d9; + --color-iled-bg: #fafaf8; + --color-iled-surface: #ffffff; + --color-iled-success: #16a34a; + --color-iled-danger: #dc2626; + --color-iled-focus: #ffbc39; + --font-sans: 'Outfit', ui-sans-serif, system-ui, sans-serif; + --font-display: 'Outfit', ui-sans-serif, system-ui, sans-serif; +} + +@layer base { + body { + @apply m-0 bg-white font-sans text-base text-iled-black antialiased md:text-lg; + } + + a { + @apply text-inherit no-underline; + } + + img { + @apply block max-w-full; + } + + :focus-visible { + outline: 2px solid var(--color-iled-focus); + outline-offset: 2px; + } + + .leaflet-container img.leaflet-tile, + .leaflet-container img.leaflet-marker-icon, + .leaflet-container img.leaflet-marker-shadow { + max-width: none !important; + max-height: none !important; + } + + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } +} + +@layer components { + .iled-container { + @apply mx-auto w-full max-w-7xl px-4 sm:px-6; + } + + .iled-input { + @apply min-h-11 w-full border border-iled-grey-light bg-white px-4 py-2.5 text-base text-iled-black transition placeholder:text-iled-grey-dark focus:border-iled-black focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-iled-primary; + } + + .iled-label { + @apply mb-1.5 block text-sm font-medium text-iled-black; + } + + .iled-btn--primary { + @apply inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 bg-iled-black px-6 py-2.5 text-base font-medium text-white transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60; + } + + .iled-btn--accent { + @apply inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 bg-iled-primary px-6 py-2.5 text-base font-medium text-iled-black transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60; + } + + .iled-btn--ghost { + @apply inline-flex min-h-11 cursor-pointer items-center justify-center gap-2 border border-iled-grey-light bg-transparent px-6 py-2.5 text-base font-medium text-iled-black transition hover:border-iled-black disabled:cursor-not-allowed disabled:opacity-60; + } + + .iled-field-error { + @apply mt-1 text-sm text-iled-danger; + } + + .iled-fade-up { + animation: iled-fade-up 0.55s ease-out both; + } + + .iled-hover-lift { + @apply transition duration-200 ease-out hover:-translate-y-0.5 hover:shadow-md; + } +} + +@layer utilities { + .font-display { + font-family: var(--font-display); + letter-spacing: -0.02em; + } + + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } + + .focus\:not-sr-only:focus { + position: static; + width: auto; + height: auto; + padding: 0.5rem 1rem; + margin: 0; + overflow: visible; + clip: auto; + white-space: normal; + } +} + +@keyframes iled-fade-up { + from { + opacity: 0; + transform: translateY(12px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/shop/src/styles/_tokens.scss b/src/shop/src/styles/_tokens.scss new file mode 100644 index 0000000..e3a3cd8 --- /dev/null +++ b/src/shop/src/styles/_tokens.scss @@ -0,0 +1,53 @@ +$color-primary: #ffbc39; +$color-black: #111111; +$color-grey-dark: #6b6b6b; +$color-grey-light: #d9d9d9; +$color-white: #ffffff; +$color-bg: #fafaf8; +$color-surface: #ffffff; +$color-success: #16a34a; +$color-danger: #dc2626; + +$font-family: 'Outfit', sans-serif; +$font-display: 'Outfit', ui-sans-serif, system-ui, sans-serif; + +$font-size-h1: 65px; +$font-size-h2: 40px; +$font-size-h3: 30px; +$font-size-h4: 20px; +$font-size-body: 18px; +$font-size-small: 15px; + +$container-max: 1280px; +$header-height: 80px; + +@mixin container { + max-width: $container-max; + margin: 0 auto; + padding: 0 24px; +} + +@mixin button-primary { + background: $color-black; + color: $color-white; + border: none; + padding: 14px 32px; + font-size: $font-size-body; + font-weight: 500; + cursor: pointer; + transition: opacity 0.2s; + + &:hover { + opacity: 0.85; + } +} + +@mixin button-accent { + background: $color-primary; + color: $color-black; + border: none; + padding: 14px 32px; + font-size: $font-size-body; + font-weight: 500; + cursor: pointer; +} diff --git a/src/shop/tsconfig.app.json b/src/shop/tsconfig.app.json new file mode 100644 index 0000000..264f459 --- /dev/null +++ b/src/shop/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": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/src/shop/tsconfig.json b/src/shop/tsconfig.json new file mode 100644 index 0000000..2ab7442 --- /dev/null +++ b/src/shop/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": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/src/shop/tsconfig.spec.json b/src/shop/tsconfig.spec.json new file mode 100644 index 0000000..d383706 --- /dev/null +++ b/src/shop/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": [ + "vitest/globals" + ] + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.spec.ts" + ] +}