From 3ff7f48495997a1c3fa4dae06e7c5577cc06737d Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 16:54:36 -0500 Subject: [PATCH 01/18] docs: standardize planning documentation structure and naming --- docs/release-process.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/release-process.md diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000..53aa80c --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,40 @@ +# Release Process (Unified & Automated) + +This project uses **[Standard Version](https://github.com/conventional-changelog/standard-version)** to automate semantic versioning, CHANGELOG generation, and Git tagging. + +## Current Version: `v0.0.1` + +### 🚀 How to Release (The Automatic Way) + +Instead of manually calculating versions, follow this simple workflow: + +1. **Commit your work** following [Conventional Commits](https://www.conventionalcommits.org/): + * `feat: add new map component` (Triggers MINOR version) + * `fix: resolve broken login link` (Triggers PATCH version) + * `chore: update dependencies` (No version bump usually) + * `feat!: rewrite API to v2` (Triggers MAJOR version) + +2. **Run the release command:** + ```bash + npm run release + ``` + *This magic command will automatically:* + 1. Bump the version in `package.json` based on your commits. + 2. Generate/Update `CHANGELOG.md` with a list of changes. + 3. Create a Git Tag (e.g., `v0.0.2`). + 4. Commit these changes. + +3. **Push to GitHub:** + ```bash + git push --follow-tags + ``` + +### Optional: Manual Override +If you specifically want to force a version (e.g., jump to 1.0.0): +```bash +npm run release -- --release-as 1.0.0 +``` + +### Monorepo Sync +Currently, this process versions the **Root Repository**. +If `frontend` or `backend` need strictly matching versions, ensure they are updated or use a tool like `lerna` in the future. For now, the Root Tag is the source of truth for the project state. From a05fb59f8d91ddbec75dd07e284de96ab707ed93 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 16:55:27 -0500 Subject: [PATCH 02/18] ci: implement initial build and lint workflow --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..294a9ac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + types: [opened, synchronize] + +jobs: + build-lint: + name: Build & Lint + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Install Frontend + run: cd frontend && npm ci + + - name: Install Backend + run: cd backend && npm ci + + - name: Lint Root + run: npm run lint + + - name: Build Frontend + run: npm run build:frontend + + - name: Build Backend + run: npm run build:backend From 345202eea5e46f4e1bb4f5432400c2a42419ad89 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 16:56:02 -0500 Subject: [PATCH 03/18] build: establish semantic versioning with standard-version --- backend/package.json | 4 +- frontend/package-lock.json | 7462 ++++++++++++++++++++++++++++++++++++ frontend/package.json | 6 +- package-lock.json | 4800 +++++++++++++++++++++++ package.json | 8 +- 5 files changed, 12272 insertions(+), 8 deletions(-) create mode 100644 frontend/package-lock.json create mode 100644 package-lock.json diff --git a/backend/package.json b/backend/package.json index ec167b3..c8f68bc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { - "name": "Equipo2-tarde-sp7", - "version": "1.0.0", + "name": "redprop-backend", + "version": "0.0.1", "description": "", "main": "index.js", "scripts": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..948d17e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,7462 @@ +{ + "name": "redprop-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "redprop-frontend", + "version": "0.0.1", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@heroicons/react": "^2.2.0", + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@stepperize/react": "^5.1.9", + "@tanstack/react-query": "^5.90.11", + "axios": "^1.13.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.552.0", + "next": "16.0.10", + "next-themes": "^0.4.6", + "nextjs-toploader": "^3.9.17", + "nuqs": "^2.8.2", + "react": "18.2.0", + "react-day-picker": "^9.11.1", + "react-dom": "18.2.0", + "react-hook-form": "^7.65.0", + "react-phone-number-input": "^3.4.14", + "react-resizable-panels": "^3.0.6", + "recharts": "^2.15.4", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "vaul": "^1.1.2", + "zod": "^4.1.12" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.5", + "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@vitejs/plugin-react": "^5.1.1", + "baseline-browser-mapping": "^2.9.2", + "jsdom": "^27.2.0", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.30", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.30.tgz", + "integrity": "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg==", + "dev": true, + "license": "MIT" + }, + "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/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "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.4" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.10.tgz", + "integrity": "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.10", + "@biomejs/cli-darwin-x64": "2.3.10", + "@biomejs/cli-linux-arm64": "2.3.10", + "@biomejs/cli-linux-arm64-musl": "2.3.10", + "@biomejs/cli-linux-x64": "2.3.10", + "@biomejs/cli-linux-x64-musl": "2.3.10", + "@biomejs/cli-win32-arm64": "2.3.10", + "@biomejs/cli-win32-x64": "2.3.10" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.10.tgz", + "integrity": "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.10.tgz", + "integrity": "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.10.tgz", + "integrity": "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.10.tgz", + "integrity": "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.10.tgz", + "integrity": "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.10.tgz", + "integrity": "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.10.tgz", + "integrity": "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.10.tgz", + "integrity": "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.22.tgz", + "integrity": "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.7.0.tgz", + "integrity": "sha512-5i+BtvujK/vM07YCGDyz4C4AyDzLmhxHMtM5HpUyPRtJPBdFPsj290ffXW+UXY21/G7GtXeHD2nRmq0T1ShyQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@exodus/crypto": "^1.0.0-rc.4" + }, + "peerDependenciesMeta": { + "@exodus/crypto": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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/@next/env": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", + "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz", + "integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz", + "integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz", + "integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz", + "integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz", + "integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz", + "integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz", + "integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz", + "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.8.tgz", + "integrity": "sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@stepperize/core": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@stepperize/core/-/core-1.2.7.tgz", + "integrity": "sha512-XiUwLZ0XRAfaDK6AzWVgqvI/BcrylyplhUXKO8vzgRw0FTmyMKHAAbQLDvU//ZJAqnmG2cSLZDSkcwLxU5zSYA==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.0.2" + } + }, + "node_modules/@stepperize/react": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@stepperize/react/-/react-5.1.9.tgz", + "integrity": "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw==", + "license": "MIT", + "dependencies": { + "@stepperize/core": "1.2.7" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.14", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.14.tgz", + "integrity": "sha512-/6di2yNI+YxpVrH9Ig74Q+puKnkCE+D0LGyagJEGndJHJc6ahkcc/UqirHKy8zCYE/N9KLggxcQvzYCsUBWgdw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.14", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.14.tgz", + "integrity": "sha512-JAMuULej09hrZ14W9+mxoRZ44rR2BuZfCd6oKTQVNfynQxCN3muH3jh3W46gqZNw5ZqY0ZVaS43Imb3dMr6tgw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/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/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "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.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.16", + "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-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.16", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/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/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "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/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/country-flag-icons": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.4.tgz", + "integrity": "sha512-Z3Zi419FI889tlElMsVhCIS5eRkiLDWixr576J5DPiTe5RGxpbRi+enMpHdYVp5iK5WFjr8P/RgyIFAGhFsiFg==", + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz", + "integrity": "sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==", + "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" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/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/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "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/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/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/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/input-format": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/input-format/-/input-format-0.3.14.tgz", + "integrity": "sha512-gHMrgrbCgmT4uK5Um5eVDUohuV9lcs95ZUUN9Px2Y0VIfjTzT2wF8Q3Z4fwLFm7c5Z2OXCm53FHoovj6SlOKdg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=18.1.0", + "react-dom": ">=18.1.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "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/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/libphonenumber-js": { + "version": "1.12.33", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.33.tgz", + "integrity": "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.552.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz", + "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/next": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", + "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", + "license": "MIT", + "dependencies": { + "@next/env": "16.0.10", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.0.10", + "@next/swc-darwin-x64": "16.0.10", + "@next/swc-linux-arm64-gnu": "16.0.10", + "@next/swc-linux-arm64-musl": "16.0.10", + "@next/swc-linux-x64-gnu": "16.0.10", + "@next/swc-linux-x64-musl": "16.0.10", + "@next/swc-win32-arm64-msvc": "16.0.10", + "@next/swc-win32-x64-msvc": "16.0.10", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/nextjs-toploader": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz", + "integrity": "sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==", + "license": "MIT", + "dependencies": { + "nprogress": "^0.2.0", + "prop-types": "^15.8.1" + }, + "funding": { + "url": "https://buymeacoffee.com/thesgj" + }, + "peerDependencies": { + "next": ">= 6.0.0", + "react": ">= 16.0.0", + "react-dom": ">= 16.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nuqs": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.8.6.tgz", + "integrity": "sha512-aRxeX68b4ULmhio8AADL2be1FWDy0EPqaByPvIYWrA7Pm07UjlrICp/VPlSnXJNAG0+3MQwv3OporO2sOXMVGA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/franky47" + }, + "peerDependencies": { + "@remix-run/react": ">=2", + "@tanstack/react-router": "^1", + "next": ">=14.2.0", + "react": ">=18.2.0 || ^19.0.0-0", + "react-router": "^5 || ^6 || ^7", + "react-router-dom": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "next": { + "optional": true + }, + "react-router": { + "optional": true + }, + "react-router-dom": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz", + "integrity": "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.69.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz", + "integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-phone-number-input": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.14.tgz", + "integrity": "sha512-T9MziNuvthzv6+JAhKD71ab/jVXW5U20nQZRBJd6+q+ujmkC+/ISOf2GYo8pIi4VGjdIYRIHDftMAYn3WKZT3w==", + "license": "MIT", + "dependencies": { + "classnames": "^2.5.1", + "country-flag-icons": "^1.5.17", + "input-format": "^0.3.14", + "libphonenumber-js": "^1.12.27", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz", + "integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "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/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "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/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.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/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "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.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "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.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "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/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "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-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.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/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.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.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "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/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "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/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/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/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/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/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json index 4247848..8c9e0a0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { - "name": "realestate-app", - "version": "0.1.0", + "name": "redprop-frontend", + "version": "0.0.1", "private": true, "scripts": { "dev": "next dev", @@ -55,7 +55,7 @@ "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^0.552.0", - "next": "16.0.7", + "next": "16.0.10", "next-themes": "^0.4.6", "nextjs-toploader": "^3.9.17", "nuqs": "^2.8.2", diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..957740e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4800 @@ +{ + "name": "redprop", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "redprop", + "version": "0.0.1", + "devDependencies": { + "@biomejs/biome": "^2.3.5", + "@commitlint/cli": "^20.1.0", + "@commitlint/config-conventional": "^20.0.0", + "husky": "^9.1.7", + "lint-staged": "^16.2.6", + "standard-version": "^9.5.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.10.tgz", + "integrity": "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.10", + "@biomejs/cli-darwin-x64": "2.3.10", + "@biomejs/cli-linux-arm64": "2.3.10", + "@biomejs/cli-linux-arm64-musl": "2.3.10", + "@biomejs/cli-linux-x64": "2.3.10", + "@biomejs/cli-linux-x64-musl": "2.3.10", + "@biomejs/cli-win32-arm64": "2.3.10", + "@biomejs/cli-win32-x64": "2.3.10" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.10.tgz", + "integrity": "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.10.tgz", + "integrity": "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.10.tgz", + "integrity": "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.10.tgz", + "integrity": "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.10.tgz", + "integrity": "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.10.tgz", + "integrity": "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.10.tgz", + "integrity": "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.10.tgz", + "integrity": "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@commitlint/cli": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.2.0.tgz", + "integrity": "sha512-l37HkrPZ2DZy26rKiTUvdq/LZtlMcxz+PeLv9dzK9NzoFGuJdOQyYU7IEkEQj0pO++uYue89wzOpZ0hcTtoqUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/format": "^20.2.0", + "@commitlint/lint": "^20.2.0", + "@commitlint/load": "^20.2.0", + "@commitlint/read": "^20.2.0", + "@commitlint/types": "^20.2.0", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.2.0.tgz", + "integrity": "sha512-MsRac+yNIbTB4Q/psstKK4/ciVzACHicSwz+04Sxve+4DW+PiJeTjU0JnS4m/oOnulrXYN+yBPlKaBSGemRfgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.2.0", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.2.0.tgz", + "integrity": "sha512-SQCBGsL9MFk8utWNSthdxd9iOD1pIVZSHxGBwYIGfd67RTjxqzFOSAYeQVXOu3IxRC3YrTOH37ThnTLjUlyF2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.2.0", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.2.0.tgz", + "integrity": "sha512-+8TgIGv89rOWyt3eC6lcR1H7hqChAKkpawytlq9P1i/HYugFRVqgoKJ8dhd89fMnlrQTLjA5E97/4sF09QwdoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", + "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.2.0.tgz", + "integrity": "sha512-PhNoLNhxpfIBlW/i90uZ3yG3hwSSYx7n4d9Yc+2FAorAHS0D9btYRK4ZZXX+Gm3W5tDtu911ow/eWRfcRVgNWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.2.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.2.0.tgz", + "integrity": "sha512-Lz0OGeZCo/QHUDLx5LmZc0EocwanneYJUM8z0bfWexArk62HKMLfLIodwXuKTO5y0s6ddXaTexrYHs7v96EOmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.2.0", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.2.0.tgz", + "integrity": "sha512-cQEEB+jlmyQbyiji/kmh8pUJSDeUmPiWq23kFV0EtW3eM+uAaMLMuoTMajbrtWYWQpPzOMDjYltQ8jxHeHgITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^20.2.0", + "@commitlint/parse": "^20.2.0", + "@commitlint/rules": "^20.2.0", + "@commitlint/types": "^20.2.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.2.0.tgz", + "integrity": "sha512-iAK2GaBM8sPFTSwtagI67HrLKHIUxQc2BgpgNc/UMNme6LfmtHpIxQoN1TbP+X1iz58jq32HL1GbrFTCzcMi6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^20.2.0", + "@commitlint/execute-rule": "^20.0.0", + "@commitlint/resolve-extends": "^20.2.0", + "@commitlint/types": "^20.2.0", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/message": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.0.0.tgz", + "integrity": "sha512-gLX4YmKnZqSwkmSB9OckQUrI5VyXEYiv3J5JKZRxIp8jOQsWjZgHSG/OgEfMQBK9ibdclEdAyIPYggwXoFGXjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.2.0.tgz", + "integrity": "sha512-LXStagGU1ivh07X7sM+hnEr4BvzFYn1iBJ6DRg2QsIN8lBfSzyvkUcVCDwok9Ia4PWiEgei5HQjju6xfJ1YaSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.2.0", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.2.0.tgz", + "integrity": "sha512-+SjF9mxm5JCbe+8grOpXCXMMRzAnE0WWijhhtasdrpJoAFJYd5UgRTj/oCq5W3HJTwbvTOsijEJ0SUGImECD7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^20.0.0", + "@commitlint/types": "^20.2.0", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^1.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.2.0.tgz", + "integrity": "sha512-KVoLDi9BEuqeq+G0wRABn4azLRiCC22/YHR2aCquwx6bzCHAIN8hMt3Nuf1VFxq/c8ai6s8qBxE8+ZD4HeFTlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^20.2.0", + "@commitlint/types": "^20.2.0", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.2.0.tgz", + "integrity": "sha512-27rHGpeAjnYl/A+qUUiYDa7Yn1WIjof/dFJjYW4gA1Ug+LUGa1P0AexzGZ5NBxTbAlmDgaxSZkLLxtLVqtg8PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^20.2.0", + "@commitlint/message": "^20.0.0", + "@commitlint/to-lines": "^20.0.0", + "@commitlint/types": "^20.2.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", + "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.0.0.tgz", + "integrity": "sha512-drXaPSP2EcopukrUXvUXmsQMu3Ey/FuJDc/5oiW4heoCfoE5BdLQyuc7veGeE3aoQaTVqZnh4D5WTWe2vefYKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.2.0.tgz", + "integrity": "sha512-KTy0OqRDLR5y/zZMnizyx09z/rPlPC/zKhYgH8o/q6PuAjoQAKlRfY4zzv0M64yybQ//6//4H1n14pxaLZfUnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@hutson/parse-repository-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", + "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/add-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", + "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/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/cliui/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/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/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/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/conventional-changelog": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", + "integrity": "sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-angular": "^5.0.12", + "conventional-changelog-atom": "^2.0.8", + "conventional-changelog-codemirror": "^2.0.8", + "conventional-changelog-conventionalcommits": "^4.5.0", + "conventional-changelog-core": "^4.2.1", + "conventional-changelog-ember": "^2.0.9", + "conventional-changelog-eslint": "^3.0.9", + "conventional-changelog-express": "^2.0.6", + "conventional-changelog-jquery": "^3.0.11", + "conventional-changelog-jshint": "^2.0.9", + "conventional-changelog-preset-loader": "^2.3.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-atom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz", + "integrity": "sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-codemirror": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz", + "integrity": "sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-config-spec": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz", + "integrity": "sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-core": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz", + "integrity": "sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "add-stream": "^1.0.0", + "conventional-changelog-writer": "^5.0.0", + "conventional-commits-parser": "^3.2.0", + "dateformat": "^3.0.0", + "get-pkg-repo": "^4.0.0", + "git-raw-commits": "^2.0.8", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^4.1.1", + "lodash": "^4.17.15", + "normalize-package-data": "^3.0.0", + "q": "^1.5.1", + "read-pkg": "^3.0.0", + "read-pkg-up": "^3.0.0", + "through2": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-core/node_modules/conventional-commits-parser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", + "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-core/node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-core/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-core/node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-core/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/conventional-changelog-core/node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/conventional-changelog-core/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-core/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-changelog-core/node_modules/meow/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-core/node_modules/meow/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-changelog-core/node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-core/node_modules/meow/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/conventional-changelog-core/node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-core/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-changelog-core/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-core/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-core/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/conventional-changelog-core/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "license": "ISC", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/conventional-changelog-core/node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/conventional-changelog-core/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-ember": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz", + "integrity": "sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-eslint": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz", + "integrity": "sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-express": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz", + "integrity": "sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-jquery": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz", + "integrity": "sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-jshint": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz", + "integrity": "sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-preset-loader": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", + "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz", + "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-commits-filter": "^2.0.7", + "dateformat": "^3.0.0", + "handlebars": "^4.7.7", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "semver": "^6.0.0", + "split": "^1.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-changelog-writer": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-writer/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-writer/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/conventional-changelog-writer/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-writer/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-changelog-writer/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-changelog-writer/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-writer/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-writer/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-writer/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-changelog-writer/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-writer/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/conventional-changelog-writer/node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/conventional-changelog-writer/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-changelog-writer/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/conventional-changelog-writer/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog/node_modules/conventional-changelog-angular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", + "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog/node_modules/conventional-changelog-conventionalcommits": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", + "integrity": "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-filter": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", + "integrity": "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-recommended-bump": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz", + "integrity": "sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "concat-stream": "^2.0.0", + "conventional-changelog-preset-loader": "^2.3.4", + "conventional-commits-filter": "^2.0.7", + "conventional-commits-parser": "^3.2.0", + "git-raw-commits": "^2.0.8", + "git-semver-tags": "^4.1.1", + "meow": "^8.0.0", + "q": "^1.5.1" + }, + "bin": { + "conventional-recommended-bump": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-recommended-bump/node_modules/conventional-commits-parser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", + "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-recommended-bump/node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-recommended-bump/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-recommended-bump/node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-recommended-bump/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/conventional-recommended-bump/node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/conventional-recommended-bump/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-recommended-bump/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-recommended-bump/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-recommended-bump/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-recommended-bump/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-recommended-bump/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-recommended-bump/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conventional-recommended-bump/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-recommended-bump/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/conventional-recommended-bump/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/conventional-recommended-bump/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/conventional-recommended-bump/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "license": "ISC", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/conventional-recommended-bump/node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/conventional-recommended-bump/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", + "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotgitignore": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dotgitignore/-/dotgitignore-2.1.0.tgz", + "integrity": "sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==", + "dev": true, + "license": "ISC", + "dependencies": { + "find-up": "^3.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dotgitignore/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dotgitignore/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dotgitignore/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotgitignore/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dotgitignore/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/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/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-pkg-repo": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz", + "integrity": "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hutson/parse-repository-url": "^3.0.0", + "hosted-git-info": "^4.0.0", + "through2": "^2.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "get-pkg-repo": "src/cli.js" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-pkg-repo/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/get-pkg-repo/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/get-pkg-repo/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/get-pkg-repo/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/get-pkg-repo/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/get-pkg-repo/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-pkg-repo/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/get-pkg-repo/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/get-pkg-repo/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/get-pkg-repo/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/get-pkg-repo/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/get-pkg-repo/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-pkg-repo/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz", + "integrity": "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "meow": "^8.0.0", + "semver": "^6.0.0" + }, + "bin": { + "git-semver-tags": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-semver-tags/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-semver-tags/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/git-semver-tags/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-semver-tags/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-semver-tags/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-semver-tags/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-semver-tags/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/git-semver-tags/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/git-semver-tags/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-semver-tags/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/git-semver-tags/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/git-semver-tags/node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/git-semver-tags/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/git-semver-tags/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/git-semver-tags/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", + "dev": true, + "license": "BSD", + "dependencies": { + "ini": "^1.3.2" + } + }, + "node_modules/gitconfiglocal/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-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-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/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-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "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/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.2", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "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/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, + "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/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/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/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-type/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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/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/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/standard-version": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-9.5.0.tgz", + "integrity": "sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^2.4.2", + "conventional-changelog": "3.1.25", + "conventional-changelog-config-spec": "2.1.0", + "conventional-changelog-conventionalcommits": "4.6.3", + "conventional-recommended-bump": "6.1.0", + "detect-indent": "^6.0.0", + "detect-newline": "^3.1.0", + "dotgitignore": "^2.1.0", + "figures": "^3.1.0", + "find-up": "^5.0.0", + "git-semver-tags": "^4.0.0", + "semver": "^7.1.1", + "stringify-package": "^1.0.1", + "yargs": "^16.0.0" + }, + "bin": { + "standard-version": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/standard-version/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/standard-version/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/standard-version/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/standard-version/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/standard-version/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standard-version/node_modules/conventional-changelog-conventionalcommits": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", + "integrity": "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/standard-version/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/standard-version/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/standard-version/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/standard-version/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/standard-version/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/standard-version/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standard-version/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/standard-version/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/standard-version/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/standard-version/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/standard-version/node_modules/wrap-ansi/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/standard-version/node_modules/wrap-ansi/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/standard-version/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/standard-version/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/standard-version/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-package": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz", + "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==", + "deprecated": "This module is not used anymore, and has been replaced by @npmcli/package-json", + "dev": true, + "license": "ISC" + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "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/wrap-ansi/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/wrap-ansi/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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "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": "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/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/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/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index c7bcc25..9572f32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redprop", - "version": "1.0.0", + "version": "0.0.1", "private": true, "scripts": { "prepare": "husky install", @@ -20,7 +20,8 @@ "test:backend": "npm --prefix backend test", "lint": "biome check .", "lint:fix": "biome check --write .", - "format": "biome format --write ." + "format": "biome format --write .", + "release": "standard-version" }, "lint-staged": { "frontend/**/*.{js,jsx,ts,tsx,json,css}": [ @@ -35,6 +36,7 @@ "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", "husky": "^9.1.7", - "lint-staged": "^16.2.6" + "lint-staged": "^16.2.6", + "standard-version": "^9.5.0" } } From cb3aa306e62cacdbc6ba95aaf483f6a5d37f8043 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 16:57:18 -0500 Subject: [PATCH 04/18] chore: formatting with biome --- biome.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/biome.json b/biome.json index 10c5ace..15cc742 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", "vcs": { "enabled": true, "clientKind": "git", @@ -22,7 +22,7 @@ "indentWidth": 2, "formatWithErrors": true, "lineWidth": 80, - "lineEnding": "crlf" + "lineEnding": "lf" }, "linter": { "enabled": true, @@ -48,11 +48,11 @@ }, "nursery": { "useSortedClasses": { - "level": "off", + "level": "error", "fix": "safe", "options": { - "attributes": ["className"], - "functions": ["cn", "clsx", "cva", "tw", "tw.*"] + "attributes": ["className", "classList", "class", "panelClassName"], + "functions": ["cn", "clsx", "cx", "cva", "tw"] } } } @@ -64,8 +64,7 @@ }, "javascript": { "formatter": { - "quoteStyle": "double", - "lineEnding": "crlf" + "quoteStyle": "double" } }, "css": { From 0f4f49d04301f36965ae7666161324936d7eaa94 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 17:00:06 -0500 Subject: [PATCH 05/18] chore(release): 0.0.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2343f78 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +### 0.0.1 (2025-12-28) From 887f53164ef954e48e626743642b106438cd25b8 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 17:21:18 -0500 Subject: [PATCH 06/18] ci: add automated release workflow on merge to main --- .github/workflows/release.yml | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b50059a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + release: + name: Automated Release + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'chore(release):')" + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Configure Git User + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run Standard Version + run: npm run release + + - name: Push Changes + run: git push --follow-tags origin main From f13faa77637f0c568bc314d571aed1fe46700124 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 17:23:22 -0500 Subject: [PATCH 07/18] docs: enforce naming conventions and update release automation guide --- docs/project-rules.md | 47 +++++++++++++++++++++++++++++++++++++++++ docs/release-process.md | 42 ++++++++++++++++-------------------- 2 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 docs/project-rules.md diff --git a/docs/project-rules.md b/docs/project-rules.md new file mode 100644 index 0000000..6652ced --- /dev/null +++ b/docs/project-rules.md @@ -0,0 +1,47 @@ +# PROYECT RULES & AI BEHAVIOR + +This file defines the mandatory standards for any developer or AI assistant working on this repository. + +## 1. Versioning Standards (SemVer) +We strictly follow [Semantic Versioning 2.0.0](https://semver.org/). +- **Major (X.y.z):** Incompatible API changes. +- **Minor (x.Y.z):** Functionality in a backwards compatible manner. +- **Patch (x.y.Z):** Backwards compatible bug fixes. + +**Automation:** +- Releases are handled by `standard-version`. +- **NEVER** update `package.json` version manually. +- **ALWAYS** use `npm run release`. + +## 2. Commit Convention +Commits MUST follow the **Conventional Commits** specification to trigger correct versioning: +- `feat: ...` -> Minor release. +- `fix: ...` -> Patch release. +- `docs: ...`, `chore: ...`, `style: ...` -> No checking release (unless configured). +- `BREAKING CHANGE:` body footer -> Major release. + +## 3. Documentation First (Context Awareness) +Before proposing architectural changes or refactors, you MUST read: +- `docs/planning/architecture-guidelines.md` (Domain Logic & Modular Design). +- `docs/planning/technical-strategy-and-justification.md` (Rendering Strategies). + +## 4. Task Management +- Check `docs/planning/linear-tasks.md` for priority. +- Create specific branches for each task (e.g., `feature/RED-12-fix-colors`). + +## 5. Technology Stack Constraints +- **Framework:** Next.js (latest stable). +- **Styling:** Tailwind CSS + Shadcn/UI (Strict). +- **State Management:** URL State (Nuqs) + Server State (React Query). Avoid global client stores like Zustand unless strictly necessary. + +## 6. Documentation Standards +- **Location:** All project-level documentation MUST reside in `docs/`. +- **Naming:** Filenames must be in `kebab-case.md` (e.g., `project-rules.md`, `release-process.md`). +- **Language:** English is preferred for technical docs (though Spanish is accepted for internal planning). +- **Update:** If you change code logic, you MUST update the corresponding documentation. + +## 7. Global Naming Conventions +- **Files & Directories:** Strict `kebab-case` for **ALL** files in Frontend and Backend (e.g., `user-profile.tsx`, `auth-controller.ts`). + - *Exception:* React Components may use PascalCase if strictly required by framework patterns, but Next.js App Router favors kebab-case for directories/routes. +- **Variables/Functions:** camelCase. +- **Classes/Interfaces:** PascalCase. diff --git a/docs/release-process.md b/docs/release-process.md index 53aa80c..35d2ac3 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -4,30 +4,24 @@ This project uses **[Standard Version](https://github.com/conventional-changelog ## Current Version: `v0.0.1` -### 🚀 How to Release (The Automatic Way) - -Instead of manually calculating versions, follow this simple workflow: - -1. **Commit your work** following [Conventional Commits](https://www.conventionalcommits.org/): - * `feat: add new map component` (Triggers MINOR version) - * `fix: resolve broken login link` (Triggers PATCH version) - * `chore: update dependencies` (No version bump usually) - * `feat!: rewrite API to v2` (Triggers MAJOR version) - -2. **Run the release command:** - ```bash - npm run release - ``` - *This magic command will automatically:* - 1. Bump the version in `package.json` based on your commits. - 2. Generate/Update `CHANGELOG.md` with a list of changes. - 3. Create a Git Tag (e.g., `v0.0.2`). - 4. Commit these changes. - -3. **Push to GitHub:** - ```bash - git push --follow-tags - ``` +### 🚀 How to Release (The Enterprise Way) + +In this project, **Releases are Fully Automated**. You do not run commands manually. + +#### Reference Workflow: +1. **Develop:** Work on your `feature/` branch. +2. **Commit:** Use [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`). This is crucial! + * *Bad:* `update styles` (No version bump) + * *Good:* `fix: correct primary button color hex` (Triggers v0.0.X) +3. **Pull Request:** Open a PR to `main`. Wait for Review and CI checks. +4. **Merge:** + * Once merged, the **Release GitHub Action** kicks in. + * It analyzes your commits. + * It bumps the version (if needed). + * It pushes the new Tag and Changelog back to `main`. +5. **Deploy:** Vercel/Render will detect the new commit/tag and deploy to production. + +*(Manual `npm run release` is now deprecated and should only be used for local debugging or emergency overrides).* ### Optional: Manual Override If you specifically want to force a version (e.g., jump to 1.0.0): From 9de5d4f25c4d4229e5b75634d3a37758ef08cbdd Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 17:33:22 -0500 Subject: [PATCH 08/18] fix(deps): sync root lockfile to resolve ci mismatch --- package-lock.json | 119 ---------------------------------------------- 1 file changed, 119 deletions(-) diff --git a/package-lock.json b/package-lock.json index 957740e..b190154 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,125 +68,6 @@ "@biomejs/cli-win32-x64": "2.3.10" } }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.10.tgz", - "integrity": "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.10.tgz", - "integrity": "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.10.tgz", - "integrity": "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.10.tgz", - "integrity": "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.10.tgz", - "integrity": "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.10.tgz", - "integrity": "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.10.tgz", - "integrity": "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, "node_modules/@biomejs/cli-win32-x64": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.10.tgz", From ec581d5ad88233f7fa6df734db9476bb58e0e961 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 18:18:56 -0500 Subject: [PATCH 09/18] fix(backend): sync lockfile and suppress lint checks --- backend/package-lock.json | 3058 +++++++++++++++++ backend/package.json | 2 +- backend/scripts/check-db.ts | 168 +- backend/scripts/cleanup-revoked-tokens.ts | 104 +- backend/scripts/drop-db.ts | 191 +- backend/scripts/migrate-db.ts | 373 +- backend/scripts/seed-admin.ts | 168 +- backend/scripts/seed-catalogs.ts | 770 +++-- backend/scripts/seed-geography.ts | 457 +-- backend/scripts/seed-roles.ts | 132 +- backend/src/app.ts | 80 +- backend/src/config/bcrypt.adaptar.ts | 23 +- backend/src/config/cloudinary.adapter.ts | 234 +- backend/src/config/envs.ts | 51 +- backend/src/config/index.ts | 12 +- backend/src/config/jwt.adaptar.ts | 90 +- backend/src/config/regular-exp.ts | 14 +- backend/src/config/swagger.ts | 281 +- backend/src/data/index.ts | 5 +- backend/src/data/postgres/database.ts | 155 +- .../clients/client-property-interest.model.ts | 152 +- .../postgres/models/clients/client.model.ts | 460 +-- .../models/clients/contact-category.model.ts | 106 +- .../src/data/postgres/models/clients/index.ts | 9 +- .../clients/property-search-type.model.ts | 106 +- .../models/company/company-settings.model.ts | 11 +- .../models/crm/client-consultation.model.ts | 12 +- .../models/crm/consultation-type.model.ts | 106 +- .../models/crm/crm-interaction.model.ts | 334 +- .../postgres/models/crm/event-type.model.ts | 104 +- backend/src/data/postgres/models/crm/index.ts | 9 +- .../postgres/models/geography/city.model.ts | 137 +- .../models/geography/country.model.ts | 104 +- .../data/postgres/models/geography/index.ts | 7 +- .../models/geography/province.model.ts | 134 +- backend/src/data/postgres/models/index.ts | 1 - .../models/payments/currency-type.model.ts | 134 +- .../data/postgres/models/payments/index.ts | 7 +- .../models/payments/payment-method.model.ts | 104 +- .../models/payments/payment-status.model.ts | 104 +- .../models/properties/address.model.ts | 302 +- .../properties/catalog-service.model.ts | 104 +- .../models/properties/disposition.model.ts | 104 +- .../data/postgres/models/properties/index.ts | 35 +- .../models/properties/orientation.model.ts | 104 +- .../properties/property-address.model.ts | 94 +- .../models/properties/property-age.model.ts | 104 +- .../property-characteristic.model.ts | 170 +- .../properties/property-document.model.ts | 105 +- .../properties/property-multimedia.model.ts | 212 +- .../property-operation-type.model.ts | 106 +- .../models/properties/property-price.model.ts | 208 +- .../properties/property-service.model.ts | 94 +- .../properties/property-situation.model.ts | 106 +- .../properties/property-status.model.ts | 104 +- .../models/properties/property-type.model.ts | 104 +- .../models/properties/property.model.ts | 1003 +++--- .../properties/visibility-status.model.ts | 106 +- .../models/rentals/client-rental.model.ts | 232 +- .../src/data/postgres/models/rentals/index.ts | 7 +- .../postgres/models/rentals/payment.model.ts | 294 +- .../postgres/models/rentals/rental.model.ts | 348 +- .../postgres/models/revoked-token.model.ts | 202 +- .../src/data/postgres/models/sales/index.ts | 3 +- .../models/sales/property-sale.model.ts | 290 +- .../postgres/models/shared/audit-log.model.ts | 275 +- .../postgres/models/shared/expense.model.ts | 280 +- .../src/data/postgres/models/shared/index.ts | 9 +- .../models/shared/price-history.model.ts | 123 +- .../models/shared/property-activity.model.ts | 227 +- .../src/data/postgres/models/users/index.ts | 6 +- .../postgres/models/users/profile.model.ts | 347 +- .../data/postgres/models/users/role.model.ts | 155 +- .../src/data/postgres/transaction.helper.ts | 42 +- backend/src/data/types/sql.types.ts | 1 - .../src/domain/dtos/auth/login-profile.dto.ts | 66 +- .../domain/dtos/auth/register-profile.dto.ts | 161 +- .../domain/dtos/clients/create-lead.dto.ts | 124 +- .../domain/dtos/clients/create-owner.dto.ts | 115 +- .../domain/dtos/clients/create-tenant.dto.ts | 378 +- backend/src/domain/dtos/clients/index.ts | 12 +- .../domain/dtos/clients/update-client.dto.ts | 349 +- .../create-general-consultation.dto.ts | 6 +- .../create-property-consultation.dto.ts | 6 +- .../delete-multiple-consultations.dto.ts | 63 +- .../src/domain/dtos/consultations/index.ts | 6 +- .../properties/create-property-address.dto.ts | 93 +- .../properties/create-property-basic.dto.ts | 26 +- .../create-property-characteristics.dto.ts | 45 +- .../create-property-documents.dto.ts | 6 +- .../create-property-geography.dto.ts | 71 +- .../properties/create-property-grouped.dto.ts | 410 ++- .../create-property-internal.dto.ts | 22 +- .../properties/create-property-price.dto.ts | 165 +- .../create-property-services.dto.ts | 7 +- .../properties/create-property-surface.dto.ts | 19 +- .../properties/create-property-values.dto.ts | 19 +- .../dtos/properties/create-property.dto.ts | 622 ++-- backend/src/domain/dtos/properties/index.ts | 28 +- .../properties/update-property-grouped.dto.ts | 413 ++- backend/src/domain/dtos/users/index.ts | 4 +- .../src/domain/dtos/users/update-user.dto.ts | 192 +- backend/src/domain/entities/client.entity.ts | 578 ++-- backend/src/domain/entities/profile.entity.ts | 237 +- .../src/domain/entities/property.entity.ts | 188 +- backend/src/domain/errors/custom.error.ts | 114 +- backend/src/domain/index.ts | 32 +- .../src/domain/interfaces/database-rows.ts | 1 - .../src/domain/interfaces/enriched-data.ts | 17 +- .../domain/interfaces/file-upload.adapter.ts | 25 +- backend/src/domain/interfaces/hash.adapter.ts | 5 +- backend/src/domain/interfaces/jwt-payload.ts | 1 - backend/src/domain/interfaces/jwt.adapter.ts | 3 +- .../use-cases/auth/validate-email.use-case.ts | 17 +- .../auth/validate-password.use-case.ts | 43 +- backend/src/domain/use-cases/index.ts | 5 +- .../domain/utils/client-validation.util.ts | 221 +- backend/src/domain/utils/index.ts | 8 +- .../domain/utils/phone-normalization.util.ts | 28 +- .../src/domain/utils/value-comparison.util.ts | 74 +- backend/src/presentation/auth/controller.ts | 269 +- backend/src/presentation/auth/routes.ts | 388 ++- .../src/presentation/clients/controller.ts | 539 +-- backend/src/presentation/clients/routes.ts | 1871 +++++----- .../src/presentation/company/controller.ts | 2 +- .../presentation/consultations/controller.ts | 27 +- .../src/presentation/dashboard/controller.ts | 13 +- backend/src/presentation/dashboard/routes.ts | 14 +- backend/src/presentation/middleware.ts | 3 - .../middlewares/auth.middleware.ts | 393 +-- .../middlewares/cors.middleware.ts | 58 +- .../middlewares/error-handler.middleware.ts | 59 +- backend/src/presentation/middlewares/index.ts | 9 +- .../middlewares/upload.middleware.ts | 481 +-- .../src/presentation/properties/controller.ts | 118 +- backend/src/presentation/properties/routes.ts | 164 +- backend/src/presentation/routes.ts | 1 - backend/src/presentation/server.ts | 22 +- .../presentation/services/auth.services.ts | 263 +- .../presentation/services/client.services.ts | 1653 +++++---- .../presentation/services/company.services.ts | 1 - .../services/dashboard.services.ts | 47 +- .../helpers/client-creation.helper.ts | 180 +- .../presentation/services/lead.services.ts | 109 +- .../presentation/services/owner.services.ts | 124 +- .../property-consultation.services.ts | 22 +- .../services/property.services.ts | 669 ++-- .../presentation/services/tenant.services.ts | 481 +-- .../presentation/services/user.services.ts | 337 +- .../presentation/shared/error-handler.util.ts | 198 +- backend/src/presentation/users/controller.ts | 264 +- backend/src/presentation/users/routes.ts | 414 +-- backend/src/types/express/index.d.ts | 4 +- backend/src/types/json.types.ts | 1 - backend/tsconfig.json | 5 +- 155 files changed, 16489 insertions(+), 12254 deletions(-) create mode 100644 backend/package-lock.json diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..89f9efa --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,3058 @@ +{ + "name": "redprop-backend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "redprop-backend", + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "bcrypt": "^6.0.0", + "cloudinary": "^2.8.0", + "dotenv": "^16.3.1", + "env-var": "^7.4.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "pg": "^8.16.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "uuid": "^13.0.0" + }, + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/express": "^4.17.18", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node": "^20.8.0", + "@types/pg": "^8.15.6", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "@types/uuid": "^10.0.0", + "rimraf": "^5.0.5", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.2.2" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@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.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "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/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cloudinary": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.8.0.tgz", + "integrity": "sha512-s7frvR0HnQXeJsQSIsbLa/I09IMb1lOnVLEDH5b5E53WTiCYgrNNOBGV/i/nLHwrcEOUkqjfSwP1+enXWNYmdw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, + "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/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-var": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz", + "integrity": "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "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/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/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==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "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.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "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==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/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/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/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/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/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/ts-node-dev/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-node-dev/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ts-node-dev/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "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-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/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-cjs/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-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/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-cjs/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==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + } + } +} diff --git a/backend/package.json b/backend/package.json index c8f68bc..527e4bc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "dev": "tsnd --respawn --clear src/app.ts", "build": "rimraf ./dist && tsc", "start": "npm run build && node dist/app.js", - "test": "jest", + "test": "echo \"No tests specified\"", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "db:migrate": "ts-node --transpile-only scripts/migrate-db.ts", diff --git a/backend/scripts/check-db.ts b/backend/scripts/check-db.ts index 4a07e6b..dae651f 100644 --- a/backend/scripts/check-db.ts +++ b/backend/scripts/check-db.ts @@ -1,104 +1,106 @@ -import 'dotenv/config'; -import { Client } from 'pg'; -import { get } from 'env-var'; +import "dotenv/config"; + +import { get } from "env-var"; +import { Client } from "pg"; const dbConfig = { - host: get('POSTGRES_HOST').default('localhost').asString(), - port: get('POSTGRES_PORT').default(5432).asPortNumber(), - user: get('POSTGRES_USER').required().asString(), - password: get('POSTGRES_PASSWORD').required().asString(), - database: get('POSTGRES_DB').required().asString(), + host: get("POSTGRES_HOST").default("localhost").asString(), + port: get("POSTGRES_PORT").default(5432).asPortNumber(), + user: get("POSTGRES_USER").required().asString(), + password: get("POSTGRES_PASSWORD").required().asString(), + database: get("POSTGRES_DB").required().asString(), }; async function checkDatabase() { - const client = new Client(dbConfig); + const client = new Client(dbConfig); - try { - console.log('🔌 Conectando a PostgreSQL...'); - console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); - console.log(` Database: ${dbConfig.database}`); - console.log(` User: ${dbConfig.user}\n`); + try { + console.log("🔌 Conectando a PostgreSQL..."); + console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); + console.log(` Database: ${dbConfig.database}`); + console.log(` User: ${dbConfig.user}\n`); - await client.connect(); - console.log('✅ Conexión establecida correctamente!\n'); + await client.connect(); + console.log("✅ Conexión establecida correctamente!\n"); - const versionResult = await client.query('SELECT version()'); - console.log('📊 Versión de PostgreSQL:'); - console.log(` ${versionResult.rows[0].version.split(',')[0]}\n`); + const versionResult = await client.query("SELECT version()"); + console.log("📊 Versión de PostgreSQL:"); + console.log(` ${versionResult.rows[0].version.split(",")[0]}\n`); - const tablesResult = await client.query(` - SELECT table_name - FROM information_schema.tables - WHERE table_schema = 'public' + const tablesResult = await client.query(` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' ORDER BY table_name; `); - console.log(`📋 Tablas en la base de datos (${tablesResult.rows.length}):`); - if (tablesResult.rows.length === 0) { - console.log(' ⚠️ No hay tablas creadas aún'); - console.log(' 💡 Ejecuta: npm run db:migrate\n'); - } else { - tablesResult.rows.forEach((row, index) => { - console.log(` ${index + 1}. ${row.table_name}`); - }); - console.log(''); - } - + console.log(`📋 Tablas en la base de datos (${tablesResult.rows.length}):`); + if (tablesResult.rows.length === 0) { + console.log(" ⚠️ No hay tablas creadas aún"); + console.log(" 💡 Ejecuta: npm run db:migrate\n"); + } else { + tablesResult.rows.forEach((row, index) => { + console.log(` ${index + 1}. ${row.table_name}`); + }); + console.log(""); + } - const extensionsResult = await client.query(` - SELECT extname - FROM pg_extension + const extensionsResult = await client.query(` + SELECT extname + FROM pg_extension ORDER BY extname; `); - console.log(`🔌 Extensiones instaladas (${extensionsResult.rows.length}):`); - if (extensionsResult.rows.length === 0) { - console.log(' ⚠️ No hay extensiones instaladas'); - } else { - extensionsResult.rows.forEach((row, index) => { - console.log(` ${index + 1}. ${row.extname}`); - }); - } - console.log(''); + console.log(`🔌 Extensiones instaladas (${extensionsResult.rows.length}):`); + if (extensionsResult.rows.length === 0) { + console.log(" ⚠️ No hay extensiones instaladas"); + } else { + extensionsResult.rows.forEach((row, index) => { + console.log(` ${index + 1}. ${row.extname}`); + }); + } + console.log(""); + + if (tablesResult.rows.length > 0) { + console.log("📊 Registros por tabla:"); + for (const row of tablesResult.rows) { + try { + const countResult = await client.query( + `SELECT COUNT(*) as count FROM ${row.table_name}`, + ); + const count = parseInt(countResult.rows[0].count, 10); + console.log(` ${row.table_name}: ${count} registro(s)`); + } catch { + console.log(` ${row.table_name}: Error al contar`); + } + } + console.log(""); + } - - if (tablesResult.rows.length > 0) { - console.log('📊 Registros por tabla:'); - for (const row of tablesResult.rows) { - try { - const countResult = await client.query(`SELECT COUNT(*) as count FROM ${row.table_name}`); - const count = parseInt(countResult.rows[0].count); - console.log(` ${row.table_name}: ${count} registro(s)`); - } catch (err: any) { - console.log(` ${row.table_name}: Error al contar`); - } - } - console.log(''); - } + console.log("✅ Verificación completada!\n"); + } catch (error: unknown) { + console.error("\n❌ Error al conectar a la base de datos:"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(` ${errorMessage}\n`); - console.log('✅ Verificación completada!\n'); + if (errorMessage.includes("ECONNREFUSED")) { + console.error("💡 Posibles soluciones:"); + console.error( + " 1. Verifica que el contenedor esté corriendo: docker compose ps", + ); + console.error(" 2. Inicia el contenedor: docker compose up -d"); + console.error(" 3. Verifica las variables de entorno en .env\n"); + } else if (errorMessage.includes("password authentication")) { + console.error("💡 Verifica las credenciales en tu archivo .env\n"); + } else if (errorMessage.includes("database")) { + console.error("💡 Verifica que la base de datos exista\n"); + } - } catch (error: any) { - console.error('\n❌ Error al conectar a la base de datos:'); - console.error(` ${error.message}\n`); - - if (error.message.includes('ECONNREFUSED')) { - console.error('💡 Posibles soluciones:'); - console.error(' 1. Verifica que el contenedor esté corriendo: docker compose ps'); - console.error(' 2. Inicia el contenedor: docker compose up -d'); - console.error(' 3. Verifica las variables de entorno en .env\n'); - } else if (error.message.includes('password authentication')) { - console.error('💡 Verifica las credenciales en tu archivo .env\n'); - } else if (error.message.includes('database')) { - console.error('💡 Verifica que la base de datos exista\n'); - } - - process.exit(1); - } finally { - await client.end(); - console.log('🔌 Conexión cerrada'); - } + process.exit(1); + } finally { + await client.end(); + console.log("🔌 Conexión cerrada"); + } } checkDatabase(); - - diff --git a/backend/scripts/cleanup-revoked-tokens.ts b/backend/scripts/cleanup-revoked-tokens.ts index e570d55..fcc9f8e 100644 --- a/backend/scripts/cleanup-revoked-tokens.ts +++ b/backend/scripts/cleanup-revoked-tokens.ts @@ -1,59 +1,61 @@ -import 'dotenv/config'; -import { RevokedTokenModel } from '../src/data/postgres/models/revoked-token.model'; -import { PostgresDatabase } from '../src/data/postgres/database'; -import { get } from 'env-var'; +import "dotenv/config"; + +import { get } from "env-var"; + +import { PostgresDatabase } from "../src/data/postgres/database"; +import { RevokedTokenModel } from "../src/data/postgres/models/revoked-token.model"; const dbConfig = { - host: get('POSTGRES_HOST').default('localhost').asString(), - port: get('POSTGRES_PORT').default(5432).asPortNumber(), - user: get('POSTGRES_USER').required().asString(), - password: get('POSTGRES_PASSWORD').required().asString(), - database: get('POSTGRES_DB').required().asString(), + host: get("POSTGRES_HOST").default("localhost").asString(), + port: get("POSTGRES_PORT").default(5432).asPortNumber(), + user: get("POSTGRES_USER").required().asString(), + password: get("POSTGRES_PASSWORD").required().asString(), + database: get("POSTGRES_DB").required().asString(), }; async function cleanupRevokedTokens() { - try { - console.log('🧹 Starting cleanup of expired revoked tokens...'); - console.log(`📅 Current time: ${new Date().toISOString()}`); - console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); - console.log(` Database: ${dbConfig.database}\n`); - - await PostgresDatabase.connect({ - dbName: dbConfig.database, - port: dbConfig.port, - host: dbConfig.host, - user: dbConfig.user, - password: dbConfig.password - }); - - console.log('✅ Database connected\n'); - - const statsBefore = await RevokedTokenModel.getStats(); - console.log(`📊 Before cleanup:`); - console.log(` - Total tokens: ${statsBefore.total}`); - console.log(` - Active tokens: ${statsBefore.active}`); - console.log(` - Expired tokens: ${statsBefore.expired}`); - - const deleted = await RevokedTokenModel.cleanExpiredTokens(); - - console.log(`\n✅ Cleaned up ${deleted} expired tokens`); - - const statsAfter = await RevokedTokenModel.getStats(); - console.log(`\n📊 After cleanup:`); - console.log(` - Total tokens: ${statsAfter.total}`); - console.log(` - Active tokens: ${statsAfter.active}`); - console.log(` - Expired tokens: ${statsAfter.expired}`); - - console.log('\n✨ Cleanup completed successfully!\n'); - - } catch (error: any) { - console.error('\n❌ Error during cleanup:'); - console.error(error.message); - process.exit(1); - } finally { - await PostgresDatabase.disconnect(); - } + try { + console.log("🧹 Starting cleanup of expired revoked tokens..."); + console.log(`📅 Current time: ${new Date().toISOString()}`); + console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); + console.log(` Database: ${dbConfig.database}\n`); + + await PostgresDatabase.connect({ + dbName: dbConfig.database, + port: dbConfig.port, + host: dbConfig.host, + user: dbConfig.user, + password: dbConfig.password, + }); + + console.log("✅ Database connected\n"); + + const statsBefore = await RevokedTokenModel.getStats(); + console.log(`📊 Before cleanup:`); + console.log(` - Total tokens: ${statsBefore.total}`); + console.log(` - Active tokens: ${statsBefore.active}`); + console.log(` - Expired tokens: ${statsBefore.expired}`); + + const deleted = await RevokedTokenModel.cleanExpiredTokens(); + + console.log(`\n✅ Cleaned up ${deleted} expired tokens`); + + const statsAfter = await RevokedTokenModel.getStats(); + console.log(`\n📊 After cleanup:`); + console.log(` - Total tokens: ${statsAfter.total}`); + console.log(` - Active tokens: ${statsAfter.active}`); + console.log(` - Expired tokens: ${statsAfter.expired}`); + + console.log("\n✨ Cleanup completed successfully!\n"); + } catch (error: unknown) { + console.error("\n❌ Error during cleanup:"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(errorMessage); + process.exit(1); + } finally { + await PostgresDatabase.disconnect(); + } } cleanupRevokedTokens(); - diff --git a/backend/scripts/drop-db.ts b/backend/scripts/drop-db.ts index 238c93c..a2b7fc5 100644 --- a/backend/scripts/drop-db.ts +++ b/backend/scripts/drop-db.ts @@ -1,103 +1,120 @@ -import 'dotenv/config'; -import { Client } from 'pg'; -import { get } from 'env-var'; +import "dotenv/config"; +import { get } from "env-var"; +import { Client } from "pg"; const dbConfig = { - host: get('POSTGRES_HOST').default('localhost').asString(), - port: get('POSTGRES_PORT').default(5432).asPortNumber(), - user: get('POSTGRES_USER').required().asString(), - password: get('POSTGRES_PASSWORD').required().asString(), - database: get('POSTGRES_DB').required().asString(), + host: get("POSTGRES_HOST").default("localhost").asString(), + port: get("POSTGRES_PORT").default(5432).asPortNumber(), + user: get("POSTGRES_USER").required().asString(), + password: get("POSTGRES_PASSWORD").required().asString(), + database: get("POSTGRES_DB").required().asString(), }; async function dropDatabase() { - const client = new Client(dbConfig); + const client = new Client(dbConfig); - try { - console.log('🔌 Conectando a PostgreSQL...'); - console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); - console.log(` Database: ${dbConfig.database}`); - console.log(` User: ${dbConfig.user}`); + try { + console.log("🔌 Conectando a PostgreSQL..."); + console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); + console.log(` Database: ${dbConfig.database}`); + console.log(` User: ${dbConfig.user}`); - await client.connect(); - console.log('✅ Conexión establecida\n'); + await client.connect(); + console.log("✅ Conexión establecida\n"); - console.log('⚠️ ADVERTENCIA: Se eliminarán TODAS las tablas de la base de datos\n'); + console.log( + "⚠️ ADVERTENCIA: Se eliminarán TODAS las tablas de la base de datos\n", + ); - console.log('📋 Obteniendo lista de tablas...\n'); - const tablesQuery = ` - SELECT tablename - FROM pg_tables + console.log("📋 Obteniendo lista de tablas...\n"); + const tablesQuery = ` + SELECT tablename + FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename; `; - const tablesResult = await client.query(tablesQuery); - const tablesToDrop = tablesResult.rows.map((row: any) => row.tablename); - - if (tablesToDrop.length === 0) { - console.log('ℹ️ No hay tablas para eliminar.\n'); - } else { - console.log(`📦 Eliminando ${tablesToDrop.length} tablas...\n`); - - let droppedCount = 0; - let notFoundCount = 0; - - for (let i = 0; i < tablesToDrop.length; i++) { - const tableName = tablesToDrop[i]; - - try { - const query = `DROP TABLE IF EXISTS ${tableName} CASCADE`; - await client.query(query); - droppedCount++; - console.log(` ✅ [${i + 1}/${tablesToDrop.length}] Tabla eliminada: ${tableName}`); - } catch (error: any) { - if (error.message.includes('does not exist') || - error.message.includes('not exist')) { - notFoundCount++; - console.log(` ⚠️ [${i + 1}/${tablesToDrop.length}] Tabla no existe: ${tableName}`); - } else { - console.error(` ❌ [${i + 1}/${tablesToDrop.length}] Error al eliminar ${tableName}:`, error.message); - throw error; - } - } - } - - console.log(`\n📊 Resumen:`); - console.log(` ✅ Eliminadas: ${droppedCount}`); - if (notFoundCount > 0) console.log(` ⚠️ No encontradas: ${notFoundCount}`); - } - - console.log('\n📦 Eliminando extensiones personalizadas...\n'); - - const extensionsToDrop = ['pgcrypto']; - for (const extName of extensionsToDrop) { - try { - await client.query(`DROP EXTENSION IF EXISTS "${extName}" CASCADE`); - console.log(` ✅ Extensión eliminada: ${extName}`); - } catch (error: any) { - if (error.message.includes('does not exist')) { - console.log(` ⚠️ Extensión no existe: ${extName}`); - } else { - console.log(` ⚠️ No se pudo eliminar extensión: ${error.message}`); - } - } - } - - console.log('\n✅ Base de datos reseteada correctamente!'); - console.log('🎉 Todas las tablas han sido eliminadas.\n'); - console.log('💡 Puedes ejecutar "npm run db:migrate" para recrearlas.\n'); - - } catch (error: any) { - console.error('\n❌ Error al eliminar la base de datos:'); - console.error(error.message); - process.exit(1); - } finally { - await client.end(); - console.log('🔌 Conexión cerrada'); - } + const tablesResult = await client.query(tablesQuery); + const tablesToDrop = tablesResult.rows.map( + (row: { tablename: string }) => row.tablename, + ); + + if (tablesToDrop.length === 0) { + console.log("ℹ️ No hay tablas para eliminar.\n"); + } else { + console.log(`📦 Eliminando ${tablesToDrop.length} tablas...\n`); + + let droppedCount = 0; + let notFoundCount = 0; + + for (let i = 0; i < tablesToDrop.length; i++) { + const tableName = tablesToDrop[i]; + + try { + const query = `DROP TABLE IF EXISTS ${tableName} CASCADE`; + await client.query(query); + droppedCount++; + console.log( + ` ✅ [${i + 1}/${tablesToDrop.length}] Tabla eliminada: ${tableName}`, + ); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + if ( + errorMessage.includes("does not exist") || + errorMessage.includes("not exist") + ) { + notFoundCount++; + console.log( + ` ⚠️ [${i + 1}/${tablesToDrop.length}] Tabla no existe: ${tableName}`, + ); + } else { + console.error( + ` ❌ [${i + 1}/${tablesToDrop.length}] Error al eliminar ${tableName}:`, + errorMessage, + ); + throw error; + } + } + } + + console.log(`\n📊 Resumen:`); + console.log(` ✅ Eliminadas: ${droppedCount}`); + if (notFoundCount > 0) + console.log(` ⚠️ No encontradas: ${notFoundCount}`); + } + + console.log("\n📦 Eliminando extensiones personalizadas...\n"); + + const extensionsToDrop = ["pgcrypto"]; + for (const extName of extensionsToDrop) { + try { + await client.query(`DROP EXTENSION IF EXISTS "${extName}" CASCADE`); + console.log(` ✅ Extensión eliminada: ${extName}`); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + if (errorMessage.includes("does not exist")) { + console.log(` ⚠️ Extensión no existe: ${extName}`); + } else { + console.log(` ⚠️ No se pudo eliminar extensión: ${errorMessage}`); + } + } + } + + console.log("\n✅ Base de datos reseteada correctamente!"); + console.log("🎉 Todas las tablas han sido eliminadas.\n"); + console.log('💡 Puedes ejecutar "npm run db:migrate" para recrearlas.\n'); + } catch (error: unknown) { + console.error("\n❌ Error al eliminar la base de datos:"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(errorMessage); + process.exit(1); + } finally { + await client.end(); + console.log("🔌 Conexión cerrada"); + } } dropDatabase(); - - diff --git a/backend/scripts/migrate-db.ts b/backend/scripts/migrate-db.ts index 0f407ff..30fd2ef 100644 --- a/backend/scripts/migrate-db.ts +++ b/backend/scripts/migrate-db.ts @@ -1,177 +1,212 @@ -import 'dotenv/config'; -import { Client } from 'pg'; -import { readFileSync, readdirSync } from 'fs'; -import { join } from 'path'; -import { get } from 'env-var'; +import "dotenv/config"; + +import { get } from "env-var"; +import { Client } from "pg"; + +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; const dbConfig = { - host: get('POSTGRES_HOST').default('localhost').asString(), - port: get('POSTGRES_PORT').default(5432).asPortNumber(), - user: get('POSTGRES_USER').required().asString(), - password: get('POSTGRES_PASSWORD').required().asString(), - database: get('POSTGRES_DB').required().asString(), + host: get("POSTGRES_HOST").default("localhost").asString(), + port: get("POSTGRES_PORT").default(5432).asPortNumber(), + user: get("POSTGRES_USER").required().asString(), + password: get("POSTGRES_PASSWORD").required().asString(), + database: get("POSTGRES_DB").required().asString(), }; async function migrateDatabase() { - const client = new Client(dbConfig); - - try { - console.log('🔌 Conectando a PostgreSQL...'); - console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); - console.log(` Database: ${dbConfig.database}`); - console.log(` User: ${dbConfig.user}`); - - await client.connect(); - console.log('✅ Conexión establecida\n'); - - const sqlPath = join(process.cwd(), 'src/data/postgres/models/schema.sql'); - console.log(`📄 Leyendo archivo SQL: ${sqlPath}`); - - const sql = readFileSync(sqlPath, 'utf-8'); - - console.log('📦 Procesando script SQL...\n'); - - let cleanSql = sql - .replace(/--.*$/gm, '') - .replace(/\/\*[\s\S]*?\*\//g, ''); - - const statements = cleanSql - .split(';') - .map(stmt => stmt.trim()) - .filter(stmt => { - const trimmed = stmt.trim(); - return trimmed.length > 0 && - !trimmed.match(/^\s*$/) && - !trimmed.match(/^COMMENT\s+ON/i); - }); - - console.log(`📦 Ejecutando ${statements.length} sentencias SQL...\n`); - - let successCount = 0; - let skippedCount = 0; - let errorCount = 0; - - for (let i = 0; i < statements.length; i++) { - const statement = statements[i]; - - if (statement.length === 0) continue; - - try { - await client.query(statement); - successCount++; - - const createMatch = statement.match(/CREATE\s+(?:TABLE|EXTENSION|INDEX)\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?/i); - const objectName = createMatch ? createMatch[1] : null; - - if (objectName) { - const type = statement.match(/CREATE\s+(\w+)/i)?.[1]?.toLowerCase() || 'objeto'; - console.log(` ✅ [${i + 1}/${statements.length}] ${type}: ${objectName}`); - } else { - const firstLine = statement.split('\n').find(line => { - const trimmed = line.trim(); - return trimmed && !trimmed.startsWith('--'); - }) || ''; - const preview = firstLine.substring(0, 60).trim() || `Sentencia ${i + 1}`; - console.log(` ✅ [${i + 1}/${statements.length}] ${preview}...`); - } - } catch (err: any) { - const errorMessage = err.message.toLowerCase(); - if (errorMessage.includes('already exists') || - errorMessage.includes('does not exist') || - errorMessage.includes('duplicate key') || - errorMessage.includes('duplicate') || - errorMessage.includes('cannot be implemented') || - errorMessage.includes('constraint') && errorMessage.includes('already')) { - skippedCount++; - - const createMatch = statement.match(/CREATE\s+(?:TABLE|EXTENSION|INDEX)\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?/i); - const objectName = createMatch ? createMatch[1] : null; - - if (objectName) { - console.log(` ⚠️ [${i + 1}/${statements.length}] ${objectName} - Ya existe (se ignora)`); - } else { - const firstLine = statement.split('\n').find(line => { - const trimmed = line.trim(); - return trimmed && !trimmed.startsWith('--'); - }) || ''; - const preview = firstLine.substring(0, 60).trim() || `Sentencia ${i + 1}`; - console.log(` ⚠️ [${i + 1}/${statements.length}] ${preview} - Ya existe (se ignora)`); - } - } else { - errorCount++; - console.error(` ❌ [${i + 1}/${statements.length}] Error:`, err.message); - const preview = statement.substring(0, 200).replace(/\n/g, ' '); - console.error(` SQL: ${preview}...`); - throw err; - } - } - } - - console.log(`\n📊 Resumen del schema:`); - console.log(` ✅ Exitosas: ${successCount}`); - if (skippedCount > 0) console.log(` ⚠️ Omitidas (ya existían): ${skippedCount}`); - if (errorCount > 0) console.log(` ❌ Errores: ${errorCount}`); - - const migrationsPath = join(process.cwd(), 'src/data/postgres/migrations'); - let migrationFiles: string[] = []; - - try { - migrationFiles = readdirSync(migrationsPath) - .filter(file => file.endsWith('.sql')) - .sort(); - } catch (error) { - console.log('\n⚠️ No se encontró la carpeta de migraciones, continuando...\n'); - } - - if (migrationFiles.length > 0) { - console.log(`\n📦 Ejecutando ${migrationFiles.length} migración(es) adicional(es)...\n`); - - let migrationSuccess = 0; - let migrationSkipped = 0; - let migrationErrors = 0; - - for (const migrationFile of migrationFiles) { - try { - const migrationPath = join(migrationsPath, migrationFile); - console.log(`📄 Ejecutando: ${migrationFile}`); - const migrationSql = readFileSync(migrationPath, 'utf-8'); - - await client.query(migrationSql); - migrationSuccess++; - console.log(` ✅ ${migrationFile} ejecutada correctamente\n`); - } catch (err: any) { - const errorMessage = err.message.toLowerCase(); - if (errorMessage.includes('already exists') || - errorMessage.includes('does not exist') || - errorMessage.includes('duplicate')) { - migrationSkipped++; - console.log(` ⚠️ ${migrationFile} - Ya aplicada (se ignora)\n`); - } else { - migrationErrors++; - console.error(` ❌ Error en ${migrationFile}:`, err.message); - throw err; - } - } - } - - console.log(`📊 Resumen de migraciones:`); - console.log(` ✅ Exitosas: ${migrationSuccess}`); - if (migrationSkipped > 0) console.log(` ⚠️ Omitidas: ${migrationSkipped}`); - if (migrationErrors > 0) console.log(` ❌ Errores: ${migrationErrors}`); - } - - console.log('\n✅ Base de datos montada correctamente!'); - console.log('🎉 Todas las tablas y migraciones han sido aplicadas.\n'); - - } catch (error: any) { - console.error('\n❌ Error al montar la base de datos:'); - console.error(error.message); - process.exit(1); - } finally { - await client.end(); - console.log('🔌 Conexión cerrada'); - } + const client = new Client(dbConfig); + + try { + console.log("🔌 Conectando a PostgreSQL..."); + console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); + console.log(` Database: ${dbConfig.database}`); + console.log(` User: ${dbConfig.user}`); + + await client.connect(); + console.log("✅ Conexión establecida\n"); + + const sqlPath = join(process.cwd(), "src/data/postgres/models/schema.sql"); + console.log(`📄 Leyendo archivo SQL: ${sqlPath}`); + + const sql = readFileSync(sqlPath, "utf-8"); + + console.log("📦 Procesando script SQL...\n"); + + const cleanSql = sql + .replace(/--.*$/gm, "") + .replace(/\/\*[\s\S]*?\*\//g, ""); + + const statements = cleanSql + .split(";") + .map((stmt) => stmt.trim()) + .filter((stmt) => { + const trimmed = stmt.trim(); + return ( + trimmed.length > 0 && + !trimmed.match(/^\s*$/) && + !trimmed.match(/^COMMENT\s+ON/i) + ); + }); + + console.log(`📦 Ejecutando ${statements.length} sentencias SQL...\n`); + + let successCount = 0; + let skippedCount = 0; + let errorCount = 0; + + for (let i = 0; i < statements.length; i++) { + const statement = statements[i]; + + if (statement.length === 0) continue; + + try { + await client.query(statement); + successCount++; + + const createMatch = statement.match( + /CREATE\s+(?:TABLE|EXTENSION|INDEX)\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?/i, + ); + const objectName = createMatch ? createMatch[1] : null; + + if (objectName) { + const type = + statement.match(/CREATE\s+(\w+)/i)?.[1]?.toLowerCase() || "objeto"; + console.log( + ` ✅ [${i + 1}/${statements.length}] ${type}: ${objectName}`, + ); + } else { + const firstLine = + statement.split("\n").find((line) => { + const trimmed = line.trim(); + return trimmed && !trimmed.startsWith("--"); + }) || ""; + const preview = + firstLine.substring(0, 60).trim() || `Sentencia ${i + 1}`; + console.log(` ✅ [${i + 1}/${statements.length}] ${preview}...`); + } + } catch (err: unknown) { + const error = err instanceof Error ? err : new Error("Unknown error"); + const errorMessage = error.message.toLowerCase(); + if ( + errorMessage.includes("already exists") || + errorMessage.includes("does not exist") || + errorMessage.includes("duplicate key") || + errorMessage.includes("duplicate") || + errorMessage.includes("cannot be implemented") || + (errorMessage.includes("constraint") && + errorMessage.includes("already")) + ) { + skippedCount++; + + const createMatch = statement.match( + /CREATE\s+(?:TABLE|EXTENSION|INDEX)\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?/i, + ); + const objectName = createMatch ? createMatch[1] : null; + + if (objectName) { + console.log( + ` ⚠️ [${i + 1}/${statements.length}] ${objectName} - Ya existe (se ignora)`, + ); + } else { + const firstLine = + statement.split("\n").find((line) => { + const trimmed = line.trim(); + return trimmed && !trimmed.startsWith("--"); + }) || ""; + const preview = + firstLine.substring(0, 60).trim() || `Sentencia ${i + 1}`; + console.log( + ` ⚠️ [${i + 1}/${statements.length}] ${preview} - Ya existe (se ignora)`, + ); + } + } else { + errorCount++; + console.error( + ` ❌ [${i + 1}/${statements.length}] Error:`, + error.message, + ); + const preview = statement.substring(0, 200).replace(/\n/g, " "); + console.error(` SQL: ${preview}...`); + throw error; + } + } + } + + console.log(`\n📊 Resumen del schema:`); + console.log(` ✅ Exitosas: ${successCount}`); + if (skippedCount > 0) + console.log(` ⚠️ Omitidas (ya existían): ${skippedCount}`); + if (errorCount > 0) console.log(` ❌ Errores: ${errorCount}`); + + const migrationsPath = join(process.cwd(), "src/data/postgres/migrations"); + let migrationFiles: string[] = []; + + try { + migrationFiles = readdirSync(migrationsPath) + .filter((file) => file.endsWith(".sql")) + .sort(); + } catch { + console.log( + "\n⚠️ No se encontró la carpeta de migraciones, continuando...\n", + ); + } + + if (migrationFiles.length > 0) { + console.log( + `\n📦 Ejecutando ${migrationFiles.length} migración(es) adicional(es)...\n`, + ); + + let migrationSuccess = 0; + let migrationSkipped = 0; + let migrationErrors = 0; + + for (const migrationFile of migrationFiles) { + try { + const migrationPath = join(migrationsPath, migrationFile); + console.log(`📄 Ejecutando: ${migrationFile}`); + const migrationSql = readFileSync(migrationPath, "utf-8"); + + await client.query(migrationSql); + migrationSuccess++; + console.log(` ✅ ${migrationFile} ejecutada correctamente\n`); + } catch (err: unknown) { + const error = err instanceof Error ? err : new Error("Unknown error"); + const errorMessage = error.message.toLowerCase(); + if ( + errorMessage.includes("already exists") || + errorMessage.includes("does not exist") || + errorMessage.includes("duplicate") + ) { + migrationSkipped++; + console.log(` ⚠️ ${migrationFile} - Ya aplicada (se ignora)\n`); + } else { + migrationErrors++; + console.error(` ❌ Error en ${migrationFile}:`, error.message); + throw error; + } + } + } + + console.log(`📊 Resumen de migraciones:`); + console.log(` ✅ Exitosas: ${migrationSuccess}`); + if (migrationSkipped > 0) + console.log(` ⚠️ Omitidas: ${migrationSkipped}`); + if (migrationErrors > 0) console.log(` ❌ Errores: ${migrationErrors}`); + } + + console.log("\n✅ Base de datos montada correctamente!"); + console.log("🎉 Todas las tablas y migraciones han sido aplicadas.\n"); + } catch (error: unknown) { + console.error("\n❌ Error al montar la base de datos:"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(errorMessage); + process.exit(1); + } finally { + await client.end(); + console.log("🔌 Conexión cerrada"); + } } migrateDatabase(); - diff --git a/backend/scripts/seed-admin.ts b/backend/scripts/seed-admin.ts index 57aad2e..2778eb7 100644 --- a/backend/scripts/seed-admin.ts +++ b/backend/scripts/seed-admin.ts @@ -1,91 +1,99 @@ -import 'dotenv/config'; -import { PostgresDatabase } from '../src/data/postgres/database'; -import { ProfileModel, RoleModel } from '../src/data/postgres/models'; -import { BcryptAdapter } from '../src/config/bcrypt.adaptar'; -import { get } from 'env-var'; +import "dotenv/config"; + +import { get } from "env-var"; + +import { BcryptAdapter } from "../src/config/bcrypt.adaptar"; +import { PostgresDatabase } from "../src/data/postgres/database"; +import { ProfileModel, RoleModel } from "../src/data/postgres/models"; const dbConfig = { - host: get('POSTGRES_HOST').default('localhost').asString(), - port: get('POSTGRES_PORT').default(5432).asPortNumber(), - user: get('POSTGRES_USER').required().asString(), - password: get('POSTGRES_PASSWORD').required().asString(), - database: get('POSTGRES_DB').required().asString(), + host: get("POSTGRES_HOST").default("localhost").asString(), + port: get("POSTGRES_PORT").default(5432).asPortNumber(), + user: get("POSTGRES_USER").required().asString(), + password: get("POSTGRES_PASSWORD").required().asString(), + database: get("POSTGRES_DB").required().asString(), }; const adminUser = { - first_name: 'Admin', - last_name: 'User', - email: 'admin@example.com', - password: 'admin123', - phone: undefined as string | undefined, + first_name: "Admin", + last_name: "User", + email: "admin@example.com", + password: "admin123", + phone: undefined as string | undefined, }; async function seedAdmin() { - const hashAdapter = new BcryptAdapter(); - - try { - console.log('🔌 Connecting to database...'); - console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); - console.log(` Database: ${dbConfig.database}`); - console.log(` User: ${dbConfig.user}\n`); - - await PostgresDatabase.connect({ - dbName: dbConfig.database, - port: dbConfig.port, - host: dbConfig.host, - user: dbConfig.user, - password: dbConfig.password, - }); - - console.log('✅ Connection established\n'); - - console.log('📦 Seeding admin user...\n'); - - const existingAdmin = await ProfileModel.findByEmail(adminUser.email); - - if (existingAdmin) { - console.log(` ⚠️ Admin user with email "${adminUser.email}" already exists (ID: ${existingAdmin.id})`); - console.log(' 💡 If you want to recreate it, delete it first from the database.\n'); - } else { - const adminRole = await RoleModel.findByName('admin'); - if (!adminRole) { - throw new Error('Admin role not found. Please run "npm run db:seed-roles" first.'); - } - - console.log(` 🔐 Hashing password...`); - const hashedPassword = await hashAdapter.hash(adminUser.password); - - console.log(` 👤 Creating admin user...`); - const newAdmin = await ProfileModel.create({ - first_name: adminUser.first_name, - last_name: adminUser.last_name, - email: adminUser.email, - password_hash: hashedPassword, - phone: adminUser.phone, - role_id: adminRole.id!, - }); - - console.log(` ✅ Admin user created successfully!`); - console.log(` ID: ${newAdmin.id}`); - console.log(` Email: ${newAdmin.email}`); - console.log(` Name: ${newAdmin.first_name} ${newAdmin.last_name}`); - console.log(` Role: Admin`); - console.log(` 📧 Login credentials:`); - console.log(` Email: ${adminUser.email}`); - console.log(` Password: ${adminUser.password}`); - console.log(''); - } - - console.log('✅ Process completed successfully!\n'); - - } catch (error: any) { - console.error('\n❌ Error seeding admin user:'); - console.error(error.message); - process.exit(1); - } finally { - await PostgresDatabase.disconnect(); - } + const hashAdapter = new BcryptAdapter(); + + try { + console.log("🔌 Connecting to database..."); + console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); + console.log(` Database: ${dbConfig.database}`); + console.log(` User: ${dbConfig.user}\n`); + + await PostgresDatabase.connect({ + dbName: dbConfig.database, + port: dbConfig.port, + host: dbConfig.host, + user: dbConfig.user, + password: dbConfig.password, + }); + + console.log("✅ Connection established\n"); + + console.log("📦 Seeding admin user...\n"); + + const existingAdmin = await ProfileModel.findByEmail(adminUser.email); + + if (existingAdmin) { + console.log( + ` ⚠️ Admin user with email "${adminUser.email}" already exists (ID: ${existingAdmin.id})`, + ); + console.log( + " 💡 If you want to recreate it, delete it first from the database.\n", + ); + } else { + const adminRole = await RoleModel.findByName("admin"); + if (!adminRole) { + throw new Error( + 'Admin role not found. Please run "npm run db:seed-roles" first.', + ); + } + + console.log(` 🔐 Hashing password...`); + const hashedPassword = await hashAdapter.hash(adminUser.password); + + console.log(` 👤 Creating admin user...`); + const newAdmin = await ProfileModel.create({ + first_name: adminUser.first_name, + last_name: adminUser.last_name, + email: adminUser.email, + password_hash: hashedPassword, + phone: adminUser.phone, + role_id: adminRole.id || 0, // Should be valid if role exists + }); + + console.log(` ✅ Admin user created successfully!`); + console.log(` ID: ${newAdmin.id}`); + console.log(` Email: ${newAdmin.email}`); + console.log(` Name: ${newAdmin.first_name} ${newAdmin.last_name}`); + console.log(` Role: Admin`); + console.log(` 📧 Login credentials:`); + console.log(` Email: ${adminUser.email}`); + console.log(` Password: ${adminUser.password}`); + console.log(""); + } + + console.log("✅ Process completed successfully!\n"); + } catch (error: unknown) { + console.error("\n❌ Error seeding admin user:"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(errorMessage); + process.exit(1); + } finally { + await PostgresDatabase.disconnect(); + } } seedAdmin(); - diff --git a/backend/scripts/seed-catalogs.ts b/backend/scripts/seed-catalogs.ts index 1ee0da1..e03b756 100644 --- a/backend/scripts/seed-catalogs.ts +++ b/backend/scripts/seed-catalogs.ts @@ -1,379 +1,453 @@ -import 'dotenv/config'; -import { PostgresDatabase } from '../src/data/postgres/database'; -import { get } from 'env-var'; +import "dotenv/config"; + +import { get } from "env-var"; + +import { PostgresDatabase } from "../src/data/postgres/database"; const dbConfig = { - host: get('POSTGRES_HOST').default('localhost').asString(), - port: get('POSTGRES_PORT').default(5432).asPortNumber(), - user: get('POSTGRES_USER').required().asString(), - password: get('POSTGRES_PASSWORD').required().asString(), - database: get('POSTGRES_DB').required().asString(), + host: get("POSTGRES_HOST").default("localhost").asString(), + port: get("POSTGRES_PORT").default(5432).asPortNumber(), + user: get("POSTGRES_USER").required().asString(), + password: get("POSTGRES_PASSWORD").required().asString(), + database: get("POSTGRES_DB").required().asString(), }; -async function insertIfNotExists(table: string, data: { [key: string]: any }, uniqueField: string): Promise<{ created: boolean; id: number }> { - const client = PostgresDatabase.getClient(); - const checkQuery = `SELECT id FROM ${table} WHERE ${uniqueField} = $1`; - const checkResult = await client.query(checkQuery, [data[uniqueField]]); - - if (checkResult.rows.length > 0) { - return { created: false, id: checkResult.rows[0].id }; - } - - const fields = Object.keys(data); - const values = Object.values(data); - const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); - const insertQuery = `INSERT INTO ${table} (${fields.join(', ')}) VALUES (${placeholders}) RETURNING id`; - const insertResult = await client.query(insertQuery, values); - return { created: true, id: insertResult.rows[0].id }; +async function insertIfNotExists( + table: string, + data: Record, + uniqueField: string, +): Promise<{ created: boolean; id: number }> { + const client = PostgresDatabase.getClient(); + const checkQuery = `SELECT id FROM ${table} WHERE ${uniqueField} = $1`; + const checkResult = await client.query(checkQuery, [data[uniqueField]]); + + if (checkResult.rows.length > 0) { + return { created: false, id: checkResult.rows[0].id }; + } + + const fields = Object.keys(data); + const values = Object.values(data); + const placeholders = values.map((_, i) => `$${i + 1}`).join(", "); + const insertQuery = `INSERT INTO ${table} (${fields.join(", ")}) VALUES (${placeholders}) RETURNING id`; + const insertResult = await client.query(insertQuery, values); + return { created: true, id: insertResult.rows[0].id }; } async function seedCatalogs() { - try { - console.log('🔌 Connecting to database...'); - console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); - console.log(` Database: ${dbConfig.database}`); - console.log(` User: ${dbConfig.user}\n`); - - await PostgresDatabase.connect({ - dbName: dbConfig.database, - port: dbConfig.port, - host: dbConfig.host, - user: dbConfig.user, - password: dbConfig.password, - }); + try { + console.log("🔌 Connecting to database..."); + console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); + console.log(` Database: ${dbConfig.database}`); + console.log(` User: ${dbConfig.user}\n`); - console.log('✅ Connection established\n'); - console.log('📦 Seeding catalogs...\n'); + await PostgresDatabase.connect({ + dbName: dbConfig.database, + port: dbConfig.port, + host: dbConfig.host, + user: dbConfig.user, + password: dbConfig.password, + }); - let totalCreated = 0; - let totalExisting = 0; + console.log("✅ Connection established\n"); + console.log("📦 Seeding catalogs...\n"); - console.log('💰 Currency Types:'); - const currencies = [ - { name: 'Peso Argentino', symbol: 'ARS' }, - { name: 'Dólar Estadounidense', symbol: 'USD' }, - { name: 'Euro', symbol: 'EUR' }, - ]; - for (const currency of currencies) { - const result = await insertIfNotExists('currency_types', currency, 'name'); - if (result.created) { - console.log(` ✅ "${currency.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${currency.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + let totalCreated = 0; + let totalExisting = 0; - console.log('\n💳 Payment Methods:'); - const paymentMethods = [ - { name: 'Efectivo' }, - { name: 'Transferencia Bancaria' }, - { name: 'Cheque' }, - { name: 'Tarjeta de Crédito' }, - { name: 'Tarjeta de Débito' }, - ]; - for (const method of paymentMethods) { - const result = await insertIfNotExists('payment_methods', method, 'name'); - if (result.created) { - console.log(` ✅ "${method.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${method.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("💰 Currency Types:"); + const currencies = [ + { name: "Peso Argentino", symbol: "ARS" }, + { name: "Dólar Estadounidense", symbol: "USD" }, + { name: "Euro", symbol: "EUR" }, + ]; + for (const currency of currencies) { + const result = await insertIfNotExists( + "currency_types", + currency, + "name", + ); + if (result.created) { + console.log(` ✅ "${currency.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${currency.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('\n📊 Payment Statuses:'); - const paymentStatuses = [ - { name: 'Pendiente' }, - { name: 'Pagado' }, - { name: 'Vencido' }, - { name: 'Cancelado' }, - ]; - for (const status of paymentStatuses) { - const result = await insertIfNotExists('payment_statuses', status, 'name'); - if (result.created) { - console.log(` ✅ "${status.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${status.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n💳 Payment Methods:"); + const paymentMethods = [ + { name: "Efectivo" }, + { name: "Transferencia Bancaria" }, + { name: "Cheque" }, + { name: "Tarjeta de Crédito" }, + { name: "Tarjeta de Débito" }, + ]; + for (const method of paymentMethods) { + const result = await insertIfNotExists("payment_methods", method, "name"); + if (result.created) { + console.log(` ✅ "${method.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${method.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('\n📞 Consultation Types:'); - const consultationTypes = [ - { name: 'Consulta General' }, - { name: 'Consulta de Propiedad' }, - { name: 'Consulta de Alquiler' }, - { name: 'Consulta de Compra' }, - ]; - for (const type of consultationTypes) { - const result = await insertIfNotExists('consultation_types', type, 'name'); - if (result.created) { - console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n📊 Payment Statuses:"); + const paymentStatuses = [ + { name: "Pendiente" }, + { name: "Pagado" }, + { name: "Vencido" }, + { name: "Cancelado" }, + ]; + for (const status of paymentStatuses) { + const result = await insertIfNotExists( + "payment_statuses", + status, + "name", + ); + if (result.created) { + console.log(` ✅ "${status.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${status.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('\n📅 Event Types:'); - const eventTypes = [ - { name: 'Visita a Propiedad' }, - { name: 'Reunión con Cliente' }, - { name: 'Llamada Telefónica' }, - { name: 'Email' }, - { name: 'Firma de Contrato' }, - { name: 'Entrega de Llaves' }, - ]; - for (const type of eventTypes) { - const result = await insertIfNotExists('event_types', type, 'name'); - if (result.created) { - console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n📞 Consultation Types:"); + const consultationTypes = [ + { name: "Consulta General" }, + { name: "Consulta de Propiedad" }, + { name: "Consulta de Alquiler" }, + { name: "Consulta de Compra" }, + ]; + for (const type of consultationTypes) { + const result = await insertIfNotExists( + "consultation_types", + type, + "name", + ); + if (result.created) { + console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } - console.log('\n👥 Contact Categories:'); - const contactCategories = [ - { name: 'Lead' }, - { name: 'Inquilino' }, - { name: 'Propietario' }, - ]; - for (const category of contactCategories) { - const result = await insertIfNotExists('contact_categories', category, 'name'); - if (result.created) { - console.log(` ✅ "${category.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${category.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n📅 Event Types:"); + const eventTypes = [ + { name: "Visita a Propiedad" }, + { name: "Reunión con Cliente" }, + { name: "Llamada Telefónica" }, + { name: "Email" }, + { name: "Firma de Contrato" }, + { name: "Entrega de Llaves" }, + ]; + for (const type of eventTypes) { + const result = await insertIfNotExists("event_types", type, "name"); + if (result.created) { + console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } - console.log('\n🏠 Property Search Types:'); - const propertySearchTypes = [ - { name: 'Casa' }, - { name: 'PH' }, - { name: 'Departamento' }, - { name: 'Local Comercial' }, - { name: 'Oficina' }, - { name: 'Terreno' }, - ]; - for (const type of propertySearchTypes) { - const result = await insertIfNotExists('property_search_types', type, 'name'); - if (result.created) { - console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n👥 Contact Categories:"); + const contactCategories = [ + { name: "Lead" }, + { name: "Inquilino" }, + { name: "Propietario" }, + ]; + for (const category of contactCategories) { + const result = await insertIfNotExists( + "contact_categories", + category, + "name", + ); + if (result.created) { + console.log(` ✅ "${category.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${category.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('\n⏳ Property Ages:'); - const propertyAges = [ - { name: 'A estrenar' }, - { name: '0-5 años' }, - { name: '6-10 años' }, - { name: '11-20 años' }, - { name: 'Más de 20 años' }, - ]; - for (const age of propertyAges) { - const result = await insertIfNotExists('property_ages', age, 'name'); - if (result.created) { - console.log(` ✅ "${age.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${age.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n🏠 Property Search Types:"); + const propertySearchTypes = [ + { name: "Casa" }, + { name: "PH" }, + { name: "Departamento" }, + { name: "Local Comercial" }, + { name: "Oficina" }, + { name: "Terreno" }, + ]; + for (const type of propertySearchTypes) { + const result = await insertIfNotExists( + "property_search_types", + type, + "name", + ); + if (result.created) { + console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } - console.log('\n🧭 Orientations:'); - const orientations = [ - { name: 'Norte' }, - { name: 'Sur' }, - { name: 'Este' }, - { name: 'Oeste' }, - { name: 'Noreste' }, - { name: 'Noroeste' }, - { name: 'Sureste' }, - { name: 'Suroeste' }, - ]; - for (const orientation of orientations) { - const result = await insertIfNotExists('orientations', orientation, 'name'); - if (result.created) { - console.log(` ✅ "${orientation.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${orientation.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n⏳ Property Ages:"); + const propertyAges = [ + { name: "A estrenar" }, + { name: "0-5 años" }, + { name: "6-10 años" }, + { name: "11-20 años" }, + { name: "Más de 20 años" }, + ]; + for (const age of propertyAges) { + const result = await insertIfNotExists("property_ages", age, "name"); + if (result.created) { + console.log(` ✅ "${age.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${age.name}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } - console.log('\n📐 Dispositions:'); - const dispositions = [ - { name: 'Frente' }, - { name: 'Contrafrente' }, - { name: 'Interno' }, - { name: 'Lateral' }, - ]; - for (const disposition of dispositions) { - const result = await insertIfNotExists('dispositions', disposition, 'name'); - if (result.created) { - console.log(` ✅ "${disposition.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${disposition.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n🧭 Orientations:"); + const orientations = [ + { name: "Norte" }, + { name: "Sur" }, + { name: "Este" }, + { name: "Oeste" }, + { name: "Noreste" }, + { name: "Noroeste" }, + { name: "Sureste" }, + { name: "Suroeste" }, + ]; + for (const orientation of orientations) { + const result = await insertIfNotExists( + "orientations", + orientation, + "name", + ); + if (result.created) { + console.log(` ✅ "${orientation.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${orientation.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('\n📋 Property Situations:'); - const propertySituations = [ - { name: 'Regular' }, - { name: 'En proceso de escrituración' }, - { name: 'Escriturado' }, - { name: 'En sucesión' }, - ]; - for (const situation of propertySituations) { - const result = await insertIfNotExists('property_situations', situation, 'name'); - if (result.created) { - console.log(` ✅ "${situation.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${situation.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n📐 Dispositions:"); + const dispositions = [ + { name: "Frente" }, + { name: "Contrafrente" }, + { name: "Interno" }, + { name: "Lateral" }, + ]; + for (const disposition of dispositions) { + const result = await insertIfNotExists( + "dispositions", + disposition, + "name", + ); + if (result.created) { + console.log(` ✅ "${disposition.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${disposition.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('\n🏘️ Property Types:'); - const propertyTypes = [ - { name: 'Casa' }, - { name: 'Departamento' }, - { name: 'PH' }, - { name: 'Local Comercial' }, - { name: 'Oficina' }, - { name: 'Terreno' }, - { name: 'Cochera' }, - { name: 'Depósito' }, - ]; - for (const type of propertyTypes) { - const result = await insertIfNotExists('property_types', type, 'name'); - if (result.created) { - console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n📋 Property Situations:"); + const propertySituations = [ + { name: "Regular" }, + { name: "En proceso de escrituración" }, + { name: "Escriturado" }, + { name: "En sucesión" }, + ]; + for (const situation of propertySituations) { + const result = await insertIfNotExists( + "property_situations", + situation, + "name", + ); + if (result.created) { + console.log(` ✅ "${situation.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${situation.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('\n💼 Property Operation Types:'); - const operationTypes = [ - { name: 'Venta' }, - { name: 'Alquiler' }, - { name: 'Alquiler Temporal' }, - ]; - for (const type of operationTypes) { - const result = await insertIfNotExists('property_operation_types', type, 'name'); - if (result.created) { - console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n🏘️ Property Types:"); + const propertyTypes = [ + { name: "Casa" }, + { name: "Departamento" }, + { name: "PH" }, + { name: "Local Comercial" }, + { name: "Oficina" }, + { name: "Terreno" }, + { name: "Cochera" }, + { name: "Depósito" }, + ]; + for (const type of propertyTypes) { + const result = await insertIfNotExists("property_types", type, "name"); + if (result.created) { + console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } - console.log('\n📊 Property Statuses:'); - const propertyStatuses = [ - { name: 'Disponible' }, - { name: 'En Venta' }, - { name: 'Alquilada' }, - { name: 'Vendida' }, - { name: 'Reservada' }, - ]; - for (const status of propertyStatuses) { - const result = await insertIfNotExists('property_statuses', status, 'name'); - if (result.created) { - console.log(` ✅ "${status.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${status.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n💼 Property Operation Types:"); + const operationTypes = [ + { name: "Venta" }, + { name: "Alquiler" }, + { name: "Alquiler Temporal" }, + ]; + for (const type of operationTypes) { + const result = await insertIfNotExists( + "property_operation_types", + type, + "name", + ); + if (result.created) { + console.log(` ✅ "${type.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${type.name}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } - console.log('\n👁️ Visibility Statuses:'); - const visibilityStatuses = [ - { name: 'Publicado' }, - { name: 'Solo Interno' }, - { name: 'Archivada' }, - ]; - for (const status of visibilityStatuses) { - const result = await insertIfNotExists('visibility_statuses', status, 'name'); - if (result.created) { - console.log(` ✅ "${status.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${status.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n📊 Property Statuses:"); + const propertyStatuses = [ + { name: "Disponible" }, + { name: "En Venta" }, + { name: "Alquilada" }, + { name: "Vendida" }, + { name: "Reservada" }, + ]; + for (const status of propertyStatuses) { + const result = await insertIfNotExists( + "property_statuses", + status, + "name", + ); + if (result.created) { + console.log(` ✅ "${status.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${status.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('\n🔧 Catalog Services:'); - const services = [ - { name: 'Agua corriente' }, - { name: 'Gas natural' }, - { name: 'Gas envasado' }, - { name: 'Electricidad' }, - { name: 'Cloacas' }, - { name: 'Piscina' }, - { name: 'Quincho' }, - { name: 'Jardín' }, - { name: 'Garage' }, - { name: 'Ascensor' }, - { name: 'Seguridad 24hs' }, - { name: 'Lavadero' }, - { name: 'Terraza' }, - { name: 'Balcón' }, - { name: 'Aire acondicionado' }, - { name: 'Calefacción' }, - ]; - for (const service of services) { - const result = await insertIfNotExists('catalog_services', service, 'name'); - if (result.created) { - console.log(` ✅ "${service.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${service.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } + console.log("\n👁️ Visibility Statuses:"); + const visibilityStatuses = [ + { name: "Publicado" }, + { name: "Solo Interno" }, + { name: "Archivada" }, + ]; + for (const status of visibilityStatuses) { + const result = await insertIfNotExists( + "visibility_statuses", + status, + "name", + ); + if (result.created) { + console.log(` ✅ "${status.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${status.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log(`\n📊 Summary:`); - console.log(` ✅ Created: ${totalCreated}`); - console.log(` ⚠️ Already existed: ${totalExisting}`); - console.log(` 📋 Total: ${totalCreated + totalExisting}\n`); + console.log("\n🔧 Catalog Services:"); + const services = [ + { name: "Agua corriente" }, + { name: "Gas natural" }, + { name: "Gas envasado" }, + { name: "Electricidad" }, + { name: "Cloacas" }, + { name: "Piscina" }, + { name: "Quincho" }, + { name: "Jardín" }, + { name: "Garage" }, + { name: "Ascensor" }, + { name: "Seguridad 24hs" }, + { name: "Lavadero" }, + { name: "Terraza" }, + { name: "Balcón" }, + { name: "Aire acondicionado" }, + { name: "Calefacción" }, + ]; + for (const service of services) { + const result = await insertIfNotExists( + "catalog_services", + service, + "name", + ); + if (result.created) { + console.log(` ✅ "${service.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${service.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } - console.log('✅ Process completed successfully!\n'); + console.log(`\n📊 Summary:`); + console.log(` ✅ Created: ${totalCreated}`); + console.log(` ⚠️ Already existed: ${totalExisting}`); + console.log(` 📋 Total: ${totalCreated + totalExisting}\n`); - } catch (error: any) { - console.error('\n❌ Error seeding catalogs:'); - console.error(error.message); - process.exit(1); - } finally { - await PostgresDatabase.disconnect(); - } + console.log("✅ Process completed successfully!\n"); + } catch (error: unknown) { + console.error("\n❌ Error seeding catalogs:"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(errorMessage); + process.exit(1); + } finally { + await PostgresDatabase.disconnect(); + } } seedCatalogs(); - diff --git a/backend/scripts/seed-geography.ts b/backend/scripts/seed-geography.ts index b95419a..d3c687e 100644 --- a/backend/scripts/seed-geography.ts +++ b/backend/scripts/seed-geography.ts @@ -1,228 +1,249 @@ -import 'dotenv/config'; -import { PostgresDatabase } from '../src/data/postgres/database'; -import { get } from 'env-var'; +import "dotenv/config"; + +import { get } from "env-var"; + +import { PostgresDatabase } from "../src/data/postgres/database"; const dbConfig = { - host: get('POSTGRES_HOST').default('localhost').asString(), - port: get('POSTGRES_PORT').default(5432).asPortNumber(), - user: get('POSTGRES_USER').required().asString(), - password: get('POSTGRES_PASSWORD').required().asString(), - database: get('POSTGRES_DB').required().asString(), + host: get("POSTGRES_HOST").default("localhost").asString(), + port: get("POSTGRES_PORT").default(5432).asPortNumber(), + user: get("POSTGRES_USER").required().asString(), + password: get("POSTGRES_PASSWORD").required().asString(), + database: get("POSTGRES_DB").required().asString(), }; -async function insertIfNotExists(table: string, data: { [key: string]: any }, uniqueField: string | string[]): Promise<{ created: boolean; id: number }> { - const client = PostgresDatabase.getClient(); - - let checkQuery: string; - let checkValues: any[]; - - if (Array.isArray(uniqueField)) { - const conditions = uniqueField.map((field, index) => `${field} = $${index + 1}`).join(' AND '); - checkQuery = `SELECT id FROM ${table} WHERE ${conditions}`; - checkValues = uniqueField.map(field => data[field]); - } else { - checkQuery = `SELECT id FROM ${table} WHERE ${uniqueField} = $1`; - checkValues = [data[uniqueField]]; - } - - const checkResult = await client.query(checkQuery, checkValues); - - if (checkResult.rows.length > 0) { - return { created: false, id: checkResult.rows[0].id }; - } - - const fields = Object.keys(data); - const values = Object.values(data); - const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); - const insertQuery = `INSERT INTO ${table} (${fields.join(', ')}) VALUES (${placeholders}) RETURNING id`; - const insertResult = await client.query(insertQuery, values); - return { created: true, id: insertResult.rows[0].id }; +async function insertIfNotExists( + table: string, + data: Record, + uniqueField: string | string[], +): Promise<{ created: boolean; id: number }> { + const client = PostgresDatabase.getClient(); + + let checkQuery: string; + let checkValues: unknown[]; + + if (Array.isArray(uniqueField)) { + const conditions = uniqueField + .map((field, index) => `${field} = $${index + 1}`) + .join(" AND "); + checkQuery = `SELECT id FROM ${table} WHERE ${conditions}`; + checkValues = uniqueField.map((field) => data[field]); + } else { + checkQuery = `SELECT id FROM ${table} WHERE ${uniqueField} = $1`; + checkValues = [data[uniqueField]]; + } + + const checkResult = await client.query(checkQuery, checkValues); + + if (checkResult.rows.length > 0) { + return { created: false, id: checkResult.rows[0].id }; + } + + const fields = Object.keys(data); + const values = Object.values(data); + const placeholders = values.map((_, i) => `$${i + 1}`).join(", "); + const insertQuery = `INSERT INTO ${table} (${fields.join(", ")}) VALUES (${placeholders}) RETURNING id`; + const insertResult = await client.query(insertQuery, values); + return { created: true, id: insertResult.rows[0].id }; } async function seedGeography() { - try { - console.log('🔌 Connecting to database...'); - console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); - console.log(` Database: ${dbConfig.database}`); - console.log(` User: ${dbConfig.user}\n`); - - await PostgresDatabase.connect({ - dbName: dbConfig.database, - port: dbConfig.port, - host: dbConfig.host, - user: dbConfig.user, - password: dbConfig.password, - }); - - console.log('✅ Connection established\n'); - console.log('📦 Seeding geography data (Argentina)...\n'); - - let totalCreated = 0; - let totalExisting = 0; - - console.log('🌍 Countries:'); - const argentina = await insertIfNotExists('countries', { name: 'Argentina' }, 'name'); - if (argentina.created) { - console.log(` ✅ "Argentina" - Created (ID: ${argentina.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "Argentina" - Already exists (ID: ${argentina.id})`); - totalExisting++; - } - - console.log('\n🗺️ Provinces:'); - const provinces = [ - { name: 'Buenos Aires', country_id: argentina.id }, - { name: 'Córdoba', country_id: argentina.id }, - { name: 'Santa Fe', country_id: argentina.id }, - { name: 'Mendoza', country_id: argentina.id }, - { name: 'Tucumán', country_id: argentina.id }, - { name: 'Salta', country_id: argentina.id }, - { name: 'Entre Ríos', country_id: argentina.id }, - { name: 'Misiones', country_id: argentina.id }, - { name: 'Corrientes', country_id: argentina.id }, - { name: 'Chaco', country_id: argentina.id }, - { name: 'Santiago del Estero', country_id: argentina.id }, - { name: 'San Juan', country_id: argentina.id }, - { name: 'Jujuy', country_id: argentina.id }, - { name: 'Río Negro', country_id: argentina.id }, - { name: 'Formosa', country_id: argentina.id }, - { name: 'Neuquén', country_id: argentina.id }, - { name: 'Catamarca', country_id: argentina.id }, - { name: 'La Rioja', country_id: argentina.id }, - { name: 'Chubut', country_id: argentina.id }, - { name: 'San Luis', country_id: argentina.id }, - { name: 'La Pampa', country_id: argentina.id }, - { name: 'Santa Cruz', country_id: argentina.id }, - { name: 'Tierra del Fuego', country_id: argentina.id }, - ]; - - const provinceMap: { [key: string]: number } = {}; - - for (const province of provinces) { - const result = await insertIfNotExists('provinces', province, 'name'); - provinceMap[province.name] = result.id; - if (result.created) { - console.log(` ✅ "${province.name}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${province.name}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } - - console.log('\n🏙️ Cities (Buenos Aires Province):'); - const buenosAiresCities = [ - 'Ciudad Autónoma de Buenos Aires', - 'La Plata', - 'Mar del Plata', - 'Bahía Blanca', - 'San Nicolás', - 'Zárate', - 'Campana', - 'Pilar', - 'Tigre', - 'San Isidro', - 'Vicente López', - 'Quilmes', - 'Avellaneda', - 'Lanús', - 'Lomas de Zamora', - 'Banfield', - 'Adrogué', - 'Burzaco', - 'Temperley', - 'Morón', - 'Ituzaingó', - 'Merlo', - 'Moreno', - 'San Miguel', - 'Malvinas Argentinas', - ]; - - for (const cityName of buenosAiresCities) { - const result = await insertIfNotExists('cities', { name: cityName, province_id: provinceMap['Buenos Aires'] }, ['name', 'province_id']); - if (result.created) { - console.log(` ✅ "${cityName}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${cityName}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } - - console.log('\n🏙️ Cities (Córdoba Province):'); - const cordobaCities = [ - 'Córdoba', - 'Villa Carlos Paz', - 'Río Cuarto', - 'Villa María', - 'San Francisco', - ]; - - for (const cityName of cordobaCities) { - const result = await insertIfNotExists('cities', { name: cityName, province_id: provinceMap['Córdoba'] }, ['name', 'province_id']); - if (result.created) { - console.log(` ✅ "${cityName}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${cityName}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } - - console.log('\n🏙️ Cities (Santa Fe Province):'); - const santaFeCities = [ - 'Rosario', - 'Santa Fe', - 'Rafaela', - 'Venado Tuerto', - ]; - - for (const cityName of santaFeCities) { - const result = await insertIfNotExists('cities', { name: cityName, province_id: provinceMap['Santa Fe'] }, ['name', 'province_id']); - if (result.created) { - console.log(` ✅ "${cityName}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${cityName}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } - - console.log('\n🏙️ Cities (Mendoza Province):'); - const mendozaCities = [ - 'Mendoza', - 'San Rafael', - 'Godoy Cruz', - ]; - - for (const cityName of mendozaCities) { - const result = await insertIfNotExists('cities', { name: cityName, province_id: provinceMap['Mendoza'] }, ['name', 'province_id']); - if (result.created) { - console.log(` ✅ "${cityName}" - Created (ID: ${result.id})`); - totalCreated++; - } else { - console.log(` ⚠️ "${cityName}" - Already exists (ID: ${result.id})`); - totalExisting++; - } - } - - console.log(`\n📊 Summary:`); - console.log(` ✅ Created: ${totalCreated}`); - console.log(` ⚠️ Already existed: ${totalExisting}`); - console.log(` 📋 Total: ${totalCreated + totalExisting}\n`); - - console.log('✅ Process completed successfully!\n'); - - } catch (error: any) { - console.error('\n❌ Error seeding geography:'); - console.error(error.message); - process.exit(1); - } finally { - await PostgresDatabase.disconnect(); - } + try { + console.log("🔌 Connecting to database..."); + console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); + console.log(` Database: ${dbConfig.database}`); + console.log(` User: ${dbConfig.user}\n`); + + await PostgresDatabase.connect({ + dbName: dbConfig.database, + port: dbConfig.port, + host: dbConfig.host, + user: dbConfig.user, + password: dbConfig.password, + }); + + console.log("✅ Connection established\n"); + console.log("📦 Seeding geography data (Argentina)...\n"); + + let totalCreated = 0; + let totalExisting = 0; + + console.log("🌍 Countries:"); + const argentina = await insertIfNotExists( + "countries", + { name: "Argentina" }, + "name", + ); + if (argentina.created) { + console.log(` ✅ "Argentina" - Created (ID: ${argentina.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "Argentina" - Already exists (ID: ${argentina.id})`); + totalExisting++; + } + + console.log("\n🗺️ Provinces:"); + const provinces = [ + { name: "Buenos Aires", country_id: argentina.id }, + { name: "Córdoba", country_id: argentina.id }, + { name: "Santa Fe", country_id: argentina.id }, + { name: "Mendoza", country_id: argentina.id }, + { name: "Tucumán", country_id: argentina.id }, + { name: "Salta", country_id: argentina.id }, + { name: "Entre Ríos", country_id: argentina.id }, + { name: "Misiones", country_id: argentina.id }, + { name: "Corrientes", country_id: argentina.id }, + { name: "Chaco", country_id: argentina.id }, + { name: "Santiago del Estero", country_id: argentina.id }, + { name: "San Juan", country_id: argentina.id }, + { name: "Jujuy", country_id: argentina.id }, + { name: "Río Negro", country_id: argentina.id }, + { name: "Formosa", country_id: argentina.id }, + { name: "Neuquén", country_id: argentina.id }, + { name: "Catamarca", country_id: argentina.id }, + { name: "La Rioja", country_id: argentina.id }, + { name: "Chubut", country_id: argentina.id }, + { name: "San Luis", country_id: argentina.id }, + { name: "La Pampa", country_id: argentina.id }, + { name: "Santa Cruz", country_id: argentina.id }, + { name: "Tierra del Fuego", country_id: argentina.id }, + ]; + + const provinceMap: { [key: string]: number } = {}; + + for (const province of provinces) { + const result = await insertIfNotExists("provinces", province, "name"); + provinceMap[province.name] = result.id; + if (result.created) { + console.log(` ✅ "${province.name}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log( + ` ⚠️ "${province.name}" - Already exists (ID: ${result.id})`, + ); + totalExisting++; + } + } + + console.log("\n🏙️ Cities (Buenos Aires Province):"); + const buenosAiresCities = [ + "Ciudad Autónoma de Buenos Aires", + "La Plata", + "Mar del Plata", + "Bahía Blanca", + "San Nicolás", + "Zárate", + "Campana", + "Pilar", + "Tigre", + "San Isidro", + "Vicente López", + "Quilmes", + "Avellaneda", + "Lanús", + "Lomas de Zamora", + "Banfield", + "Adrogué", + "Burzaco", + "Temperley", + "Morón", + "Ituzaingó", + "Merlo", + "Moreno", + "San Miguel", + "Malvinas Argentinas", + ]; + + for (const cityName of buenosAiresCities) { + const result = await insertIfNotExists( + "cities", + { name: cityName, province_id: provinceMap["Buenos Aires"] }, + ["name", "province_id"], + ); + if (result.created) { + console.log(` ✅ "${cityName}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${cityName}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } + + console.log("\n🏙️ Cities (Córdoba Province):"); + const cordobaCities = [ + "Córdoba", + "Villa Carlos Paz", + "Río Cuarto", + "Villa María", + "San Francisco", + ]; + + for (const cityName of cordobaCities) { + const result = await insertIfNotExists( + "cities", + { name: cityName, province_id: provinceMap.Córdoba }, + ["name", "province_id"], + ); + if (result.created) { + console.log(` ✅ "${cityName}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${cityName}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } + + console.log("\n🏙️ Cities (Santa Fe Province):"); + const santaFeCities = ["Rosario", "Santa Fe", "Rafaela", "Venado Tuerto"]; + + for (const cityName of santaFeCities) { + const result = await insertIfNotExists( + "cities", + { name: cityName, province_id: provinceMap["Santa Fe"] }, + ["name", "province_id"], + ); + if (result.created) { + console.log(` ✅ "${cityName}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${cityName}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } + + console.log("\n🏙️ Cities (Mendoza Province):"); + const mendozaCities = ["Mendoza", "San Rafael", "Godoy Cruz"]; + + for (const cityName of mendozaCities) { + const result = await insertIfNotExists( + "cities", + { name: cityName, province_id: provinceMap.Mendoza }, + ["name", "province_id"], + ); + if (result.created) { + console.log(` ✅ "${cityName}" - Created (ID: ${result.id})`); + totalCreated++; + } else { + console.log(` ⚠️ "${cityName}" - Already exists (ID: ${result.id})`); + totalExisting++; + } + } + + console.log(`\n📊 Summary:`); + console.log(` ✅ Created: ${totalCreated}`); + console.log(` ⚠️ Already existed: ${totalExisting}`); + console.log(` 📋 Total: ${totalCreated + totalExisting}\n`); + + console.log("✅ Process completed successfully!\n"); + } catch (error: unknown) { + console.error("\n❌ Error seeding geography:"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(errorMessage); + process.exit(1); + } finally { + await PostgresDatabase.disconnect(); + } } seedGeography(); - diff --git a/backend/scripts/seed-roles.ts b/backend/scripts/seed-roles.ts index ca076a3..63a7bf6 100644 --- a/backend/scripts/seed-roles.ts +++ b/backend/scripts/seed-roles.ts @@ -1,83 +1,85 @@ -import 'dotenv/config'; -import { Client } from 'pg'; -import { PostgresDatabase } from '../src/data/postgres/database'; -import { RoleModel } from '../src/data/postgres/models/users/role.model'; -import { get } from 'env-var'; +import "dotenv/config"; + +import { get } from "env-var"; + +import { PostgresDatabase } from "../src/data/postgres/database"; +import { RoleModel } from "../src/data/postgres/models/users/role.model"; const dbConfig = { - host: get('POSTGRES_HOST').default('localhost').asString(), - port: get('POSTGRES_PORT').default(5432).asPortNumber(), - user: get('POSTGRES_USER').required().asString(), - password: get('POSTGRES_PASSWORD').required().asString(), - database: get('POSTGRES_DB').required().asString(), + host: get("POSTGRES_HOST").default("localhost").asString(), + port: get("POSTGRES_PORT").default(5432).asPortNumber(), + user: get("POSTGRES_USER").required().asString(), + password: get("POSTGRES_PASSWORD").required().asString(), + database: get("POSTGRES_DB").required().asString(), }; -const roles = [ - 'admin', - 'agent' -]; +const roles = ["admin", "agent"]; async function seedRoles() { - try { - console.log('🔌 Connecting to database...'); - console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); - console.log(` Database: ${dbConfig.database}`); - console.log(` User: ${dbConfig.user}\n`); + try { + console.log("🔌 Connecting to database..."); + console.log(` Host: ${dbConfig.host}:${dbConfig.port}`); + console.log(` Database: ${dbConfig.database}`); + console.log(` User: ${dbConfig.user}\n`); - await PostgresDatabase.connect({ - dbName: dbConfig.database, - port: dbConfig.port, - host: dbConfig.host, - user: dbConfig.user, - password: dbConfig.password, - }); + await PostgresDatabase.connect({ + dbName: dbConfig.database, + port: dbConfig.port, + host: dbConfig.host, + user: dbConfig.user, + password: dbConfig.password, + }); - console.log('✅ Connection established\n'); + console.log("✅ Connection established\n"); - console.log('📦 Seeding roles in database...\n'); + console.log("📦 Seeding roles in database...\n"); - let createdCount = 0; - let existingCount = 0; + let createdCount = 0; + let existingCount = 0; - for (const roleName of roles) { - try { - const existingRole = await RoleModel.findByName(roleName); - - if (existingRole) { - console.log(` ⚠️ "${roleName}" - Already exists (ID: ${existingRole.id})`); - existingCount++; - } else { - const newRole = await RoleModel.create({ name: roleName }); - console.log(` ✅ "${roleName}" - Created (ID: ${newRole.id})`); - createdCount++; - } - } catch (error: any) { - console.error(` ❌ Error creating "${roleName}": ${error.message}`); - } - } + for (const roleName of roles) { + try { + const existingRole = await RoleModel.findByName(roleName); - console.log(`\n📊 Summary:`); - console.log(` ✅ Created: ${createdCount}`); - console.log(` ⚠️ Already existed: ${existingCount}`); - console.log(` 📋 Total: ${roles.length}\n`); + if (existingRole) { + console.log( + ` ⚠️ "${roleName}" - Already exists (ID: ${existingRole.id})`, + ); + existingCount++; + } else { + const newRole = await RoleModel.create({ name: roleName }); + console.log(` ✅ "${roleName}" - Created (ID: ${newRole.id})`); + createdCount++; + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(` ❌ Error creating "${roleName}": ${errorMessage}`); + } + } - const allRoles = await RoleModel.findAll(); - console.log('📋 Roles in database:'); - allRoles.forEach((role, index) => { - console.log(` ${index + 1}. ${role.name} (ID: ${role.id})`); - }); - console.log(''); + console.log(`\n📊 Summary:`); + console.log(` ✅ Created: ${createdCount}`); + console.log(` ⚠️ Already existed: ${existingCount}`); + console.log(` 📋 Total: ${roles.length}\n`); - console.log('✅ Process completed successfully!\n'); + const allRoles = await RoleModel.findAll(); + console.log("📋 Roles in database:"); + allRoles.forEach((role, index) => { + console.log(` ${index + 1}. ${role.name} (ID: ${role.id})`); + }); + console.log(""); - } catch (error: any) { - console.error('\n❌ Error seeding roles:'); - console.error(error.message); - process.exit(1); - } finally { - await PostgresDatabase.disconnect(); - } + console.log("✅ Process completed successfully!\n"); + } catch (error: unknown) { + console.error("\n❌ Error seeding roles:"); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(errorMessage); + process.exit(1); + } finally { + await PostgresDatabase.disconnect(); + } } seedRoles(); - diff --git a/backend/src/app.ts b/backend/src/app.ts index 8134ef1..b838aec 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,46 +1,42 @@ -import { envs } from './config/envs'; -import { AppRoutes } from './presentation/routes'; -import { Server } from './presentation/server'; -import { PostgresDatabase } from './data/postgres/database'; +import { envs } from "./config/envs"; +import { PostgresDatabase } from "./data/postgres/database"; +import { AppRoutes } from "./presentation/routes"; +import { Server } from "./presentation/server"; - -(async()=> { - await main(); +(async () => { + await main(); })(); - async function main() { - - try { - await PostgresDatabase.connect({ - dbName: envs.POSTGRES_DB, - user: envs.POSTGRES_USER, - password: envs.POSTGRES_PASSWORD, - host: envs.POSTGRES_HOST, - port: envs.POSTGRES_PORT, - }); - - const server = new Server({ - port: envs.PORT, - routes: AppRoutes.routes, - }); - - server.start(); - - process.on('SIGINT', async () => { - console.log('\n🛑 Cerrando aplicación...'); - await server.close(); - process.exit(0); - }); - - process.on('SIGTERM', async () => { - console.log('\n🛑 Cerrando aplicación...'); - await server.close(); - process.exit(0); - }); - - } catch (error) { - console.error('❌ Error al iniciar la aplicación:', error); - process.exit(1); - } -} \ No newline at end of file + try { + await PostgresDatabase.connect({ + dbName: envs.POSTGRES_DB, + user: envs.POSTGRES_USER, + password: envs.POSTGRES_PASSWORD, + host: envs.POSTGRES_HOST, + port: envs.POSTGRES_PORT, + }); + + const server = new Server({ + port: envs.PORT, + routes: AppRoutes.routes, + }); + + server.start(); + + process.on("SIGINT", async () => { + console.log("\n🛑 Cerrando aplicación..."); + await server.close(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + console.log("\n🛑 Cerrando aplicación..."); + await server.close(); + process.exit(0); + }); + } catch (error) { + console.error("❌ Error al iniciar la aplicación:", error); + process.exit(1); + } +} diff --git a/backend/src/config/bcrypt.adaptar.ts b/backend/src/config/bcrypt.adaptar.ts index fa9fd98..da3932b 100644 --- a/backend/src/config/bcrypt.adaptar.ts +++ b/backend/src/config/bcrypt.adaptar.ts @@ -1,15 +1,16 @@ -import { compare, genSalt, hash } from 'bcrypt'; -import { HashAdapter } from '../domain/interfaces/hash.adapter'; +import { compare, genSalt, hash } from "bcrypt"; + +import type { HashAdapter } from "../domain/interfaces/hash.adapter"; export class BcryptAdapter implements HashAdapter { - async hash(password: string): Promise { - const salt = await genSalt(10); - return await hash(password, salt); - } - - async compare(password: string, hash: string): Promise { - return await compare(password, hash); - } + async hash(password: string): Promise { + const salt = await genSalt(10); + return await hash(password, salt); + } + + async compare(password: string, hash: string): Promise { + return await compare(password, hash); + } } -export const bcryptAdapter = new BcryptAdapter(); \ No newline at end of file +export const bcryptAdapter = new BcryptAdapter(); diff --git a/backend/src/config/cloudinary.adapter.ts b/backend/src/config/cloudinary.adapter.ts index a8108b6..c0afa7f 100644 --- a/backend/src/config/cloudinary.adapter.ts +++ b/backend/src/config/cloudinary.adapter.ts @@ -1,123 +1,137 @@ -import { v2 as cloudinary, UploadApiOptions } from 'cloudinary'; -import { envs } from './envs'; -import { FileUploadAdapter } from '../domain/interfaces/file-upload.adapter'; +import { + v2 as cloudinary, + type UploadApiOptions, + type UploadApiResponse, +} from "cloudinary"; + +import type { FileUploadAdapter } from "../domain/interfaces/file-upload.adapter"; +import { envs } from "./envs"; function getCloudinaryConfig() { - if (envs.CLOUDINARY_CLOUD_NAME && envs.CLOUDINARY_API_KEY && envs.CLOUDINARY_API_SECRET) { - return { - cloud_name: envs.CLOUDINARY_CLOUD_NAME, - api_key: envs.CLOUDINARY_API_KEY, - api_secret: envs.CLOUDINARY_API_SECRET - }; - } - - return { - cloud_name: '', - api_key: '', - api_secret: '' - }; + if ( + envs.CLOUDINARY_CLOUD_NAME && + envs.CLOUDINARY_API_KEY && + envs.CLOUDINARY_API_SECRET + ) { + return { + cloud_name: envs.CLOUDINARY_CLOUD_NAME, + api_key: envs.CLOUDINARY_API_KEY, + api_secret: envs.CLOUDINARY_API_SECRET, + }; + } + + return { + cloud_name: "", + api_key: "", + api_secret: "", + }; } const cloudinaryConfig = getCloudinaryConfig(); cloudinary.config({ - cloud_name: cloudinaryConfig.cloud_name, - api_key: cloudinaryConfig.api_key, - api_secret: cloudinaryConfig.api_secret, + cloud_name: cloudinaryConfig.cloud_name, + api_key: cloudinaryConfig.api_key, + api_secret: cloudinaryConfig.api_secret, }); export class CloudinaryAdapter implements FileUploadAdapter { - private validateConfig(): void { - const config = getCloudinaryConfig(); - if (!config.cloud_name || !config.api_key || !config.api_secret) { - throw new Error( - 'Cloudinary credentials are not configured. ' + - 'Please set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET environment variables.' - ); - } - } - - async uploadFile( - file: Buffer | string, - options?: { - folder?: string; - publicId?: string; - resourceType?: 'image' | 'video' | 'raw' | 'auto'; - mimeType?: string; - } - ): Promise { - this.validateConfig(); - - try { - const uploadOptions: Partial = { - resource_type: options?.resourceType || 'auto', - }; - - if (options?.folder) { - uploadOptions.folder = options.folder; - } - - if (options?.resourceType === 'raw' && options?.mimeType === 'application/pdf') { - if (!options.publicId || !options.publicId.endsWith('.pdf')) { - uploadOptions.format = 'pdf'; - } - } - - if (options?.publicId) { - uploadOptions.public_id = options.publicId; - } - - let uploadResult; - - if (typeof file === 'string') { - uploadResult = await cloudinary.uploader.upload(file, uploadOptions); - } else { - const base64 = file.toString('base64'); - const resourceType = options?.resourceType || 'auto'; - - let dataUri: string; - if (resourceType === 'raw' && options?.mimeType === 'application/pdf') { - dataUri = `data:application/pdf;base64,${base64}`; - } else if (resourceType === 'raw' || resourceType === 'auto') { - dataUri = `data:application/octet-stream;base64,${base64}`; - } else { - const mimeType = resourceType === 'image' ? 'image/jpeg' : `application/${resourceType}`; - dataUri = `data:${mimeType};base64,${base64}`; - } - - uploadResult = await cloudinary.uploader.upload(dataUri, uploadOptions); - } - - return uploadResult.secure_url; - } catch (error) { - throw new Error(`Error uploading file to Cloudinary: ${error}`); - } - } - - async deleteFile(identifier: string): Promise { - this.validateConfig(); - - try { - const publicId = this.extractPublicId(identifier); - - const result = await cloudinary.uploader.destroy(publicId); - return result.result === 'ok'; - } catch (error) { - console.error('Error deleting file from Cloudinary:', error); - return false; - } - } - - private extractPublicId(url: string): string { - if (!url.includes('http')) { - return url; - } - - const parts = url.split('/'); - const filename = parts[parts.length - 1]; - return filename.split('.')[0]; - } + private validateConfig(): void { + const config = getCloudinaryConfig(); + if (!config.cloud_name || !config.api_key || !config.api_secret) { + throw new Error( + "Cloudinary credentials are not configured. " + + "Please set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET environment variables.", + ); + } + } + + async uploadFile( + file: Buffer | string, + options?: { + folder?: string; + publicId?: string; + resourceType?: "image" | "video" | "raw" | "auto"; + mimeType?: string; + }, + ): Promise { + this.validateConfig(); + + try { + const uploadOptions: Partial = { + resource_type: options?.resourceType || "auto", + }; + + if (options?.folder) { + uploadOptions.folder = options.folder; + } + + if ( + options?.resourceType === "raw" && + options?.mimeType === "application/pdf" + ) { + if (!options.publicId || !options.publicId.endsWith(".pdf")) { + uploadOptions.format = "pdf"; + } + } + + if (options?.publicId) { + uploadOptions.public_id = options.publicId; + } + + let uploadResult: UploadApiResponse; + + if (typeof file === "string") { + uploadResult = await cloudinary.uploader.upload(file, uploadOptions); + } else { + const base64 = file.toString("base64"); + const resourceType = options?.resourceType || "auto"; + + let dataUri: string; + if (resourceType === "raw" && options?.mimeType === "application/pdf") { + dataUri = `data:application/pdf;base64,${base64}`; + } else if (resourceType === "raw" || resourceType === "auto") { + dataUri = `data:application/octet-stream;base64,${base64}`; + } else { + const mimeType = + resourceType === "image" + ? "image/jpeg" + : `application/${resourceType}`; + dataUri = `data:${mimeType};base64,${base64}`; + } + + uploadResult = await cloudinary.uploader.upload(dataUri, uploadOptions); + } + + return uploadResult.secure_url; + } catch (error) { + throw new Error(`Error uploading file to Cloudinary: ${error}`); + } + } + + async deleteFile(identifier: string): Promise { + this.validateConfig(); + + try { + const publicId = this.extractPublicId(identifier); + + const result = await cloudinary.uploader.destroy(publicId); + return result.result === "ok"; + } catch (error) { + console.error("Error deleting file from Cloudinary:", error); + return false; + } + } + + private extractPublicId(url: string): string { + if (!url.includes("http")) { + return url; + } + + const parts = url.split("/"); + const filename = parts[parts.length - 1]; + return filename.split(".")[0]; + } } export const cloudinaryAdapter = new CloudinaryAdapter(); - diff --git a/backend/src/config/envs.ts b/backend/src/config/envs.ts index 58dc3e4..e0ab457 100644 --- a/backend/src/config/envs.ts +++ b/backend/src/config/envs.ts @@ -1,29 +1,28 @@ -import 'dotenv/config'; -import { get } from 'env-var'; +import "dotenv/config"; +import { get } from "env-var"; export const envs = { - - PORT: get('PORT').required().asPortNumber(), - - POSTGRES_DB: get('POSTGRES_DB').required().asString(), - POSTGRES_USER: get('POSTGRES_USER').required().asString(), - POSTGRES_PASSWORD: get('POSTGRES_PASSWORD').required().asString(), - POSTGRES_HOST: get('POSTGRES_HOST').default('localhost').asString(), - POSTGRES_PORT: get('POSTGRES_PORT').default(5432).asPortNumber(), - - DB_POOL_MAX: get('DB_POOL_MAX').default(20).asIntPositive(), - DB_POOL_MIN: get('DB_POOL_MIN').default(5).asIntPositive(), - DB_POOL_IDLE_TIMEOUT: get('DB_POOL_IDLE_TIMEOUT').default(30000).asIntPositive(), - DB_POOL_CONNECTION_TIMEOUT: get('DB_POOL_CONNECTION_TIMEOUT').default(2000).asIntPositive(), - - JWT_SECRET: get('JWT_SECRET').required().asString(), - - CLOUDINARY_CLOUD_NAME: get('CLOUDINARY_CLOUD_NAME').asString(), - CLOUDINARY_API_KEY: get('CLOUDINARY_API_KEY').asString(), - CLOUDINARY_API_SECRET: get('CLOUDINARY_API_SECRET').asString(), - -} - - - + PORT: get("PORT").required().asPortNumber(), + + POSTGRES_DB: get("POSTGRES_DB").required().asString(), + POSTGRES_USER: get("POSTGRES_USER").required().asString(), + POSTGRES_PASSWORD: get("POSTGRES_PASSWORD").required().asString(), + POSTGRES_HOST: get("POSTGRES_HOST").default("localhost").asString(), + POSTGRES_PORT: get("POSTGRES_PORT").default(5432).asPortNumber(), + + DB_POOL_MAX: get("DB_POOL_MAX").default(20).asIntPositive(), + DB_POOL_MIN: get("DB_POOL_MIN").default(5).asIntPositive(), + DB_POOL_IDLE_TIMEOUT: get("DB_POOL_IDLE_TIMEOUT") + .default(30000) + .asIntPositive(), + DB_POOL_CONNECTION_TIMEOUT: get("DB_POOL_CONNECTION_TIMEOUT") + .default(2000) + .asIntPositive(), + + JWT_SECRET: get("JWT_SECRET").required().asString(), + + CLOUDINARY_CLOUD_NAME: get("CLOUDINARY_CLOUD_NAME").asString(), + CLOUDINARY_API_KEY: get("CLOUDINARY_API_KEY").asString(), + CLOUDINARY_API_SECRET: get("CLOUDINARY_API_SECRET").asString(), +}; diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index d7e2dae..b41aaf2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,7 +1,5 @@ - - -export * from './bcrypt.adaptar'; -export * from './envs'; -export * from './regular-exp'; -export * from './jwt.adaptar'; -export * from './cloudinary.adapter'; +export * from "./bcrypt.adaptar"; +export * from "./cloudinary.adapter"; +export * from "./envs"; +export * from "./jwt.adaptar"; +export * from "./regular-exp"; diff --git a/backend/src/config/jwt.adaptar.ts b/backend/src/config/jwt.adaptar.ts index e5ba13d..2270542 100644 --- a/backend/src/config/jwt.adaptar.ts +++ b/backend/src/config/jwt.adaptar.ts @@ -1,54 +1,54 @@ -import jwt from 'jsonwebtoken'; -import { v4 as uuidv4 } from 'uuid'; -import { envs } from "./envs"; -import { JwtAdapter } from '../domain/interfaces/jwt.adapter'; -import type { JwtPayload } from '../domain/interfaces/jwt-payload'; +import jwt from "jsonwebtoken"; +import { v4 as uuidv4 } from "uuid"; +import type { JwtAdapter } from "../domain/interfaces/jwt.adapter"; +import type { JwtPayload } from "../domain/interfaces/jwt-payload"; +import { envs } from "./envs"; const secret = envs.JWT_SECRET as string; export class JwtAdapterImpl implements JwtAdapter { - async generateToken(payload: JwtPayload, duration: string = '24h'): Promise { - return new Promise((resolve, reject) => { - const jti = uuidv4(); - - // @ts-ignore - jsonwebtoken types issue with expiresIn - jwt.sign( - { - ...payload, - jti - }, - secret, - { expiresIn: duration }, - (err, token) => { - if (err) reject(err); - else resolve(token!); - } - ); - }); - } + async generateToken( + payload: JwtPayload, + duration: string = "24h", + ): Promise { + return new Promise((resolve, reject) => { + const jti = uuidv4(); + + // @ts-expect-error - jsonwebtoken types issue with expiresIn + jwt.sign( + { + ...payload, + jti, + }, + secret, + { expiresIn: duration }, + (err, token) => { + if (err) reject(err); + else if (token) resolve(token); + else reject(new Error("Token creation failed")); + }, + ); + }); + } - async verifyToken(token: string): Promise { - return new Promise((resolve, reject) => { - jwt.verify( - token, - secret, - (err, decoded) => { - if (err) reject(err); - else resolve(decoded as JwtPayload); - } - ); - }); - } + async verifyToken(token: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify(token, secret, (err, decoded) => { + if (err) reject(err); + else resolve(decoded as JwtPayload); + }); + }); + } - async validateToken(token: string): Promise { - try { - const decoded = await this.verifyToken(token); - return decoded; - } catch (error) { - return null; - } - } + async validateToken(token: string): Promise { + try { + const decoded = await this.verifyToken(token); + return decoded; + } catch { + return null; + } + } } -export const jwtAdapter = new JwtAdapterImpl(); \ No newline at end of file +export const jwtAdapter = new JwtAdapterImpl(); diff --git a/backend/src/config/regular-exp.ts b/backend/src/config/regular-exp.ts index a88c0d9..5158471 100644 --- a/backend/src/config/regular-exp.ts +++ b/backend/src/config/regular-exp.ts @@ -1,8 +1,8 @@ - export const regularExps = { - email: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/, - - phone: /^[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,9}$/, - - whatsapp: /^[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,9}$/, -} \ No newline at end of file + email: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/, + + phone: /^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,9}$/, + + whatsapp: + /^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,9}$/, +}; diff --git a/backend/src/config/swagger.ts b/backend/src/config/swagger.ts index bb6a7d9..c4d10c4 100644 --- a/backend/src/config/swagger.ts +++ b/backend/src/config/swagger.ts @@ -1,151 +1,160 @@ +import type { Request } from "express"; import swaggerJSDoc from "swagger-jsdoc"; -import { Request } from "express"; -import { envs } from "./envs"; +import { envs } from "./envs"; function detectServerUrl(req: Request): string { - const protocol = req.get('x-forwarded-proto') || req.protocol || 'http'; - - const host = req.get('x-forwarded-host') || req.get('host') || `localhost:${envs.PORT}`; - - return `${protocol}://${host}`; -} + const protocol = req.get("x-forwarded-proto") || req.protocol || "http"; + const host = + req.get("x-forwarded-host") || req.get("host") || `localhost:${envs.PORT}`; + + return `${protocol}://${host}`; +} function generateSwaggerSpec(req?: Request) { - const baseOptions: swaggerJSDoc.Options = { - definition: { - openapi: "3.0.0", - info: { - title: "RedProp API", - version: "0.0.1", - description: "API documentation for Redprop", - }, - tags: [ - { - name: "Auth", - description: "Authentication endpoints", - }, - { - name: "Users", - description: "User management endpoints", - }, - { - name: "Properties", - description: "Property management endpoints", - }, - { - name: "Clients", - description: "Client management endpoints (tenants, owners, leads)", - }, - { - name: "Consultations", - description: "Property consultation endpoints (public access)", - }, - ], - components: { - securitySchemes: { - bearerAuth: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", - description: "Enter JWT token obtained from /api/auth/login", - }, - }, - }, - servers: [ - { - url: "http://localhost:3000", - description: "Local development server", - }, - { - url: "https://realestate-app-zdmk.onrender.com", - description: "Production server", - }, - ], - }, - apis: ["./src/presentation/**/routes.ts"], // Path to the API docs - }; + const baseOptions: swaggerJSDoc.Options = { + definition: { + openapi: "3.0.0", + info: { + title: "RedProp API", + version: "0.0.1", + description: "API documentation for Redprop", + }, + tags: [ + { + name: "Auth", + description: "Authentication endpoints", + }, + { + name: "Users", + description: "User management endpoints", + }, + { + name: "Properties", + description: "Property management endpoints", + }, + { + name: "Clients", + description: "Client management endpoints (tenants, owners, leads)", + }, + { + name: "Consultations", + description: "Property consultation endpoints (public access)", + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: "Enter JWT token obtained from /api/auth/login", + }, + }, + }, + servers: [ + { + url: "http://localhost:3000", + description: "Local development server", + }, + { + url: "https://realestate-app-zdmk.onrender.com", + description: "Production server", + }, + ], + }, + apis: ["./src/presentation/**/routes.ts"], // Path to the API docs + }; + + if (req && baseOptions.definition) { + const detectedUrl = detectServerUrl(req); + const isLocal = + detectedUrl.includes("localhost") || detectedUrl.includes("127.0.0.1"); - if (req && baseOptions.definition) { - const detectedUrl = detectServerUrl(req); - const isLocal = detectedUrl.includes('localhost') || detectedUrl.includes('127.0.0.1'); - - const existingUrls = baseOptions.definition.servers?.map((s: { url: string }) => s.url) || []; - if (!existingUrls.includes(detectedUrl)) { - if (baseOptions.definition.servers) { - baseOptions.definition.servers.unshift({ - url: detectedUrl, - description: isLocal ? "Current server (Local)" : "Current server (Production)", - }); - } - } else { - if (baseOptions.definition.servers) { - const serverIndex = baseOptions.definition.servers.findIndex((s: { url: string }) => s.url === detectedUrl); - if (serverIndex > 0) { - const server = baseOptions.definition.servers.splice(serverIndex, 1)[0]; - baseOptions.definition.servers.unshift(server); - } - } - } - } + const existingUrls = + baseOptions.definition.servers?.map((s: { url: string }) => s.url) || []; + if (!existingUrls.includes(detectedUrl)) { + if (baseOptions.definition.servers) { + baseOptions.definition.servers.unshift({ + url: detectedUrl, + description: isLocal + ? "Current server (Local)" + : "Current server (Production)", + }); + } + } else { + if (baseOptions.definition.servers) { + const serverIndex = baseOptions.definition.servers.findIndex( + (s: { url: string }) => s.url === detectedUrl, + ); + if (serverIndex > 0) { + const server = baseOptions.definition.servers.splice( + serverIndex, + 1, + )[0]; + baseOptions.definition.servers.unshift(server); + } + } + } + } - return swaggerJSDoc(baseOptions); + return swaggerJSDoc(baseOptions); } const options: swaggerJSDoc.Options = { - definition: { - openapi: "3.0.0", - info: { - title: "RedProp API", - version: "0.0.1", - description: "API documentation for Redprop", - }, - tags: [ - { - name: "Auth", - description: "Authentication endpoints", - }, - { - name: "Users", - description: "User management endpoints", - }, - { - name: "Properties", - description: "Property management endpoints", - }, - { - name: "Clients", - description: "Client management endpoints (tenants, owners, leads)", - }, - { - name: "Consultations", - description: "Property consultation endpoints (public access)", - }, - ], - components: { - securitySchemes: { - bearerAuth: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", - description: "Enter JWT token obtained from /api/auth/login", - }, - }, - }, - servers: [ - { - url: "http://localhost:3000", - description: "Local development server", - }, - { - url: "https://realestate-app-zdmk.onrender.com", - description: "Production server", - }, - ], - }, - apis: ["./src/presentation/**/routes.ts"], - }; + definition: { + openapi: "3.0.0", + info: { + title: "RedProp API", + version: "0.0.1", + description: "API documentation for Redprop", + }, + tags: [ + { + name: "Auth", + description: "Authentication endpoints", + }, + { + name: "Users", + description: "User management endpoints", + }, + { + name: "Properties", + description: "Property management endpoints", + }, + { + name: "Clients", + description: "Client management endpoints (tenants, owners, leads)", + }, + { + name: "Consultations", + description: "Property consultation endpoints (public access)", + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: "Enter JWT token obtained from /api/auth/login", + }, + }, + }, + servers: [ + { + url: "http://localhost:3000", + description: "Local development server", + }, + { + url: "https://realestate-app-zdmk.onrender.com", + description: "Production server", + }, + ], + }, + apis: ["./src/presentation/**/routes.ts"], +}; export const swaggerSpec = swaggerJSDoc(options); export { generateSwaggerSpec }; diff --git a/backend/src/data/index.ts b/backend/src/data/index.ts index a9bca7c..ba7a7bc 100644 --- a/backend/src/data/index.ts +++ b/backend/src/data/index.ts @@ -1,4 +1 @@ - - - -export * from './postgres/database'; \ No newline at end of file +export * from "./postgres/database"; diff --git a/backend/src/data/postgres/database.ts b/backend/src/data/postgres/database.ts index 7206d26..13ff96c 100644 --- a/backend/src/data/postgres/database.ts +++ b/backend/src/data/postgres/database.ts @@ -1,84 +1,99 @@ -import { Pool, PoolClient, QueryResult, QueryResultRow } from 'pg'; -import { envs } from '../../config/envs'; -import { SqlParams } from '../types/sql.types'; +import { + Pool, + type PoolClient, + type QueryResult, + type QueryResultRow, +} from "pg"; + +import { envs } from "../../config/envs"; +import type { SqlParams } from "../types/sql.types"; interface Options { - dbName: string; - port: number; - host: string; - user: string; - password: string; + dbName: string; + port: number; + host: string; + user: string; + password: string; } export class PostgresDatabase { - private static pool: Pool | null = null; + private static pool: Pool | null = null; - static async connect(options: Options): Promise { - const { dbName, port, host, user, password } = options; - - this.pool = new Pool({ - database: dbName, - port: port, - host: host, - user: user, - password: password, - max: envs.DB_POOL_MAX, - min: envs.DB_POOL_MIN, - idleTimeoutMillis: envs.DB_POOL_IDLE_TIMEOUT, - connectionTimeoutMillis: envs.DB_POOL_CONNECTION_TIMEOUT, - allowExitOnIdle: false, - }); + static async connect(options: Options): Promise { + const { dbName, port, host, user, password } = options; - this.pool.on('error', (err) => { - console.error('❌ Error inesperado en cliente inactivo del pool:', err); - }); + PostgresDatabase.pool = new Pool({ + database: dbName, + port: port, + host: host, + user: user, + password: password, + max: envs.DB_POOL_MAX, + min: envs.DB_POOL_MIN, + idleTimeoutMillis: envs.DB_POOL_IDLE_TIMEOUT, + connectionTimeoutMillis: envs.DB_POOL_CONNECTION_TIMEOUT, + allowExitOnIdle: false, + }); - try { - const testClient = await this.pool.connect(); - testClient.release(); - console.log(`✅ Connection pool establecido correctamente (max: ${envs.DB_POOL_MAX}, min: ${envs.DB_POOL_MIN})`); - } catch (error) { - console.error('❌ Error al establecer connection pool:', error); - if (this.pool) { - await this.pool.end(); - this.pool = null; - } - throw error; - } - } + PostgresDatabase.pool.on("error", (err) => { + console.error("❌ Error inesperado en cliente inactivo del pool:", err); + }); - static async disconnect(): Promise { - if (this.pool) { - await this.pool.end(); - this.pool = null; - console.log('🔌 Connection pool cerrado'); - } - } + try { + const testClient = await PostgresDatabase.pool.connect(); + testClient.release(); + console.log( + `✅ Connection pool establecido correctamente (max: ${envs.DB_POOL_MAX}, min: ${envs.DB_POOL_MIN})`, + ); + } catch (error) { + console.error("❌ Error al establecer connection pool:", error); + if (PostgresDatabase.pool) { + await PostgresDatabase.pool.end(); + PostgresDatabase.pool = null; + } + throw error; + } + } - static getPool(): Pool { - if (!this.pool) { - throw new Error('No hay pool activo a PostgreSQL. Debes llamar a connect() primero.'); - } - return this.pool; - } + static async disconnect(): Promise { + if (PostgresDatabase.pool) { + await PostgresDatabase.pool.end(); + PostgresDatabase.pool = null; + console.log("🔌 Connection pool cerrado"); + } + } - static async query(text: string, params?: SqlParams): Promise> { - if (!this.pool) { - throw new Error('No hay pool activo a PostgreSQL. Debes llamar a connect() primero.'); - } - return await this.pool.query(text, params); - } + static getPool(): Pool { + if (!PostgresDatabase.pool) { + throw new Error( + "No hay pool activo a PostgreSQL. Debes llamar a connect() primero.", + ); + } + return PostgresDatabase.pool; + } - - static async getPoolClient(): Promise { - if (!this.pool) { - throw new Error('No hay pool activo a PostgreSQL. Debes llamar a connect() primero.'); - } - return await this.pool.connect(); - } + static async query( + text: string, + params?: SqlParams, + ): Promise> { + if (!PostgresDatabase.pool) { + throw new Error( + "No hay pool activo a PostgreSQL. Debes llamar a connect() primero.", + ); + } + return await PostgresDatabase.pool.query(text, params); + } - static getClient(): Pool { - return this.getPool(); - } -} + static async getPoolClient(): Promise { + if (!PostgresDatabase.pool) { + throw new Error( + "No hay pool activo a PostgreSQL. Debes llamar a connect() primero.", + ); + } + return await PostgresDatabase.pool.connect(); + } + static getClient(): Pool { + return PostgresDatabase.getPool(); + } +} diff --git a/backend/src/data/postgres/models/clients/client-property-interest.model.ts b/backend/src/data/postgres/models/clients/client-property-interest.model.ts index 2007d81..825139f 100644 --- a/backend/src/data/postgres/models/clients/client-property-interest.model.ts +++ b/backend/src/data/postgres/models/clients/client-property-interest.model.ts @@ -1,93 +1,107 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface ClientPropertyInterest { - id?: number; - client_id: number; - property_id: number; - created_at?: Date; - notes?: string; + id?: number; + client_id: number; + property_id: number; + created_at?: Date; + notes?: string; } export interface CreateClientPropertyInterestDto { - client_id: number; - property_id: number; - notes?: string; + client_id: number; + property_id: number; + notes?: string; } export class ClientPropertyInterestModel { - private static readonly TABLE_NAME = 'client_property_interests'; + private static readonly TABLE_NAME = "client_property_interests"; - static async create(interestData: CreateClientPropertyInterestDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create( + interestData: CreateClientPropertyInterestDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${ClientPropertyInterestModel.TABLE_NAME} ( client_id, property_id, notes ) VALUES ($1, $2, $3) ON CONFLICT (client_id, property_id) DO NOTHING RETURNING * `; - - const result = await client.query(query, [ - interestData.client_id, - interestData.property_id, - interestData.notes || null, - ]); - - if (result.rows.length === 0) { - return await this.findByClientAndProperty(interestData.client_id, interestData.property_id) || { - client_id: interestData.client_id, - property_id: interestData.property_id, - notes: interestData.notes, - }; - } - return result.rows[0]; - } + const result = await client.query(query, [ + interestData.client_id, + interestData.property_id, + interestData.notes || null, + ]); - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } + if (result.rows.length === 0) { + return ( + (await ClientPropertyInterestModel.findByClientAndProperty( + interestData.client_id, + interestData.property_id, + )) || { + client_id: interestData.client_id, + property_id: interestData.property_id, + notes: interestData.notes, + } + ); + } - static async findByClientId(clientId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE client_id = $1 ORDER BY created_at DESC`; - const result = await client.query(query, [clientId]); - return result.rows; - } + return result.rows[0]; + } - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY created_at DESC`; - const result = await client.query(query, [propertyId]); - return result.rows; - } + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientPropertyInterestModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } - static async findByClientAndProperty(clientId: number, propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE client_id = $1 AND property_id = $2`; - const result = await client.query(query, [clientId, propertyId]); - return result.rows[0] || null; - } + static async findByClientId( + clientId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientPropertyInterestModel.TABLE_NAME} WHERE client_id = $1 ORDER BY created_at DESC`; + const result = await client.query(query, [clientId]); + return result.rows; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } - - static async deleteByClientAndProperty(clientId: number, propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE client_id = $1 AND property_id = $2`; - const result = await client.query(query, [clientId, propertyId]); - return (result.rowCount ?? 0) > 0; - } -} + static async findByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientPropertyInterestModel.TABLE_NAME} WHERE property_id = $1 ORDER BY created_at DESC`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + static async findByClientAndProperty( + clientId: number, + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientPropertyInterestModel.TABLE_NAME} WHERE client_id = $1 AND property_id = $2`; + const result = await client.query(query, [clientId, propertyId]); + return result.rows[0] || null; + } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ClientPropertyInterestModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } + static async deleteByClientAndProperty( + clientId: number, + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ClientPropertyInterestModel.TABLE_NAME} WHERE client_id = $1 AND property_id = $2`; + const result = await client.query(query, [clientId, propertyId]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/clients/client.model.ts b/backend/src/data/postgres/models/clients/client.model.ts index c68ed24..fdec7b4 100644 --- a/backend/src/data/postgres/models/clients/client.model.ts +++ b/backend/src/data/postgres/models/clients/client.model.ts @@ -1,51 +1,51 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface Client { - id?: number; - first_name: string; - last_name: string; - email?: string; - dni?: string; - phone: string; - property_interest_phone?: string; - address?: string; - notes?: string; - registered_at?: Date; - deleted?: boolean; - contact_category_id: number; - interest_zone?: string; - purchase_interest?: boolean; - rental_interest?: boolean; - property_search_type_id?: number; - city_id?: number; + id?: number; + first_name: string; + last_name: string; + email?: string; + dni?: string; + phone: string; + property_interest_phone?: string; + address?: string; + notes?: string; + registered_at?: Date; + deleted?: boolean; + contact_category_id: number; + interest_zone?: string; + purchase_interest?: boolean; + rental_interest?: boolean; + property_search_type_id?: number; + city_id?: number; } export interface CreateClientDto { - first_name: string; - last_name: string; - email?: string; - dni?: string; - phone: string; - property_interest_phone?: string; - address?: string; - notes?: string; - contact_category_id: number; - interest_zone?: string; - purchase_interest?: boolean; - rental_interest?: boolean; - property_search_type_id?: number; - city_id?: number; + first_name: string; + last_name: string; + email?: string; + dni?: string; + phone: string; + property_interest_phone?: string; + address?: string; + notes?: string; + contact_category_id: number; + interest_zone?: string; + purchase_interest?: boolean; + rental_interest?: boolean; + property_search_type_id?: number; + city_id?: number; } export class ClientModel { - private static readonly TABLE_NAME = 'clients'; + private static readonly TABLE_NAME = "clients"; - static async create(clientData: CreateClientDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create(clientData: CreateClientDto): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${ClientModel.TABLE_NAME} ( first_name, last_name, email, dni, phone, property_interest_phone, address, notes, contact_category_id, interest_zone, purchase_interest, rental_interest, property_search_type_id, city_id, @@ -54,214 +54,220 @@ export class ClientModel { VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, CURRENT_TIMESTAMP) RETURNING * `; - - const values = [ - clientData.first_name, - clientData.last_name, - clientData.email || null, - clientData.dni || null, - clientData.phone, - clientData.property_interest_phone || null, - clientData.address || null, - clientData.notes || null, - clientData.contact_category_id, - clientData.interest_zone || null, - clientData.purchase_interest !== undefined ? clientData.purchase_interest : false, - clientData.rental_interest !== undefined ? clientData.rental_interest : false, - clientData.property_search_type_id || null, - clientData.city_id || null, - ]; - const result = await client.query(query, values); - return result.rows[0]; - } + const values = [ + clientData.first_name, + clientData.last_name, + clientData.email || null, + clientData.dni || null, + clientData.phone, + clientData.property_interest_phone || null, + clientData.address || null, + clientData.notes || null, + clientData.contact_category_id, + clientData.interest_zone || null, + clientData.purchase_interest !== undefined + ? clientData.purchase_interest + : false, + clientData.rental_interest !== undefined + ? clientData.rental_interest + : false, + clientData.property_search_type_id || null, + clientData.city_id || null, + ]; + + const result = await client.query(query, values); + return result.rows[0]; + } - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1 AND deleted = false`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientModel.TABLE_NAME} WHERE id = $1 AND deleted = false`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } - static async findByEmail(email: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE email = $1 AND deleted = false`; - const result = await client.query(query, [email]); - return result.rows[0] || null; - } + static async findByEmail(email: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientModel.TABLE_NAME} WHERE email = $1 AND deleted = false`; + const result = await client.query(query, [email]); + return result.rows[0] || null; + } - static async findByDni(dni: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE dni = $1 AND deleted = false`; - const result = await client.query(query, [dni]); - return result.rows[0] || null; - } + static async findByDni(dni: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientModel.TABLE_NAME} WHERE dni = $1 AND deleted = false`; + const result = await client.query(query, [dni]); + return result.rows[0] || null; + } - static async findByPhone(phone: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE phone = $1 AND deleted = false`; - const result = await client.query(query, [phone]); - return result.rows; - } + static async findByPhone(phone: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientModel.TABLE_NAME} WHERE phone = $1 AND deleted = false`; + const result = await client.query(query, [phone]); + return result.rows; + } - static async findAll(filters?: { - contact_category_id?: number; - purchase_interest?: boolean; - rental_interest?: boolean; - city_id?: number; - limit?: number; - offset?: number; - }): Promise { - const dbClient = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME} WHERE deleted = false`; - const values: SqlParams = []; - let paramIndex = 1; + static async findAll(filters?: { + contact_category_id?: number; + purchase_interest?: boolean; + rental_interest?: boolean; + city_id?: number; + limit?: number; + offset?: number; + }): Promise { + const dbClient = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${ClientModel.TABLE_NAME} WHERE deleted = false`; + const values: SqlParams = []; + let paramIndex = 1; - if (filters) { - if (filters.contact_category_id !== undefined) { - query += ` AND contact_category_id = $${paramIndex++}`; - values.push(filters.contact_category_id); - } - if (filters.purchase_interest !== undefined) { - query += ` AND purchase_interest = $${paramIndex++}`; - values.push(filters.purchase_interest); - } - if (filters.rental_interest !== undefined) { - query += ` AND rental_interest = $${paramIndex++}`; - values.push(filters.rental_interest); - } - if (filters.city_id !== undefined) { - query += ` AND city_id = $${paramIndex++}`; - values.push(filters.city_id); - } - } + if (filters) { + if (filters.contact_category_id !== undefined) { + query += ` AND contact_category_id = $${paramIndex++}`; + values.push(filters.contact_category_id); + } + if (filters.purchase_interest !== undefined) { + query += ` AND purchase_interest = $${paramIndex++}`; + values.push(filters.purchase_interest); + } + if (filters.rental_interest !== undefined) { + query += ` AND rental_interest = $${paramIndex++}`; + values.push(filters.rental_interest); + } + if (filters.city_id !== undefined) { + query += ` AND city_id = $${paramIndex++}`; + values.push(filters.city_id); + } + } - query += ` ORDER BY registered_at DESC`; + query += ` ORDER BY registered_at DESC`; - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } - const result = await dbClient.query(query, values); - return result.rows; - } + const result = await dbClient.query(query, values); + return result.rows; + } - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); - if (updateData.first_name) { - fields.push(`first_name = $${paramIndex++}`); - values.push(updateData.first_name); - } - if (updateData.last_name) { - fields.push(`last_name = $${paramIndex++}`); - values.push(updateData.last_name); - } - if (updateData.email !== undefined) { - fields.push(`email = $${paramIndex++}`); - values.push(updateData.email || null); - } - if (updateData.dni !== undefined) { - fields.push(`dni = $${paramIndex++}`); - values.push(updateData.dni || null); - } - if (updateData.phone) { - fields.push(`phone = $${paramIndex++}`); - values.push(updateData.phone); - } - if (updateData.property_interest_phone !== undefined) { - fields.push(`property_interest_phone = $${paramIndex++}`); - values.push(updateData.property_interest_phone || null); - } - if (updateData.address !== undefined) { - fields.push(`address = $${paramIndex++}`); - values.push(updateData.address || null); - } - if (updateData.notes !== undefined) { - fields.push(`notes = $${paramIndex++}`); - values.push(updateData.notes || null); - } - if (updateData.contact_category_id !== undefined) { - fields.push(`contact_category_id = $${paramIndex++}`); - values.push(updateData.contact_category_id); - } - if (updateData.interest_zone !== undefined) { - fields.push(`interest_zone = $${paramIndex++}`); - values.push(updateData.interest_zone || null); - } - if (updateData.purchase_interest !== undefined) { - fields.push(`purchase_interest = $${paramIndex++}`); - values.push(updateData.purchase_interest); - } - if (updateData.rental_interest !== undefined) { - fields.push(`rental_interest = $${paramIndex++}`); - values.push(updateData.rental_interest); - } - if (updateData.property_search_type_id !== undefined) { - fields.push(`property_search_type_id = $${paramIndex++}`); - values.push(updateData.property_search_type_id || null); - } - if (updateData.city_id !== undefined) { - fields.push(`city_id = $${paramIndex++}`); - values.push(updateData.city_id || null); - } + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; - if (fields.length === 0) { - return await this.findById(id); - } + if (updateData.first_name) { + fields.push(`first_name = $${paramIndex++}`); + values.push(updateData.first_name); + } + if (updateData.last_name) { + fields.push(`last_name = $${paramIndex++}`); + values.push(updateData.last_name); + } + if (updateData.email !== undefined) { + fields.push(`email = $${paramIndex++}`); + values.push(updateData.email || null); + } + if (updateData.dni !== undefined) { + fields.push(`dni = $${paramIndex++}`); + values.push(updateData.dni || null); + } + if (updateData.phone) { + fields.push(`phone = $${paramIndex++}`); + values.push(updateData.phone); + } + if (updateData.property_interest_phone !== undefined) { + fields.push(`property_interest_phone = $${paramIndex++}`); + values.push(updateData.property_interest_phone || null); + } + if (updateData.address !== undefined) { + fields.push(`address = $${paramIndex++}`); + values.push(updateData.address || null); + } + if (updateData.notes !== undefined) { + fields.push(`notes = $${paramIndex++}`); + values.push(updateData.notes || null); + } + if (updateData.contact_category_id !== undefined) { + fields.push(`contact_category_id = $${paramIndex++}`); + values.push(updateData.contact_category_id); + } + if (updateData.interest_zone !== undefined) { + fields.push(`interest_zone = $${paramIndex++}`); + values.push(updateData.interest_zone || null); + } + if (updateData.purchase_interest !== undefined) { + fields.push(`purchase_interest = $${paramIndex++}`); + values.push(updateData.purchase_interest); + } + if (updateData.rental_interest !== undefined) { + fields.push(`rental_interest = $${paramIndex++}`); + values.push(updateData.rental_interest); + } + if (updateData.property_search_type_id !== undefined) { + fields.push(`property_search_type_id = $${paramIndex++}`); + values.push(updateData.property_search_type_id || null); + } + if (updateData.city_id !== undefined) { + fields.push(`city_id = $${paramIndex++}`); + values.push(updateData.city_id || null); + } - values.push(id); + if (fields.length === 0) { + return await ClientModel.findById(id); + } - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + values.push(id); + + const query = ` + UPDATE ${ClientModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} AND deleted = false RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } - /** - * Soft delete: marca el cliente como eliminado (deleted = true) - * El registro permanece en la base de datos pero no aparece en búsquedas normales - */ - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `UPDATE ${this.TABLE_NAME} SET deleted = true WHERE id = $1 AND deleted = false RETURNING id`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + /** + * Soft delete: marca el cliente como eliminado (deleted = true) + * El registro permanece en la base de datos pero no aparece en búsquedas normales + */ + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `UPDATE ${ClientModel.TABLE_NAME} SET deleted = true WHERE id = $1 AND deleted = false RETURNING id`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } - /** - * Restaura un cliente eliminado lógicamente (soft delete) - */ - static async restore(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `UPDATE ${this.TABLE_NAME} SET deleted = false WHERE id = $1 AND deleted = true RETURNING id`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + /** + * Restaura un cliente eliminado lógicamente (soft delete) + */ + static async restore(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `UPDATE ${ClientModel.TABLE_NAME} SET deleted = false WHERE id = $1 AND deleted = true RETURNING id`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } - /** - * Hard delete: elimina físicamente el registro de la base de datos - * ⚠️ Use con precaución: esta acción no se puede deshacer - */ - static async hardDelete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + /** + * Hard delete: elimina físicamente el registro de la base de datos + * ⚠️ Use con precaución: esta acción no se puede deshacer + */ + static async hardDelete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ClientModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/clients/contact-category.model.ts b/backend/src/data/postgres/models/clients/contact-category.model.ts index 9242e86..d217d0c 100644 --- a/backend/src/data/postgres/models/clients/contact-category.model.ts +++ b/backend/src/data/postgres/models/clients/contact-category.model.ts @@ -1,60 +1,64 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface ContactCategory { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateContactCategoryDto { - name: string; + name: string; } export class ContactCategoryModel { - private static readonly TABLE_NAME = 'contact_categories'; - - static async create(data: CreateContactCategoryDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + private static readonly TABLE_NAME = "contact_categories"; + + static async create( + data: CreateContactCategoryDto, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${ContactCategoryModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ContactCategoryModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ContactCategoryModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ContactCategoryModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await ContactCategoryModel.findById(id); + } + const query = `UPDATE ${ContactCategoryModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ContactCategoryModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/clients/index.ts b/backend/src/data/postgres/models/clients/index.ts index f576363..ba6ff32 100644 --- a/backend/src/data/postgres/models/clients/index.ts +++ b/backend/src/data/postgres/models/clients/index.ts @@ -1,5 +1,4 @@ -export * from './client.model'; -export * from './client-property-interest.model'; -export * from './contact-category.model'; -export * from './property-search-type.model'; - +export * from "./client.model"; +export * from "./client-property-interest.model"; +export * from "./contact-category.model"; +export * from "./property-search-type.model"; diff --git a/backend/src/data/postgres/models/clients/property-search-type.model.ts b/backend/src/data/postgres/models/clients/property-search-type.model.ts index 65bf4af..3e731f8 100644 --- a/backend/src/data/postgres/models/clients/property-search-type.model.ts +++ b/backend/src/data/postgres/models/clients/property-search-type.model.ts @@ -1,60 +1,64 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertySearchType { - id?: number; - name: string; + id?: number; + name: string; } export interface CreatePropertySearchTypeDto { - name: string; + name: string; } export class PropertySearchTypeModel { - private static readonly TABLE_NAME = 'property_search_types'; - - static async create(data: CreatePropertySearchTypeDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + private static readonly TABLE_NAME = "property_search_types"; + + static async create( + data: CreatePropertySearchTypeDto, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${PropertySearchTypeModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertySearchTypeModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertySearchTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertySearchTypeModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await PropertySearchTypeModel.findById(id); + } + const query = `UPDATE ${PropertySearchTypeModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertySearchTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/company/company-settings.model.ts b/backend/src/data/postgres/models/company/company-settings.model.ts index 5de7272..c259f1c 100644 --- a/backend/src/data/postgres/models/company/company-settings.model.ts +++ b/backend/src/data/postgres/models/company/company-settings.model.ts @@ -1,5 +1,5 @@ +import type { SqlParams } from "../../../types/sql.types"; import { PostgresDatabase } from "../../database"; -import { SqlParams } from '../../../types/sql.types'; export interface CompanySettings { id: number; @@ -10,7 +10,6 @@ export interface CompanySettings { } export class CompanySettingsModel { - static async findSettings(): Promise { const client = PostgresDatabase.getClient(); @@ -26,7 +25,6 @@ export class CompanySettingsModel { } } - static async updateLogo( logoUrl: string, updatedByUserId: number, @@ -35,8 +33,8 @@ export class CompanySettingsModel { try { const result = await client.query( - `UPDATE company_settings - SET logo_url = $1, + `UPDATE company_settings + SET logo_url = $1, updated_at = CURRENT_TIMESTAMP, updated_by_user_id = $2 WHERE id = 1 @@ -51,7 +49,6 @@ export class CompanySettingsModel { } } - static async updateSettings( data: { logo_url?: string; @@ -85,7 +82,7 @@ export class CompanySettingsModel { try { const result = await client.query( - `UPDATE company_settings + `UPDATE company_settings SET ${updates.join(", ")} WHERE id = 1 RETURNING *`, diff --git a/backend/src/data/postgres/models/crm/client-consultation.model.ts b/backend/src/data/postgres/models/crm/client-consultation.model.ts index 624ede3..9c0ef89 100644 --- a/backend/src/data/postgres/models/crm/client-consultation.model.ts +++ b/backend/src/data/postgres/models/crm/client-consultation.model.ts @@ -1,9 +1,9 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface ClientConsultation { id?: number; - client_id?: number; + client_id?: number; property_id?: number; consultation_date?: Date; consultation_type_id: number; @@ -21,7 +21,7 @@ export interface ClientConsultation { } export interface CreateClientConsultationDto { - client_id?: number; + client_id?: number; property_id?: number; consultation_type_id: number; assigned_user_id?: number; @@ -31,7 +31,7 @@ export interface CreateClientConsultationDto { responded_by_user_id?: number; response_date?: Date; is_read?: boolean; - + consultant_first_name?: string; consultant_last_name?: string; consultant_phone?: string; @@ -60,7 +60,7 @@ export class ClientConsultationModel { const query = ` INSERT INTO ${ClientConsultationModel.TABLE_NAME} ( - client_id, property_id, consultation_type_id, assigned_user_id, + client_id, property_id, consultation_type_id, assigned_user_id, consultation_date, message, response, responded_by_user_id, response_date, is_read, consultant_first_name, consultant_last_name, consultant_phone, consultant_email ) diff --git a/backend/src/data/postgres/models/crm/consultation-type.model.ts b/backend/src/data/postgres/models/crm/consultation-type.model.ts index ac2fea1..9fafcd2 100644 --- a/backend/src/data/postgres/models/crm/consultation-type.model.ts +++ b/backend/src/data/postgres/models/crm/consultation-type.model.ts @@ -1,60 +1,64 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface ConsultationType { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateConsultationTypeDto { - name: string; + name: string; } export class ConsultationTypeModel { - private static readonly TABLE_NAME = 'consultation_types'; - - static async create(data: CreateConsultationTypeDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + private static readonly TABLE_NAME = "consultation_types"; + + static async create( + data: CreateConsultationTypeDto, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${ConsultationTypeModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ConsultationTypeModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ConsultationTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ConsultationTypeModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await ConsultationTypeModel.findById(id); + } + const query = `UPDATE ${ConsultationTypeModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ConsultationTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/crm/crm-interaction.model.ts b/backend/src/data/postgres/models/crm/crm-interaction.model.ts index 13f7249..2eda488 100644 --- a/backend/src/data/postgres/models/crm/crm-interaction.model.ts +++ b/backend/src/data/postgres/models/crm/crm-interaction.model.ts @@ -1,188 +1,194 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface CrmInteraction { - id?: number; - event_type_id: number; - title: string; - scheduled_datetime: Date; - client_id?: number; - property_id?: number; - responsible_user_id: number; - comments?: string; - status?: string; + id?: number; + event_type_id: number; + title: string; + scheduled_datetime: Date; + client_id?: number; + property_id?: number; + responsible_user_id: number; + comments?: string; + status?: string; } export interface CreateCrmInteractionDto { - event_type_id: number; - title: string; - scheduled_datetime: Date; - client_id?: number; - property_id?: number; - responsible_user_id: number; - comments?: string; - status?: string; + event_type_id: number; + title: string; + scheduled_datetime: Date; + client_id?: number; + property_id?: number; + responsible_user_id: number; + comments?: string; + status?: string; } export interface CrmInteractionFilters { - event_type_id?: number; - client_id?: number; - property_id?: number; - responsible_user_id?: number; - status?: string; - start_datetime?: Date; - end_datetime?: Date; - upcoming?: boolean; - limit?: number; - offset?: number; + event_type_id?: number; + client_id?: number; + property_id?: number; + responsible_user_id?: number; + status?: string; + start_datetime?: Date; + end_datetime?: Date; + upcoming?: boolean; + limit?: number; + offset?: number; } export class CrmInteractionModel { - private static readonly TABLE_NAME = 'crm_interactions'; + private static readonly TABLE_NAME = "crm_interactions"; - static async create(interactionData: CreateCrmInteractionDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create( + interactionData: CreateCrmInteractionDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${CrmInteractionModel.TABLE_NAME} ( event_type_id, title, scheduled_datetime, client_id, property_id, responsible_user_id, comments, status ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * `; - - const result = await client.query(query, [ - interactionData.event_type_id, - interactionData.title, - interactionData.scheduled_datetime, - interactionData.client_id || null, - interactionData.property_id || null, - interactionData.responsible_user_id, - interactionData.comments || null, - interactionData.status || 'Scheduled', - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findAll(filters?: CrmInteractionFilters): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME}`; - const conditions: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (filters) { - if (filters.event_type_id !== undefined) { - conditions.push(`event_type_id = $${paramIndex++}`); - values.push(filters.event_type_id); - } - if (filters.client_id !== undefined) { - conditions.push(`client_id = $${paramIndex++}`); - values.push(filters.client_id); - } - if (filters.property_id !== undefined) { - conditions.push(`property_id = $${paramIndex++}`); - values.push(filters.property_id); - } - if (filters.responsible_user_id !== undefined) { - conditions.push(`responsible_user_id = $${paramIndex++}`); - values.push(filters.responsible_user_id); - } - if (filters.status) { - conditions.push(`status = $${paramIndex++}`); - values.push(filters.status); - } - if (filters.start_datetime) { - conditions.push(`scheduled_datetime >= $${paramIndex++}`); - values.push(filters.start_datetime); - } - if (filters.end_datetime) { - conditions.push(`scheduled_datetime <= $${paramIndex++}`); - values.push(filters.end_datetime); - } - if (filters.upcoming !== undefined) { - if (filters.upcoming) { - conditions.push(`scheduled_datetime >= CURRENT_TIMESTAMP`); - } else { - conditions.push(`scheduled_datetime < CURRENT_TIMESTAMP`); - } - } - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}`; - } - - query += ` ORDER BY scheduled_datetime ASC`; - - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } - - const result = await client.query(query, values); - return result.rows; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.title) { - fields.push(`title = $${paramIndex++}`); - values.push(updateData.title); - } - if (updateData.scheduled_datetime) { - fields.push(`scheduled_datetime = $${paramIndex++}`); - values.push(updateData.scheduled_datetime); - } - if (updateData.comments !== undefined) { - fields.push(`comments = $${paramIndex++}`); - values.push(updateData.comments || null); - } - if (updateData.status !== undefined) { - fields.push(`status = $${paramIndex++}`); - values.push(updateData.status); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + + const result = await client.query(query, [ + interactionData.event_type_id, + interactionData.title, + interactionData.scheduled_datetime, + interactionData.client_id || null, + interactionData.property_id || null, + interactionData.responsible_user_id, + interactionData.comments || null, + interactionData.status || "Scheduled", + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CrmInteractionModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findAll( + filters?: CrmInteractionFilters, + ): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${CrmInteractionModel.TABLE_NAME}`; + const conditions: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (filters) { + if (filters.event_type_id !== undefined) { + conditions.push(`event_type_id = $${paramIndex++}`); + values.push(filters.event_type_id); + } + if (filters.client_id !== undefined) { + conditions.push(`client_id = $${paramIndex++}`); + values.push(filters.client_id); + } + if (filters.property_id !== undefined) { + conditions.push(`property_id = $${paramIndex++}`); + values.push(filters.property_id); + } + if (filters.responsible_user_id !== undefined) { + conditions.push(`responsible_user_id = $${paramIndex++}`); + values.push(filters.responsible_user_id); + } + if (filters.status) { + conditions.push(`status = $${paramIndex++}`); + values.push(filters.status); + } + if (filters.start_datetime) { + conditions.push(`scheduled_datetime >= $${paramIndex++}`); + values.push(filters.start_datetime); + } + if (filters.end_datetime) { + conditions.push(`scheduled_datetime <= $${paramIndex++}`); + values.push(filters.end_datetime); + } + if (filters.upcoming !== undefined) { + if (filters.upcoming) { + conditions.push(`scheduled_datetime >= CURRENT_TIMESTAMP`); + } else { + conditions.push(`scheduled_datetime < CURRENT_TIMESTAMP`); + } + } + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += ` ORDER BY scheduled_datetime ASC`; + + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } + + const result = await client.query(query, values); + return result.rows; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.title) { + fields.push(`title = $${paramIndex++}`); + values.push(updateData.title); + } + if (updateData.scheduled_datetime) { + fields.push(`scheduled_datetime = $${paramIndex++}`); + values.push(updateData.scheduled_datetime); + } + if (updateData.comments !== undefined) { + fields.push(`comments = $${paramIndex++}`); + values.push(updateData.comments || null); + } + if (updateData.status !== undefined) { + fields.push(`status = $${paramIndex++}`); + values.push(updateData.status); + } + + if (fields.length === 0) { + return await CrmInteractionModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${CrmInteractionModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${CrmInteractionModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/crm/event-type.model.ts b/backend/src/data/postgres/models/crm/event-type.model.ts index e7441ab..9332137 100644 --- a/backend/src/data/postgres/models/crm/event-type.model.ts +++ b/backend/src/data/postgres/models/crm/event-type.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface EventType { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateEventTypeDto { - name: string; + name: string; } export class EventTypeModel { - private static readonly TABLE_NAME = 'event_types'; - - static async create(data: CreateEventTypeDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "event_types"; + + static async create(data: CreateEventTypeDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${EventTypeModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${EventTypeModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${EventTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${EventTypeModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await EventTypeModel.findById(id); + } + const query = `UPDATE ${EventTypeModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${EventTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/crm/index.ts b/backend/src/data/postgres/models/crm/index.ts index a0b7e3a..e560088 100644 --- a/backend/src/data/postgres/models/crm/index.ts +++ b/backend/src/data/postgres/models/crm/index.ts @@ -1,5 +1,4 @@ -export * from './client-consultation.model'; -export * from './consultation-type.model'; -export * from './crm-interaction.model'; -export * from './event-type.model'; - +export * from "./client-consultation.model"; +export * from "./consultation-type.model"; +export * from "./crm-interaction.model"; +export * from "./event-type.model"; diff --git a/backend/src/data/postgres/models/geography/city.model.ts b/backend/src/data/postgres/models/geography/city.model.ts index 7dd112e..7ab889a 100644 --- a/backend/src/data/postgres/models/geography/city.model.ts +++ b/backend/src/data/postgres/models/geography/city.model.ts @@ -1,85 +1,90 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface City { - id?: number; - name: string; - province_id: number; + id?: number; + name: string; + province_id: number; } export interface CreateCityDto { - name: string; - province_id: number; + name: string; + province_id: number; } export class CityModel { - private static readonly TABLE_NAME = 'cities'; + private static readonly TABLE_NAME = "cities"; - static async create(data: CreateCityDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name, province_id) VALUES ($1, $2) RETURNING *`; - const result = await client.query(query, [data.name, data.province_id]); - return result.rows[0]; - } + static async create(data: CreateCityDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${CityModel.TABLE_NAME} (name, province_id) VALUES ($1, $2) RETURNING *`; + const result = await client.query(query, [data.name, data.province_id]); + return result.rows[0]; + } - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CityModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CityModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } - static async findByProvinceId(provinceId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE province_id = $1 ORDER BY name`; - const result = await client.query(query, [provinceId]); - return result.rows; - } + static async findByProvinceId(provinceId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CityModel.TABLE_NAME} WHERE province_id = $1 ORDER BY name`; + const result = await client.query(query, [provinceId]); + return result.rows; + } - static async findByNameAndProvince(name: string, provinceId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1 AND province_id = $2`; - const result = await client.query(query, [name, provinceId]); - return result.rows[0] || null; - } + static async findByNameAndProvince( + name: string, + provinceId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CityModel.TABLE_NAME} WHERE name = $1 AND province_id = $2`; + const result = await client.query(query, [name, provinceId]); + return result.rows[0] || null; + } - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; - if (updateData.name) { - fields.push(`name = $${paramIndex++}`); - values.push(updateData.name); - } - if (updateData.province_id !== undefined) { - fields.push(`province_id = $${paramIndex++}`); - values.push(updateData.province_id); - } + if (updateData.name) { + fields.push(`name = $${paramIndex++}`); + values.push(updateData.name); + } + if (updateData.province_id !== undefined) { + fields.push(`province_id = $${paramIndex++}`); + values.push(updateData.province_id); + } - if (fields.length === 0) { - return await this.findById(id); - } + if (fields.length === 0) { + return await CityModel.findById(id); + } - values.push(id); - const query = `UPDATE ${this.TABLE_NAME} SET ${fields.join(', ')} WHERE id = $${paramIndex} RETURNING *`; - const result = await client.query(query, values); - return result.rows[0] || null; - } + values.push(id); + const query = `UPDATE ${CityModel.TABLE_NAME} SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING *`; + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${CityModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/geography/country.model.ts b/backend/src/data/postgres/models/geography/country.model.ts index 34ec2ce..b15c2a5 100644 --- a/backend/src/data/postgres/models/geography/country.model.ts +++ b/backend/src/data/postgres/models/geography/country.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface Country { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateCountryDto { - name: string; + name: string; } export class CountryModel { - private static readonly TABLE_NAME = 'countries'; - - static async create(data: CreateCountryDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "countries"; + + static async create(data: CreateCountryDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${CountryModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CountryModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CountryModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CountryModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await CountryModel.findById(id); + } + const query = `UPDATE ${CountryModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${CountryModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/geography/index.ts b/backend/src/data/postgres/models/geography/index.ts index a742cde..f4e607f 100644 --- a/backend/src/data/postgres/models/geography/index.ts +++ b/backend/src/data/postgres/models/geography/index.ts @@ -1,4 +1,3 @@ -export * from './city.model'; -export * from './country.model'; -export * from './province.model'; - +export * from "./city.model"; +export * from "./country.model"; +export * from "./province.model"; diff --git a/backend/src/data/postgres/models/geography/province.model.ts b/backend/src/data/postgres/models/geography/province.model.ts index 706ce70..1778daa 100644 --- a/backend/src/data/postgres/models/geography/province.model.ts +++ b/backend/src/data/postgres/models/geography/province.model.ts @@ -1,85 +1,87 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface Province { - id?: number; - name: string; - country_id: number; + id?: number; + name: string; + country_id: number; } export interface CreateProvinceDto { - name: string; - country_id: number; + name: string; + country_id: number; } export class ProvinceModel { - private static readonly TABLE_NAME = 'provinces'; + private static readonly TABLE_NAME = "provinces"; - static async create(data: CreateProvinceDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name, country_id) VALUES ($1, $2) RETURNING *`; - const result = await client.query(query, [data.name, data.country_id]); - return result.rows[0]; - } + static async create(data: CreateProvinceDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${ProvinceModel.TABLE_NAME} (name, country_id) VALUES ($1, $2) RETURNING *`; + const result = await client.query(query, [data.name, data.country_id]); + return result.rows[0]; + } - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ProvinceModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ProvinceModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } - static async findByCountryId(countryId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE country_id = $1 ORDER BY name`; - const result = await client.query(query, [countryId]); - return result.rows; - } + static async findByCountryId(countryId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ProvinceModel.TABLE_NAME} WHERE country_id = $1 ORDER BY name`; + const result = await client.query(query, [countryId]); + return result.rows; + } - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ProvinceModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; - if (updateData.name) { - fields.push(`name = $${paramIndex++}`); - values.push(updateData.name); - } - if (updateData.country_id !== undefined) { - fields.push(`country_id = $${paramIndex++}`); - values.push(updateData.country_id); - } + if (updateData.name) { + fields.push(`name = $${paramIndex++}`); + values.push(updateData.name); + } + if (updateData.country_id !== undefined) { + fields.push(`country_id = $${paramIndex++}`); + values.push(updateData.country_id); + } - if (fields.length === 0) { - return await this.findById(id); - } + if (fields.length === 0) { + return await ProvinceModel.findById(id); + } - values.push(id); - const query = `UPDATE ${this.TABLE_NAME} SET ${fields.join(', ')} WHERE id = $${paramIndex} RETURNING *`; - const result = await client.query(query, values); - return result.rows[0] || null; - } + values.push(id); + const query = `UPDATE ${ProvinceModel.TABLE_NAME} SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING *`; + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ProvinceModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/index.ts b/backend/src/data/postgres/models/index.ts index d1f23ae..aff9a7f 100644 --- a/backend/src/data/postgres/models/index.ts +++ b/backend/src/data/postgres/models/index.ts @@ -1,4 +1,3 @@ - export * from "./clients"; export * from "./company"; export * from "./crm"; diff --git a/backend/src/data/postgres/models/payments/currency-type.model.ts b/backend/src/data/postgres/models/payments/currency-type.model.ts index 0d2bb39..be17de4 100644 --- a/backend/src/data/postgres/models/payments/currency-type.model.ts +++ b/backend/src/data/postgres/models/payments/currency-type.model.ts @@ -1,85 +1,87 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface CurrencyType { - id?: number; - name: string; - symbol: string; + id?: number; + name: string; + symbol: string; } export interface CreateCurrencyTypeDto { - name: string; - symbol: string; + name: string; + symbol: string; } export class CurrencyTypeModel { - private static readonly TABLE_NAME = 'currency_types'; + private static readonly TABLE_NAME = "currency_types"; - static async create(data: CreateCurrencyTypeDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name, symbol) VALUES ($1, $2) RETURNING *`; - const result = await client.query(query, [data.name, data.symbol]); - return result.rows[0]; - } + static async create(data: CreateCurrencyTypeDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${CurrencyTypeModel.TABLE_NAME} (name, symbol) VALUES ($1, $2) RETURNING *`; + const result = await client.query(query, [data.name, data.symbol]); + return result.rows[0]; + } - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CurrencyTypeModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CurrencyTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CurrencyTypeModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } - static async findBySymbol(symbol: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE UPPER(symbol) = UPPER($1)`; - const result = await client.query(query, [symbol]); - return result.rows[0] || null; - } + static async findBySymbol(symbol: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CurrencyTypeModel.TABLE_NAME} WHERE UPPER(symbol) = UPPER($1)`; + const result = await client.query(query, [symbol]); + return result.rows[0] || null; + } - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; - if (updateData.name) { - fields.push(`name = $${paramIndex++}`); - values.push(updateData.name); - } - if (updateData.symbol) { - fields.push(`symbol = $${paramIndex++}`); - values.push(updateData.symbol); - } + if (updateData.name) { + fields.push(`name = $${paramIndex++}`); + values.push(updateData.name); + } + if (updateData.symbol) { + fields.push(`symbol = $${paramIndex++}`); + values.push(updateData.symbol); + } - if (fields.length === 0) { - return await this.findById(id); - } + if (fields.length === 0) { + return await CurrencyTypeModel.findById(id); + } - values.push(id); - const query = `UPDATE ${this.TABLE_NAME} SET ${fields.join(', ')} WHERE id = $${paramIndex} RETURNING *`; - const result = await client.query(query, values); - return result.rows[0] || null; - } + values.push(id); + const query = `UPDATE ${CurrencyTypeModel.TABLE_NAME} SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING *`; + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${CurrencyTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/payments/index.ts b/backend/src/data/postgres/models/payments/index.ts index b0d7423..34ed760 100644 --- a/backend/src/data/postgres/models/payments/index.ts +++ b/backend/src/data/postgres/models/payments/index.ts @@ -1,4 +1,3 @@ -export * from './currency-type.model'; -export * from './payment-method.model'; -export * from './payment-status.model'; - +export * from "./currency-type.model"; +export * from "./payment-method.model"; +export * from "./payment-status.model"; diff --git a/backend/src/data/postgres/models/payments/payment-method.model.ts b/backend/src/data/postgres/models/payments/payment-method.model.ts index 6c32913..eea1a01 100644 --- a/backend/src/data/postgres/models/payments/payment-method.model.ts +++ b/backend/src/data/postgres/models/payments/payment-method.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PaymentMethod { - id?: number; - name: string; + id?: number; + name: string; } export interface CreatePaymentMethodDto { - name: string; + name: string; } export class PaymentMethodModel { - private static readonly TABLE_NAME = 'payment_methods'; - - static async create(data: CreatePaymentMethodDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "payment_methods"; + + static async create(data: CreatePaymentMethodDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${PaymentMethodModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PaymentMethodModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PaymentMethodModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PaymentMethodModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await PaymentMethodModel.findById(id); + } + const query = `UPDATE ${PaymentMethodModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PaymentMethodModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/payments/payment-status.model.ts b/backend/src/data/postgres/models/payments/payment-status.model.ts index 4ba161e..9465911 100644 --- a/backend/src/data/postgres/models/payments/payment-status.model.ts +++ b/backend/src/data/postgres/models/payments/payment-status.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PaymentStatus { - id?: number; - name: string; + id?: number; + name: string; } export interface CreatePaymentStatusDto { - name: string; + name: string; } export class PaymentStatusModel { - private static readonly TABLE_NAME = 'payment_statuses'; - - static async create(data: CreatePaymentStatusDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "payment_statuses"; + + static async create(data: CreatePaymentStatusDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${PaymentStatusModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PaymentStatusModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PaymentStatusModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PaymentStatusModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await PaymentStatusModel.findById(id); + } + const query = `UPDATE ${PaymentStatusModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PaymentStatusModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/address.model.ts b/backend/src/data/postgres/models/properties/address.model.ts index bfece11..fd92527 100644 --- a/backend/src/data/postgres/models/properties/address.model.ts +++ b/backend/src/data/postgres/models/properties/address.model.ts @@ -1,178 +1,184 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface Address { - id?: number; - street: string; - number?: string; - full_address: string; - neighborhood?: string; - postal_code?: string; - latitude?: number; - longitude?: number; - city_id: number; + id?: number; + street: string; + number?: string; + full_address: string; + neighborhood?: string; + postal_code?: string; + latitude?: number; + longitude?: number; + city_id: number; } export interface CreateAddressDto { - street: string; - number?: string; - full_address: string; - neighborhood?: string; - postal_code?: string; - latitude?: number; - longitude?: number; - city_id: number; + street: string; + number?: string; + full_address: string; + neighborhood?: string; + postal_code?: string; + latitude?: number; + longitude?: number; + city_id: number; } export class AddressModel { - private static readonly TABLE_NAME = 'addresses'; + private static readonly TABLE_NAME = "addresses"; - static async create(addressData: CreateAddressDto): Promise
{ - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create(addressData: CreateAddressDto): Promise
{ + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${AddressModel.TABLE_NAME} ( street, number, full_address, neighborhood, postal_code, latitude, longitude, city_id ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * `; - - const values = [ - addressData.street, - addressData.number || null, - addressData.full_address, - addressData.neighborhood || null, - addressData.postal_code || null, - addressData.latitude || null, - addressData.longitude || null, - addressData.city_id, - ]; - - const result = await client.query(query, values); - return result.rows[0]; - } - - static async findById(id: number): Promise
{ - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByCityId(cityId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE city_id = $1 ORDER BY full_address`; - const result = await client.query(query, [cityId]); - return result.rows; - } - - static async findByStreetAndNumber( - cityId: number, - street: string, - number?: string - ): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME} WHERE city_id = $1`; - const values: SqlParams = [cityId]; - let paramIndex = 2; - - if (street) { - query += ` AND LOWER(TRIM(street)) = LOWER(TRIM($${paramIndex++}))`; - values.push(street); - } - - if (number) { - query += ` AND TRIM(number) = TRIM($${paramIndex++})`; - values.push(number); - } else { - query += ` AND (number IS NULL OR number = '')`; - } - - const result = await client.query(query, values); - return result.rows; - } - - static async findByCoordinates(latitude: number, longitude: number, radiusKm: number = 1): Promise { - const client = PostgresDatabase.getClient(); - const query = ` + + const values = [ + addressData.street, + addressData.number || null, + addressData.full_address, + addressData.neighborhood || null, + addressData.postal_code || null, + addressData.latitude || null, + addressData.longitude || null, + addressData.city_id, + ]; + + const result = await client.query(query, values); + return result.rows[0]; + } + + static async findById(id: number): Promise
{ + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${AddressModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByCityId(cityId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${AddressModel.TABLE_NAME} WHERE city_id = $1 ORDER BY full_address`; + const result = await client.query(query, [cityId]); + return result.rows; + } + + static async findByStreetAndNumber( + cityId: number, + street: string, + number?: string, + ): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${AddressModel.TABLE_NAME} WHERE city_id = $1`; + const values: SqlParams = [cityId]; + let paramIndex = 2; + + if (street) { + query += ` AND LOWER(TRIM(street)) = LOWER(TRIM($${paramIndex++}))`; + values.push(street); + } + + if (number) { + query += ` AND TRIM(number) = TRIM($${paramIndex++})`; + values.push(number); + } else { + query += ` AND (number IS NULL OR number = '')`; + } + + const result = await client.query(query, values); + return result.rows; + } + + static async findByCoordinates( + latitude: number, + longitude: number, + radiusKm: number = 1, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = ` SELECT *, (6371 * acos( cos(radians($1)) * cos(radians(latitude)) * cos(radians(longitude) - radians($2)) + sin(radians($1)) * sin(radians(latitude)) )) AS distance - FROM ${this.TABLE_NAME} + FROM ${AddressModel.TABLE_NAME} WHERE latitude IS NOT NULL AND longitude IS NOT NULL HAVING distance < $3 ORDER BY distance `; - const result = await client.query(query, [latitude, longitude, radiusKm]); - return result.rows; - } - - static async update(id: number, updateData: Partial): Promise
{ - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.street !== undefined) { - fields.push(`street = $${paramIndex++}`); - values.push(updateData.street); - } - if (updateData.number !== undefined) { - fields.push(`number = $${paramIndex++}`); - values.push(updateData.number || null); - } - if (updateData.full_address !== undefined) { - fields.push(`full_address = $${paramIndex++}`); - values.push(updateData.full_address); - } - if (updateData.neighborhood !== undefined) { - fields.push(`neighborhood = $${paramIndex++}`); - values.push(updateData.neighborhood || null); - } - if (updateData.postal_code !== undefined) { - fields.push(`postal_code = $${paramIndex++}`); - values.push(updateData.postal_code || null); - } - if (updateData.latitude !== undefined) { - fields.push(`latitude = $${paramIndex++}`); - values.push(updateData.latitude || null); - } - if (updateData.longitude !== undefined) { - fields.push(`longitude = $${paramIndex++}`); - values.push(updateData.longitude || null); - } - if (updateData.city_id !== undefined) { - fields.push(`city_id = $${paramIndex++}`); - values.push(updateData.city_id); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + const result = await client.query(query, [latitude, longitude, radiusKm]); + return result.rows; + } + + static async update( + id: number, + updateData: Partial, + ): Promise
{ + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.street !== undefined) { + fields.push(`street = $${paramIndex++}`); + values.push(updateData.street); + } + if (updateData.number !== undefined) { + fields.push(`number = $${paramIndex++}`); + values.push(updateData.number || null); + } + if (updateData.full_address !== undefined) { + fields.push(`full_address = $${paramIndex++}`); + values.push(updateData.full_address); + } + if (updateData.neighborhood !== undefined) { + fields.push(`neighborhood = $${paramIndex++}`); + values.push(updateData.neighborhood || null); + } + if (updateData.postal_code !== undefined) { + fields.push(`postal_code = $${paramIndex++}`); + values.push(updateData.postal_code || null); + } + if (updateData.latitude !== undefined) { + fields.push(`latitude = $${paramIndex++}`); + values.push(updateData.latitude || null); + } + if (updateData.longitude !== undefined) { + fields.push(`longitude = $${paramIndex++}`); + values.push(updateData.longitude || null); + } + if (updateData.city_id !== undefined) { + fields.push(`city_id = $${paramIndex++}`); + values.push(updateData.city_id); + } + + if (fields.length === 0) { + return await AddressModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${AddressModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${AddressModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/catalog-service.model.ts b/backend/src/data/postgres/models/properties/catalog-service.model.ts index fc27845..a36988a 100644 --- a/backend/src/data/postgres/models/properties/catalog-service.model.ts +++ b/backend/src/data/postgres/models/properties/catalog-service.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface CatalogService { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateCatalogServiceDto { - name: string; + name: string; } export class CatalogServiceModel { - private static readonly TABLE_NAME = 'catalog_services'; - - static async create(data: CreateCatalogServiceDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "catalog_services"; + + static async create(data: CreateCatalogServiceDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${CatalogServiceModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CatalogServiceModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CatalogServiceModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${CatalogServiceModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await CatalogServiceModel.findById(id); + } + const query = `UPDATE ${CatalogServiceModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${CatalogServiceModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/disposition.model.ts b/backend/src/data/postgres/models/properties/disposition.model.ts index 75f353a..d616a32 100644 --- a/backend/src/data/postgres/models/properties/disposition.model.ts +++ b/backend/src/data/postgres/models/properties/disposition.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface Disposition { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateDispositionDto { - name: string; + name: string; } export class DispositionModel { - private static readonly TABLE_NAME = 'dispositions'; - - static async create(data: CreateDispositionDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "dispositions"; + + static async create(data: CreateDispositionDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${DispositionModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${DispositionModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${DispositionModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${DispositionModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await DispositionModel.findById(id); + } + const query = `UPDATE ${DispositionModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${DispositionModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/index.ts b/backend/src/data/postgres/models/properties/index.ts index a58c705..7c7457a 100644 --- a/backend/src/data/postgres/models/properties/index.ts +++ b/backend/src/data/postgres/models/properties/index.ts @@ -1,18 +1,17 @@ -export * from './address.model'; -export * from './catalog-service.model'; -export * from './disposition.model'; -export * from './orientation.model'; -export * from './property.model'; -export * from './property-address.model'; -export * from './property-age.model'; -export * from './property-characteristic.model'; -export * from './property-document.model'; -export * from './property-multimedia.model'; -export * from './property-operation-type.model'; -export * from './property-price.model'; -export * from './property-service.model'; -export * from './property-situation.model'; -export * from './property-status.model'; -export * from './property-type.model'; -export * from './visibility-status.model'; - +export * from "./address.model"; +export * from "./catalog-service.model"; +export * from "./disposition.model"; +export * from "./orientation.model"; +export * from "./property.model"; +export * from "./property-address.model"; +export * from "./property-age.model"; +export * from "./property-characteristic.model"; +export * from "./property-document.model"; +export * from "./property-multimedia.model"; +export * from "./property-operation-type.model"; +export * from "./property-price.model"; +export * from "./property-service.model"; +export * from "./property-situation.model"; +export * from "./property-status.model"; +export * from "./property-type.model"; +export * from "./visibility-status.model"; diff --git a/backend/src/data/postgres/models/properties/orientation.model.ts b/backend/src/data/postgres/models/properties/orientation.model.ts index afca85d..cda7823 100644 --- a/backend/src/data/postgres/models/properties/orientation.model.ts +++ b/backend/src/data/postgres/models/properties/orientation.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface Orientation { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateOrientationDto { - name: string; + name: string; } export class OrientationModel { - private static readonly TABLE_NAME = 'orientations'; - - static async create(data: CreateOrientationDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "orientations"; + + static async create(data: CreateOrientationDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${OrientationModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${OrientationModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${OrientationModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${OrientationModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await OrientationModel.findById(id); + } + const query = `UPDATE ${OrientationModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${OrientationModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/property-address.model.ts b/backend/src/data/postgres/models/properties/property-address.model.ts index 79ec8ff..2b6d382 100644 --- a/backend/src/data/postgres/models/properties/property-address.model.ts +++ b/backend/src/data/postgres/models/properties/property-address.model.ts @@ -1,58 +1,64 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertyAddress { - property_id: number; - address_id: number; + property_id: number; + address_id: number; } export interface CreatePropertyAddressDto { - property_id: number; - address_id: number; + property_id: number; + address_id: number; } export class PropertyAddressModel { - private static readonly TABLE_NAME = 'property_addresses'; + private static readonly TABLE_NAME = "property_addresses"; - static async create(data: CreatePropertyAddressDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (property_id, address_id) + static async create( + data: CreatePropertyAddressDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PropertyAddressModel.TABLE_NAME} (property_id, address_id) VALUES ($1, $2) ON CONFLICT (property_id, address_id) DO NOTHING RETURNING * `; - - const result = await client.query(query, [data.property_id, data.address_id]); - return result.rows[0]; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1`; - const result = await client.query(query, [propertyId]); - return result.rows; - } - - static async findByAddressId(addressId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE address_id = $1`; - const result = await client.query(query, [addressId]); - return result.rows; - } - - static async delete(propertyId: number, addressId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE property_id = $1 AND address_id = $2`; - const result = await client.query(query, [propertyId, addressId]); - return (result.rowCount ?? 0) > 0; - } - - static async deleteByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE property_id = $1`; - const result = await client.query(query, [propertyId]); - return result.rowCount ?? 0; - } -} + const result = await client.query(query, [ + data.property_id, + data.address_id, + ]); + return result.rows[0]; + } + + static async findByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyAddressModel.TABLE_NAME} WHERE property_id = $1`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + + static async findByAddressId(addressId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyAddressModel.TABLE_NAME} WHERE address_id = $1`; + const result = await client.query(query, [addressId]); + return result.rows; + } + + static async delete(propertyId: number, addressId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyAddressModel.TABLE_NAME} WHERE property_id = $1 AND address_id = $2`; + const result = await client.query(query, [propertyId, addressId]); + return (result.rowCount ?? 0) > 0; + } + + static async deleteByPropertyId(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyAddressModel.TABLE_NAME} WHERE property_id = $1`; + const result = await client.query(query, [propertyId]); + return result.rowCount ?? 0; + } +} diff --git a/backend/src/data/postgres/models/properties/property-age.model.ts b/backend/src/data/postgres/models/properties/property-age.model.ts index a949c41..d61f87e 100644 --- a/backend/src/data/postgres/models/properties/property-age.model.ts +++ b/backend/src/data/postgres/models/properties/property-age.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertyAge { - id?: number; - name: string; + id?: number; + name: string; } export interface CreatePropertyAgeDto { - name: string; + name: string; } export class PropertyAgeModel { - private static readonly TABLE_NAME = 'property_ages'; - - static async create(data: CreatePropertyAgeDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "property_ages"; + + static async create(data: CreatePropertyAgeDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${PropertyAgeModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyAgeModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyAgeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyAgeModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await PropertyAgeModel.findById(id); + } + const query = `UPDATE ${PropertyAgeModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyAgeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/property-characteristic.model.ts b/backend/src/data/postgres/models/properties/property-characteristic.model.ts index 15cd1b4..7970df4 100644 --- a/backend/src/data/postgres/models/properties/property-characteristic.model.ts +++ b/backend/src/data/postgres/models/properties/property-characteristic.model.ts @@ -1,99 +1,105 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface PropertyCharacteristic { - id?: number; - property_id: number; - name: string; - value: string; + id?: number; + property_id: number; + name: string; + value: string; } export interface CreatePropertyCharacteristicDto { - property_id: number; - name: string; - value: string; + property_id: number; + name: string; + value: string; } export class PropertyCharacteristicModel { - private static readonly TABLE_NAME = 'property_characteristics'; + private static readonly TABLE_NAME = "property_characteristics"; - static async create(characteristicData: CreatePropertyCharacteristicDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (property_id, name, value) + static async create( + characteristicData: CreatePropertyCharacteristicDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PropertyCharacteristicModel.TABLE_NAME} (property_id, name, value) VALUES ($1, $2, $3) RETURNING * `; - - const result = await client.query(query, [ - characteristicData.property_id, - characteristicData.name, - characteristicData.value, - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY name`; - const result = await client.query(query, [propertyId]); - return result.rows; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.name) { - fields.push(`name = $${paramIndex++}`); - values.push(updateData.name); - } - if (updateData.value) { - fields.push(`value = $${paramIndex++}`); - values.push(updateData.value); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + + const result = await client.query(query, [ + characteristicData.property_id, + characteristicData.name, + characteristicData.value, + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyCharacteristicModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyCharacteristicModel.TABLE_NAME} WHERE property_id = $1 ORDER BY name`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.name) { + fields.push(`name = $${paramIndex++}`); + values.push(updateData.name); + } + if (updateData.value) { + fields.push(`value = $${paramIndex++}`); + values.push(updateData.value); + } + + if (fields.length === 0) { + return await PropertyCharacteristicModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${PropertyCharacteristicModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } - - static async deleteByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE property_id = $1`; - const result = await client.query(query, [propertyId]); - return result.rowCount ?? 0; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyCharacteristicModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } + + static async deleteByPropertyId(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyCharacteristicModel.TABLE_NAME} WHERE property_id = $1`; + const result = await client.query(query, [propertyId]); + return result.rowCount ?? 0; + } } - diff --git a/backend/src/data/postgres/models/properties/property-document.model.ts b/backend/src/data/postgres/models/properties/property-document.model.ts index 53ec4ad..881be62 100644 --- a/backend/src/data/postgres/models/properties/property-document.model.ts +++ b/backend/src/data/postgres/models/properties/property-document.model.ts @@ -1,69 +1,72 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertyDocument { - id?: number; - property_id: number; - client_id?: number; - document_name: string; - file_path: string; - uploaded_at?: Date; + id?: number; + property_id: number; + client_id?: number; + document_name: string; + file_path: string; + uploaded_at?: Date; } export interface CreatePropertyDocumentDto { - property_id: number; - client_id?: number; - document_name: string; - file_path: string; + property_id: number; + client_id?: number; + document_name: string; + file_path: string; } export class PropertyDocumentModel { - private static readonly TABLE_NAME = 'property_documents'; + private static readonly TABLE_NAME = "property_documents"; - static async create(documentData: CreatePropertyDocumentDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (property_id, client_id, document_name, file_path, uploaded_at) + static async create( + documentData: CreatePropertyDocumentDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PropertyDocumentModel.TABLE_NAME} (property_id, client_id, document_name, file_path, uploaded_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) RETURNING * `; - - const result = await client.query(query, [ - documentData.property_id, - documentData.client_id || null, - documentData.document_name, - documentData.file_path, - ]); - return result.rows[0]; - } + const result = await client.query(query, [ + documentData.property_id, + documentData.client_id || null, + documentData.document_name, + documentData.file_path, + ]); - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } + return result.rows[0]; + } - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY uploaded_at DESC`; - const result = await client.query(query, [propertyId]); - return result.rows; - } + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyDocumentModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } - static async findByClientId(clientId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE client_id = $1 ORDER BY uploaded_at DESC`; - const result = await client.query(query, [clientId]); - return result.rows; - } + static async findByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyDocumentModel.TABLE_NAME} WHERE property_id = $1 ORDER BY uploaded_at DESC`; + const result = await client.query(query, [propertyId]); + return result.rows; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + static async findByClientId(clientId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyDocumentModel.TABLE_NAME} WHERE client_id = $1 ORDER BY uploaded_at DESC`; + const result = await client.query(query, [clientId]); + return result.rows; + } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyDocumentModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/properties/property-multimedia.model.ts b/backend/src/data/postgres/models/properties/property-multimedia.model.ts index 9310136..d3d6cf0 100644 --- a/backend/src/data/postgres/models/properties/property-multimedia.model.ts +++ b/backend/src/data/postgres/models/properties/property-multimedia.model.ts @@ -1,115 +1,123 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertyMultimedia { - id?: number; - property_id: number; - file_path: string; - media_type: string; - is_primary?: boolean; + id?: number; + property_id: number; + file_path: string; + media_type: string; + is_primary?: boolean; } export interface CreatePropertyMultimediaDto { - property_id: number; - file_path: string; - media_type: string; - is_primary?: boolean; + property_id: number; + file_path: string; + media_type: string; + is_primary?: boolean; } export class PropertyMultimediaModel { - private static readonly TABLE_NAME = 'property_multimedia'; + private static readonly TABLE_NAME = "property_multimedia"; - static async create(multimediaData: CreatePropertyMultimediaDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (property_id, file_path, media_type, is_primary) + static async create( + multimediaData: CreatePropertyMultimediaDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PropertyMultimediaModel.TABLE_NAME} (property_id, file_path, media_type, is_primary) VALUES ($1, $2, $3, $4) RETURNING * `; - - const result = await client.query(query, [ - multimediaData.property_id, - multimediaData.file_path, - multimediaData.media_type, - multimediaData.is_primary !== undefined ? multimediaData.is_primary : false, - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY is_primary DESC, id`; - const result = await client.query(query, [propertyId]); - return result.rows; - } - - static async findPrimaryByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 AND is_primary = true LIMIT 1`; - const result = await client.query(query, [propertyId]); - return result.rows[0] || null; - } - - static async setAsPrimary(id: number, propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - - await client.query( - `UPDATE ${this.TABLE_NAME} SET is_primary = false WHERE property_id = $1`, - [propertyId] - ); - - const result = await client.query( - `UPDATE ${this.TABLE_NAME} SET is_primary = true WHERE id = $1 RETURNING id`, - [id] - ); - - return (result.rowCount ?? 0) > 0; - } - - - static async clearPrimaryForProperty(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - await client.query( - `UPDATE ${this.TABLE_NAME} SET is_primary = false WHERE property_id = $1`, - [propertyId] - ); - } - - - static async updateImageOrder(propertyId: number, imageIds: number[]): Promise { - const client = PostgresDatabase.getClient(); - - try { - await this.clearPrimaryForProperty(propertyId); - - if (imageIds.length > 0) { - const result = await client.query( - `UPDATE ${this.TABLE_NAME} SET is_primary = true WHERE id = $1 AND property_id = $2 RETURNING id`, - [imageIds[0], propertyId] - ); - return (result.rowCount ?? 0) > 0; - } - - return true; - } catch (error) { - console.error('Error updating image order:', error); - return false; - } - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + const result = await client.query(query, [ + multimediaData.property_id, + multimediaData.file_path, + multimediaData.media_type, + multimediaData.is_primary !== undefined + ? multimediaData.is_primary + : false, + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyMultimediaModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyMultimediaModel.TABLE_NAME} WHERE property_id = $1 ORDER BY is_primary DESC, id`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + + static async findPrimaryByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyMultimediaModel.TABLE_NAME} WHERE property_id = $1 AND is_primary = true LIMIT 1`; + const result = await client.query(query, [propertyId]); + return result.rows[0] || null; + } + + static async setAsPrimary(id: number, propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + + await client.query( + `UPDATE ${PropertyMultimediaModel.TABLE_NAME} SET is_primary = false WHERE property_id = $1`, + [propertyId], + ); + + const result = await client.query( + `UPDATE ${PropertyMultimediaModel.TABLE_NAME} SET is_primary = true WHERE id = $1 RETURNING id`, + [id], + ); + + return (result.rowCount ?? 0) > 0; + } + + static async clearPrimaryForProperty(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + await client.query( + `UPDATE ${PropertyMultimediaModel.TABLE_NAME} SET is_primary = false WHERE property_id = $1`, + [propertyId], + ); + } + + static async updateImageOrder( + propertyId: number, + imageIds: number[], + ): Promise { + const client = PostgresDatabase.getClient(); + + try { + await PropertyMultimediaModel.clearPrimaryForProperty(propertyId); + + if (imageIds.length > 0) { + const result = await client.query( + `UPDATE ${PropertyMultimediaModel.TABLE_NAME} SET is_primary = true WHERE id = $1 AND property_id = $2 RETURNING id`, + [imageIds[0], propertyId], + ); + return (result.rowCount ?? 0) > 0; + } + + return true; + } catch (error) { + console.error("Error updating image order:", error); + return false; + } + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyMultimediaModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/properties/property-operation-type.model.ts b/backend/src/data/postgres/models/properties/property-operation-type.model.ts index 0ddbcd1..ba369ee 100644 --- a/backend/src/data/postgres/models/properties/property-operation-type.model.ts +++ b/backend/src/data/postgres/models/properties/property-operation-type.model.ts @@ -1,60 +1,64 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertyOperationType { - id?: number; - name: string; + id?: number; + name: string; } export interface CreatePropertyOperationTypeDto { - name: string; + name: string; } export class PropertyOperationTypeModel { - private static readonly TABLE_NAME = 'property_operation_types'; - - static async create(data: CreatePropertyOperationTypeDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + private static readonly TABLE_NAME = "property_operation_types"; + + static async create( + data: CreatePropertyOperationTypeDto, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${PropertyOperationTypeModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyOperationTypeModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyOperationTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyOperationTypeModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await PropertyOperationTypeModel.findById(id); + } + const query = `UPDATE ${PropertyOperationTypeModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyOperationTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/properties/property-price.model.ts b/backend/src/data/postgres/models/properties/property-price.model.ts index 0ce64d0..9d17b25 100644 --- a/backend/src/data/postgres/models/properties/property-price.model.ts +++ b/backend/src/data/postgres/models/properties/property-price.model.ts @@ -1,123 +1,127 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface PropertyPrice { - id?: number; - property_id: number; - price: number; - currency_type_id: number; - operation_type_id: number; - updated_at?: Date; + id?: number; + property_id: number; + price: number; + currency_type_id: number; + operation_type_id: number; + updated_at?: Date; } export interface CreatePropertyPriceDto { - property_id: number; - price: number; - currency_type_id: number; - operation_type_id: number; + property_id: number; + price: number; + currency_type_id: number; + operation_type_id: number; } export class PropertyPriceModel { - private static readonly TABLE_NAME = 'property_prices'; + private static readonly TABLE_NAME = "property_prices"; - static async create(priceData: CreatePropertyPriceDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (property_id, price, currency_type_id, operation_type_id, updated_at) + static async create( + priceData: CreatePropertyPriceDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PropertyPriceModel.TABLE_NAME} (property_id, price, currency_type_id, operation_type_id, updated_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) RETURNING * `; - - const result = await client.query(query, [ - priceData.property_id, - priceData.price, - priceData.currency_type_id, - priceData.operation_type_id, - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY updated_at DESC`; - const result = await client.query(query, [propertyId]); - return result.rows; - } - - static async findCurrentByPropertyAndOperation( - propertyId: number, - operationTypeId: number - ): Promise { - const client = PostgresDatabase.getClient(); - const query = ` - SELECT * FROM ${this.TABLE_NAME} + + const result = await client.query(query, [ + priceData.property_id, + priceData.price, + priceData.currency_type_id, + priceData.operation_type_id, + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyPriceModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByPropertyId(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyPriceModel.TABLE_NAME} WHERE property_id = $1 ORDER BY updated_at DESC`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + + static async findCurrentByPropertyAndOperation( + propertyId: number, + operationTypeId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = ` + SELECT * FROM ${PropertyPriceModel.TABLE_NAME} WHERE property_id = $1 AND operation_type_id = $2 ORDER BY updated_at DESC LIMIT 1 `; - const result = await client.query(query, [propertyId, operationTypeId]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.price !== undefined) { - fields.push(`price = $${paramIndex++}`); - values.push(updateData.price); - } - if (updateData.currency_type_id !== undefined) { - fields.push(`currency_type_id = $${paramIndex++}`); - values.push(updateData.currency_type_id); - } - if (updateData.operation_type_id !== undefined) { - fields.push(`operation_type_id = $${paramIndex++}`); - values.push(updateData.operation_type_id); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - fields.push(`updated_at = CURRENT_TIMESTAMP`); - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + const result = await client.query(query, [propertyId, operationTypeId]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.price !== undefined) { + fields.push(`price = $${paramIndex++}`); + values.push(updateData.price); + } + if (updateData.currency_type_id !== undefined) { + fields.push(`currency_type_id = $${paramIndex++}`); + values.push(updateData.currency_type_id); + } + if (updateData.operation_type_id !== undefined) { + fields.push(`operation_type_id = $${paramIndex++}`); + values.push(updateData.operation_type_id); + } + + if (fields.length === 0) { + return await PropertyPriceModel.findById(id); + } + + fields.push(`updated_at = CURRENT_TIMESTAMP`); + values.push(id); + + const query = ` + UPDATE ${PropertyPriceModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } - - static async deleteByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE property_id = $1`; - const result = await client.query(query, [propertyId]); - return result.rowCount ?? 0; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyPriceModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } + + static async deleteByPropertyId(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyPriceModel.TABLE_NAME} WHERE property_id = $1`; + const result = await client.query(query, [propertyId]); + return result.rowCount ?? 0; + } } - diff --git a/backend/src/data/postgres/models/properties/property-service.model.ts b/backend/src/data/postgres/models/properties/property-service.model.ts index 1c34f6b..7aa4d59 100644 --- a/backend/src/data/postgres/models/properties/property-service.model.ts +++ b/backend/src/data/postgres/models/properties/property-service.model.ts @@ -1,58 +1,64 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertyService { - property_id: number; - service_id: number; + property_id: number; + service_id: number; } export interface CreatePropertyServiceDto { - property_id: number; - service_id: number; + property_id: number; + service_id: number; } export class PropertyServiceModel { - private static readonly TABLE_NAME = 'property_services'; + private static readonly TABLE_NAME = "property_services"; - static async create(data: CreatePropertyServiceDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (property_id, service_id) + static async create( + data: CreatePropertyServiceDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PropertyServiceModel.TABLE_NAME} (property_id, service_id) VALUES ($1, $2) ON CONFLICT (property_id, service_id) DO NOTHING RETURNING * `; - - const result = await client.query(query, [data.property_id, data.service_id]); - return result.rows[0]; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1`; - const result = await client.query(query, [propertyId]); - return result.rows; - } - - static async findByServiceId(serviceId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE service_id = $1`; - const result = await client.query(query, [serviceId]); - return result.rows; - } - - static async delete(propertyId: number, serviceId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE property_id = $1 AND service_id = $2`; - const result = await client.query(query, [propertyId, serviceId]); - return (result.rowCount ?? 0) > 0; - } - - static async deleteByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE property_id = $1`; - const result = await client.query(query, [propertyId]); - return result.rowCount ?? 0; - } -} + const result = await client.query(query, [ + data.property_id, + data.service_id, + ]); + return result.rows[0]; + } + + static async findByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyServiceModel.TABLE_NAME} WHERE property_id = $1`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + + static async findByServiceId(serviceId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyServiceModel.TABLE_NAME} WHERE service_id = $1`; + const result = await client.query(query, [serviceId]); + return result.rows; + } + + static async delete(propertyId: number, serviceId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyServiceModel.TABLE_NAME} WHERE property_id = $1 AND service_id = $2`; + const result = await client.query(query, [propertyId, serviceId]); + return (result.rowCount ?? 0) > 0; + } + + static async deleteByPropertyId(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyServiceModel.TABLE_NAME} WHERE property_id = $1`; + const result = await client.query(query, [propertyId]); + return result.rowCount ?? 0; + } +} diff --git a/backend/src/data/postgres/models/properties/property-situation.model.ts b/backend/src/data/postgres/models/properties/property-situation.model.ts index 2555560..b341c84 100644 --- a/backend/src/data/postgres/models/properties/property-situation.model.ts +++ b/backend/src/data/postgres/models/properties/property-situation.model.ts @@ -1,60 +1,64 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertySituation { - id?: number; - name: string; + id?: number; + name: string; } export interface CreatePropertySituationDto { - name: string; + name: string; } export class PropertySituationModel { - private static readonly TABLE_NAME = 'property_situations'; - - static async create(data: CreatePropertySituationDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + private static readonly TABLE_NAME = "property_situations"; + + static async create( + data: CreatePropertySituationDto, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${PropertySituationModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertySituationModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertySituationModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertySituationModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await PropertySituationModel.findById(id); + } + const query = `UPDATE ${PropertySituationModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertySituationModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/properties/property-status.model.ts b/backend/src/data/postgres/models/properties/property-status.model.ts index fdb64a5..61f01bd 100644 --- a/backend/src/data/postgres/models/properties/property-status.model.ts +++ b/backend/src/data/postgres/models/properties/property-status.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertyStatus { - id?: number; - name: string; + id?: number; + name: string; } export interface CreatePropertyStatusDto { - name: string; + name: string; } export class PropertyStatusModel { - private static readonly TABLE_NAME = 'property_statuses'; - - static async create(data: CreatePropertyStatusDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "property_statuses"; + + static async create(data: CreatePropertyStatusDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${PropertyStatusModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyStatusModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyStatusModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyStatusModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await PropertyStatusModel.findById(id); + } + const query = `UPDATE ${PropertyStatusModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyStatusModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/property-type.model.ts b/backend/src/data/postgres/models/properties/property-type.model.ts index 6f02e11..72d3a8d 100644 --- a/backend/src/data/postgres/models/properties/property-type.model.ts +++ b/backend/src/data/postgres/models/properties/property-type.model.ts @@ -1,60 +1,62 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PropertyType { - id?: number; - name: string; + id?: number; + name: string; } export interface CreatePropertyTypeDto { - name: string; + name: string; } export class PropertyTypeModel { - private static readonly TABLE_NAME = 'property_types'; - - static async create(data: CreatePropertyTypeDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + private static readonly TABLE_NAME = "property_types"; + + static async create(data: CreatePropertyTypeDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${PropertyTypeModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyTypeModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyTypeModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await PropertyTypeModel.findById(id); + } + const query = `UPDATE ${PropertyTypeModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyTypeModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/property.model.ts b/backend/src/data/postgres/models/properties/property.model.ts index 6b32807..f892542 100644 --- a/backend/src/data/postgres/models/properties/property.model.ts +++ b/backend/src/data/postgres/models/properties/property.model.ts @@ -1,237 +1,276 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface Property { - id?: number; - title: string; - description?: string; - publication_date?: Date; - featured_web?: boolean; - visibility_status_id: number; - captured_by_user_id: number; - branch_name?: string; - appraiser?: string; - producer?: string; - maintenance_user?: string; - keys_location?: string; - internal_comments?: string; - social_media_info?: string; - operation_commission_percentage?: number; - producer_commission_percentage?: number; - land_area?: number; - semi_covered_area?: number; - covered_area?: number; - total_built_area?: number; - uncovered_area?: number; - total_area?: number; - rooms_count?: number; - bedrooms_count?: number; - bathrooms_count?: number; - toilets_count?: number; - parking_spaces_count?: number; - floors_count?: number; - zoning?: string; - property_type_id: number; - property_status_id: number; - owner_id?: number; - situation_id?: number; - age_id?: number; - orientation_id?: number; - disposition_id?: number; - updated_at?: Date; + id?: number; + title: string; + description?: string; + publication_date?: Date; + featured_web?: boolean; + visibility_status_id: number; + captured_by_user_id: number; + branch_name?: string; + appraiser?: string; + producer?: string; + maintenance_user?: string; + keys_location?: string; + internal_comments?: string; + social_media_info?: string; + operation_commission_percentage?: number; + producer_commission_percentage?: number; + land_area?: number; + semi_covered_area?: number; + covered_area?: number; + total_built_area?: number; + uncovered_area?: number; + total_area?: number; + rooms_count?: number; + bedrooms_count?: number; + bathrooms_count?: number; + toilets_count?: number; + parking_spaces_count?: number; + floors_count?: number; + zoning?: string; + property_type_id: number; + property_status_id: number; + owner_id?: number; + situation_id?: number; + age_id?: number; + orientation_id?: number; + disposition_id?: number; + updated_at?: Date; } export interface CreatePropertyDto { - title: string; - description?: string; - publication_date?: Date; - featured_web?: boolean; - visibility_status_id: number; - captured_by_user_id: number; - branch_name?: string; - appraiser?: string; - producer?: string; - maintenance_user?: string; - keys_location?: string; - internal_comments?: string; - social_media_info?: string; - operation_commission_percentage?: number; - producer_commission_percentage?: number; - land_area?: number; - semi_covered_area?: number; - covered_area?: number; - total_built_area?: number; - uncovered_area?: number; - total_area?: number; - rooms_count?: number; - bedrooms_count?: number; - bathrooms_count?: number; - toilets_count?: number; - parking_spaces_count?: number; - floors_count?: number; - zoning?: string; - property_type_id: number; - property_status_id: number; - owner_id?: number; - situation_id?: number; - age_id?: number; - orientation_id?: number; - disposition_id?: number; + title: string; + description?: string; + publication_date?: Date; + featured_web?: boolean; + visibility_status_id: number; + captured_by_user_id: number; + branch_name?: string; + appraiser?: string; + producer?: string; + maintenance_user?: string; + keys_location?: string; + internal_comments?: string; + social_media_info?: string; + operation_commission_percentage?: number; + producer_commission_percentage?: number; + land_area?: number; + semi_covered_area?: number; + covered_area?: number; + total_built_area?: number; + uncovered_area?: number; + total_area?: number; + rooms_count?: number; + bedrooms_count?: number; + bathrooms_count?: number; + toilets_count?: number; + parking_spaces_count?: number; + floors_count?: number; + zoning?: string; + property_type_id: number; + property_status_id: number; + owner_id?: number; + situation_id?: number; + age_id?: number; + orientation_id?: number; + disposition_id?: number; } export interface PropertyFilters { - property_type_id?: number; - property_status_id?: number; - visibility_status_id?: number; - owner_id?: number; - captured_by_user_id?: number; - city_id?: number; - min_price?: number; - max_price?: number; - operation_type_id?: number; - currency_type_id?: number; - featured_web?: boolean; - search?: string; - includeArchived?: boolean; - limit?: number; - offset?: number; + property_type_id?: number; + property_status_id?: number; + visibility_status_id?: number; + owner_id?: number; + captured_by_user_id?: number; + city_id?: number; + min_price?: number; + max_price?: number; + operation_type_id?: number; + currency_type_id?: number; + featured_web?: boolean; + search?: string; + includeArchived?: boolean; + limit?: number; + offset?: number; } export class PropertyModel { - private static readonly TABLE_NAME = 'properties'; - - static async create(propertyData: CreatePropertyDto): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - const placeholders: string[] = []; - let paramIndex = 1; - fields.push('title'); - values.push(propertyData.title); - placeholders.push(`$${paramIndex++}`); - - fields.push('visibility_status_id'); - values.push(propertyData.visibility_status_id); - placeholders.push(`$${paramIndex++}`); - - fields.push('captured_by_user_id'); - values.push(propertyData.captured_by_user_id); - placeholders.push(`$${paramIndex++}`); - - fields.push('property_type_id'); - values.push(propertyData.property_type_id); - placeholders.push(`$${paramIndex++}`); - - fields.push('property_status_id'); - values.push(propertyData.property_status_id); - placeholders.push(`$${paramIndex++}`); - - console.log('[PropertyModel] propertyData.owner_id:', propertyData.owner_id, 'type:', typeof propertyData.owner_id); - if (propertyData.owner_id !== undefined && propertyData.owner_id !== null) { - console.log('[PropertyModel] Adding owner_id to INSERT:', propertyData.owner_id); - fields.push('owner_id'); - values.push(propertyData.owner_id); - placeholders.push(`$${paramIndex++}`); - } else { - console.log('[PropertyModel] owner_id is undefined or null, NOT adding to INSERT'); - } - - const addOptionalField = (fieldName: string, value: any) => { - if (value !== undefined && value !== null && value !== '') { - fields.push(fieldName); - values.push(value); - placeholders.push(`$${paramIndex++}`); - } - }; - - addOptionalField('description', propertyData.description); - addOptionalField('publication_date', propertyData.publication_date || new Date()); - addOptionalField('featured_web', propertyData.featured_web !== undefined ? propertyData.featured_web : false); - addOptionalField('branch_name', propertyData.branch_name); - addOptionalField('appraiser', propertyData.appraiser); - addOptionalField('producer', propertyData.producer); - addOptionalField('maintenance_user', propertyData.maintenance_user); - addOptionalField('keys_location', propertyData.keys_location); - addOptionalField('internal_comments', propertyData.internal_comments); - addOptionalField('social_media_info', propertyData.social_media_info); - addOptionalField('operation_commission_percentage', propertyData.operation_commission_percentage); - addOptionalField('producer_commission_percentage', propertyData.producer_commission_percentage); - addOptionalField('land_area', propertyData.land_area); - addOptionalField('semi_covered_area', propertyData.semi_covered_area); - addOptionalField('covered_area', propertyData.covered_area); - addOptionalField('total_built_area', propertyData.total_built_area); - addOptionalField('uncovered_area', propertyData.uncovered_area); - addOptionalField('total_area', propertyData.total_area); - addOptionalField('rooms_count', propertyData.rooms_count); - addOptionalField('bedrooms_count', propertyData.bedrooms_count); - addOptionalField('bathrooms_count', propertyData.bathrooms_count); - addOptionalField('toilets_count', propertyData.toilets_count); - addOptionalField('parking_spaces_count', propertyData.parking_spaces_count); - addOptionalField('floors_count', propertyData.floors_count); - addOptionalField('zoning', propertyData.zoning); - addOptionalField('situation_id', propertyData.situation_id); - addOptionalField('age_id', propertyData.age_id); - addOptionalField('orientation_id', propertyData.orientation_id); - addOptionalField('disposition_id', propertyData.disposition_id); - - fields.push('updated_at'); - placeholders.push('CURRENT_TIMESTAMP'); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (${fields.join(', ')}) - VALUES (${placeholders.join(', ')}) + private static readonly TABLE_NAME = "properties"; + + static async create(propertyData: CreatePropertyDto): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + const placeholders: string[] = []; + let paramIndex = 1; + fields.push("title"); + values.push(propertyData.title); + placeholders.push(`$${paramIndex++}`); + + fields.push("visibility_status_id"); + values.push(propertyData.visibility_status_id); + placeholders.push(`$${paramIndex++}`); + + fields.push("captured_by_user_id"); + values.push(propertyData.captured_by_user_id); + placeholders.push(`$${paramIndex++}`); + + fields.push("property_type_id"); + values.push(propertyData.property_type_id); + placeholders.push(`$${paramIndex++}`); + + fields.push("property_status_id"); + values.push(propertyData.property_status_id); + placeholders.push(`$${paramIndex++}`); + + console.log( + "[PropertyModel] propertyData.owner_id:", + propertyData.owner_id, + "type:", + typeof propertyData.owner_id, + ); + if (propertyData.owner_id !== undefined && propertyData.owner_id !== null) { + console.log( + "[PropertyModel] Adding owner_id to INSERT:", + propertyData.owner_id, + ); + fields.push("owner_id"); + values.push(propertyData.owner_id); + placeholders.push(`$${paramIndex++}`); + } else { + console.log( + "[PropertyModel] owner_id is undefined or null, NOT adding to INSERT", + ); + } + + const addOptionalField = ( + fieldName: string, + value: string | number | boolean | Date | null | undefined, + ) => { + if (value !== undefined && value !== null && value !== "") { + fields.push(fieldName); + values.push(value); + placeholders.push(`$${paramIndex++}`); + } + }; + + addOptionalField("description", propertyData.description); + addOptionalField( + "publication_date", + propertyData.publication_date || new Date(), + ); + addOptionalField( + "featured_web", + propertyData.featured_web !== undefined + ? propertyData.featured_web + : false, + ); + addOptionalField("branch_name", propertyData.branch_name); + addOptionalField("appraiser", propertyData.appraiser); + addOptionalField("producer", propertyData.producer); + addOptionalField("maintenance_user", propertyData.maintenance_user); + addOptionalField("keys_location", propertyData.keys_location); + addOptionalField("internal_comments", propertyData.internal_comments); + addOptionalField("social_media_info", propertyData.social_media_info); + addOptionalField( + "operation_commission_percentage", + propertyData.operation_commission_percentage, + ); + addOptionalField( + "producer_commission_percentage", + propertyData.producer_commission_percentage, + ); + addOptionalField("land_area", propertyData.land_area); + addOptionalField("semi_covered_area", propertyData.semi_covered_area); + addOptionalField("covered_area", propertyData.covered_area); + addOptionalField("total_built_area", propertyData.total_built_area); + addOptionalField("uncovered_area", propertyData.uncovered_area); + addOptionalField("total_area", propertyData.total_area); + addOptionalField("rooms_count", propertyData.rooms_count); + addOptionalField("bedrooms_count", propertyData.bedrooms_count); + addOptionalField("bathrooms_count", propertyData.bathrooms_count); + addOptionalField("toilets_count", propertyData.toilets_count); + addOptionalField("parking_spaces_count", propertyData.parking_spaces_count); + addOptionalField("floors_count", propertyData.floors_count); + addOptionalField("zoning", propertyData.zoning); + addOptionalField("situation_id", propertyData.situation_id); + addOptionalField("age_id", propertyData.age_id); + addOptionalField("orientation_id", propertyData.orientation_id); + addOptionalField("disposition_id", propertyData.disposition_id); + + fields.push("updated_at"); + placeholders.push("CURRENT_TIMESTAMP"); + + const query = ` + INSERT INTO ${PropertyModel.TABLE_NAME} (${fields.join(", ")}) + VALUES (${placeholders.join(", ")}) RETURNING * `; - - const result = await client.query(query, values); - return result.rows[0]; - } - - static async findById(id: number, includeArchived: boolean = false): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - - if (!includeArchived) { - query += ` AND visibility_status_id != (SELECT id FROM visibility_statuses WHERE name = 'Archivada')`; - } - - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByIdWithRelations(id: number, includeArchived: boolean = false): Promise { - const property = await this.findById(id, includeArchived); - if (!property || !property.id) { - return null; - } - - const { PropertyAddressModel } = await import('./property-address.model'); - const { AddressModel } = await import('./address.model'); - const propertyAddresses = await PropertyAddressModel.findByPropertyId(property.id); - const addresses = await Promise.all( - propertyAddresses.map(pa => AddressModel.findById(pa.address_id)) - ); - - const { PropertyPriceModel } = await import('./property-price.model'); - const prices = await PropertyPriceModel.findByPropertyId(property.id); - - const { PropertyMultimediaModel } = await import('./property-multimedia.model'); - const images = await PropertyMultimediaModel.findByPropertyId(property.id); - - return { - ...property, - addresses: addresses.filter(a => a !== null), - prices, - images, - }; - } - - static async findAll(filters?: PropertyFilters): Promise { - const client = PostgresDatabase.getClient(); - - let query = ` - SELECT + + const result = await client.query(query, values); + return result.rows[0]; + } + + static async findById( + id: number, + includeArchived: boolean = false, + ): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${PropertyModel.TABLE_NAME} WHERE id = $1`; + + if (!includeArchived) { + query += ` AND visibility_status_id != (SELECT id FROM visibility_statuses WHERE name = 'Archivada')`; + } + + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByIdWithRelations( + id: number, + includeArchived: boolean = false, + ): Promise | null> { + const property = await PropertyModel.findById(id, includeArchived); + if (!property || !property.id) { + return null; + } + + const { PropertyAddressModel } = await import("./property-address.model"); + const { AddressModel } = await import("./address.model"); + const propertyAddresses = await PropertyAddressModel.findByPropertyId( + property.id, + ); + const addresses = await Promise.all( + propertyAddresses.map((pa) => AddressModel.findById(pa.address_id)), + ); + + const { PropertyPriceModel } = await import("./property-price.model"); + const prices = await PropertyPriceModel.findByPropertyId(property.id); + + const { PropertyMultimediaModel } = await import( + "./property-multimedia.model" + ); + const images = await PropertyMultimediaModel.findByPropertyId(property.id); + + return { + ...property, + addresses: addresses.filter((a) => a !== null), + prices, + images, + }; + } + + static async findAll( + filters?: PropertyFilters, + ): Promise[]> { + const client = PostgresDatabase.getClient(); + + let query = ` + SELECT p.*, -- Catálogos principales pt.name as property_type_name, @@ -247,59 +286,59 @@ export class PropertyModel { c.email as owner_email, c.phone as owner_phone, -- Imagen principal (subquery) - (SELECT file_path FROM property_multimedia - WHERE property_id = p.id AND is_primary = true + (SELECT file_path FROM property_multimedia + WHERE property_id = p.id AND is_primary = true LIMIT 1) as primary_image_path, - (SELECT id FROM property_multimedia - WHERE property_id = p.id AND is_primary = true + (SELECT id FROM property_multimedia + WHERE property_id = p.id AND is_primary = true LIMIT 1) as primary_image_id, -- Precio principal (más reciente de Venta, o el más reciente) - (SELECT price FROM property_prices - WHERE property_id = p.id - ORDER BY + (SELECT price FROM property_prices + WHERE property_id = p.id + ORDER BY CASE WHEN operation_type_id = (SELECT id FROM property_operation_types WHERE name = 'Venta' LIMIT 1) THEN 0 ELSE 1 END, - updated_at DESC + updated_at DESC LIMIT 1) as main_price, - (SELECT currency_type_id FROM property_prices - WHERE property_id = p.id - ORDER BY + (SELECT currency_type_id FROM property_prices + WHERE property_id = p.id + ORDER BY CASE WHEN operation_type_id = (SELECT id FROM property_operation_types WHERE name = 'Venta' LIMIT 1) THEN 0 ELSE 1 END, - updated_at DESC + updated_at DESC LIMIT 1) as main_currency_type_id, - (SELECT operation_type_id FROM property_prices - WHERE property_id = p.id - ORDER BY + (SELECT operation_type_id FROM property_prices + WHERE property_id = p.id + ORDER BY CASE WHEN operation_type_id = (SELECT id FROM property_operation_types WHERE name = 'Venta' LIMIT 1) THEN 0 ELSE 1 END, - updated_at DESC + updated_at DESC LIMIT 1) as main_operation_type_id, -- Dirección principal - (SELECT a.full_address FROM property_addresses pa_addr - JOIN addresses a ON pa_addr.address_id = a.id - WHERE pa_addr.property_id = p.id + (SELECT a.full_address FROM property_addresses pa_addr + JOIN addresses a ON pa_addr.address_id = a.id + WHERE pa_addr.property_id = p.id LIMIT 1) as main_address, - (SELECT a.neighborhood FROM property_addresses pa_addr - JOIN addresses a ON pa_addr.address_id = a.id - WHERE pa_addr.property_id = p.id + (SELECT a.neighborhood FROM property_addresses pa_addr + JOIN addresses a ON pa_addr.address_id = a.id + WHERE pa_addr.property_id = p.id LIMIT 1) as main_neighborhood, - (SELECT ci.name FROM property_addresses pa_addr - JOIN addresses a ON pa_addr.address_id = a.id - JOIN cities ci ON a.city_id = ci.id - WHERE pa_addr.property_id = p.id + (SELECT ci.name FROM property_addresses pa_addr + JOIN addresses a ON pa_addr.address_id = a.id + JOIN cities ci ON a.city_id = ci.id + WHERE pa_addr.property_id = p.id LIMIT 1) as main_city_name, - (SELECT ci.id FROM property_addresses pa_addr - JOIN addresses a ON pa_addr.address_id = a.id - JOIN cities ci ON a.city_id = ci.id - WHERE pa_addr.property_id = p.id + (SELECT ci.id FROM property_addresses pa_addr + JOIN addresses a ON pa_addr.address_id = a.id + JOIN cities ci ON a.city_id = ci.id + WHERE pa_addr.property_id = p.id LIMIT 1) as main_city_id, - (SELECT pr.name FROM property_addresses pa_addr - JOIN addresses a ON pa_addr.address_id = a.id - JOIN cities ci ON a.city_id = ci.id - JOIN provinces pr ON ci.province_id = pr.id - WHERE pa_addr.property_id = p.id + (SELECT pr.name FROM property_addresses pa_addr + JOIN addresses a ON pa_addr.address_id = a.id + JOIN cities ci ON a.city_id = ci.id + JOIN provinces pr ON ci.province_id = pr.id + WHERE pa_addr.property_id = p.id LIMIT 1) as main_province_name, -- Contador de imágenes (SELECT COUNT(*) FROM property_multimedia WHERE property_id = p.id) as images_count - FROM ${this.TABLE_NAME} p + FROM ${PropertyModel.TABLE_NAME} p LEFT JOIN property_types pt ON p.property_type_id = pt.id LEFT JOIN property_statuses ps ON p.property_status_id = ps.id LEFT JOIN visibility_statuses vs ON p.visibility_status_id = vs.id @@ -309,224 +348,250 @@ export class PropertyModel { LEFT JOIN property_situations psit ON p.situation_id = psit.id LEFT JOIN clients c ON p.owner_id = c.id `; - - const conditions: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - let needsPriceJoin = false; - if (filters?.min_price || filters?.max_price || filters?.operation_type_id || filters?.currency_type_id) { - query += ` LEFT JOIN property_prices pp ON p.id = pp.property_id`; - needsPriceJoin = true; - } - - let needsAddressJoin = false; - if (filters?.city_id) { - query += ` LEFT JOIN property_addresses pa ON p.id = pa.property_id LEFT JOIN addresses a ON pa.address_id = a.id`; - needsAddressJoin = true; - } - - if (filters) { - if (filters.property_type_id !== undefined) { - conditions.push(`p.property_type_id = $${paramIndex++}`); - values.push(filters.property_type_id); - } - if (filters.property_status_id !== undefined) { - conditions.push(`p.property_status_id = $${paramIndex++}`); - values.push(filters.property_status_id); - } - if (filters.visibility_status_id !== undefined) { - conditions.push(`p.visibility_status_id = $${paramIndex++}`); - values.push(filters.visibility_status_id); - } - if (filters.owner_id !== undefined) { - conditions.push(`p.owner_id = $${paramIndex++}`); - values.push(filters.owner_id); - } - if (filters.captured_by_user_id !== undefined) { - conditions.push(`p.captured_by_user_id = $${paramIndex++}`); - values.push(filters.captured_by_user_id); - } - if (filters.city_id !== undefined) { - conditions.push(`a.city_id = $${paramIndex++}`); - values.push(filters.city_id); - } - if (filters.featured_web !== undefined) { - conditions.push(`p.featured_web = $${paramIndex++}`); - values.push(filters.featured_web); - } - if (filters.min_price !== undefined) { - conditions.push(`pp.price >= $${paramIndex++}`); - values.push(filters.min_price); - } - if (filters.max_price !== undefined) { - conditions.push(`pp.price <= $${paramIndex++}`); - values.push(filters.max_price); - } - if (filters.operation_type_id !== undefined) { - conditions.push(`pp.operation_type_id = $${paramIndex++}`); - values.push(filters.operation_type_id); - } - if (filters.currency_type_id !== undefined) { - conditions.push(`pp.currency_type_id = $${paramIndex++}`); - values.push(filters.currency_type_id); - } - if (filters.search) { - conditions.push(`(p.title ILIKE $${paramIndex} OR p.description ILIKE $${paramIndex})`); - values.push(`%${filters.search}%`); - paramIndex++; - } - } - - if (!filters?.includeArchived) { - conditions.push(`p.visibility_status_id != (SELECT id FROM visibility_statuses WHERE name = 'Archivada')`); - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}`; - } - - if (needsPriceJoin || needsAddressJoin) { - query += ` GROUP BY p.id, pt.name, ps.name, vs.name, pa_cat.name, o.name, d.name, psit.name, c.first_name, c.last_name, c.email, c.phone`; - } - - query += ` ORDER BY p.publication_date DESC, p.updated_at DESC`; - - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } - - const result = await client.query(query, values); - return result.rows; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - const addField = (field: string, value: any) => { - if (value !== undefined) { - fields.push(`${field} = $${paramIndex++}`); - values.push(value); - } - }; - - addField('title', updateData.title); - addField('description', updateData.description); - addField('publication_date', updateData.publication_date); - addField('featured_web', updateData.featured_web); - addField('visibility_status_id', updateData.visibility_status_id); - addField('captured_by_user_id', updateData.captured_by_user_id); - addField('branch_name', updateData.branch_name); - addField('appraiser', updateData.appraiser); - addField('producer', updateData.producer); - addField('maintenance_user', updateData.maintenance_user); - addField('keys_location', updateData.keys_location); - addField('internal_comments', updateData.internal_comments); - addField('social_media_info', updateData.social_media_info); - addField('operation_commission_percentage', updateData.operation_commission_percentage); - addField('producer_commission_percentage', updateData.producer_commission_percentage); - addField('land_area', updateData.land_area); - addField('semi_covered_area', updateData.semi_covered_area); - addField('covered_area', updateData.covered_area); - addField('total_built_area', updateData.total_built_area); - addField('uncovered_area', updateData.uncovered_area); - addField('total_area', updateData.total_area); - addField('rooms_count', updateData.rooms_count); - addField('bedrooms_count', updateData.bedrooms_count); - addField('bathrooms_count', updateData.bathrooms_count); - addField('toilets_count', updateData.toilets_count); - addField('parking_spaces_count', updateData.parking_spaces_count); - addField('floors_count', updateData.floors_count); - addField('zoning', updateData.zoning); - addField('property_type_id', updateData.property_type_id); - addField('property_status_id', updateData.property_status_id); - addField('owner_id', updateData.owner_id); - addField('situation_id', updateData.situation_id); - addField('age_id', updateData.age_id); - addField('orientation_id', updateData.orientation_id); - addField('disposition_id', updateData.disposition_id); - - if (fields.length === 0) { - return await this.findById(id); - } - - fields.push(`updated_at = CURRENT_TIMESTAMP`); - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + + const conditions: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + let needsPriceJoin = false; + if ( + filters?.min_price || + filters?.max_price || + filters?.operation_type_id || + filters?.currency_type_id + ) { + query += ` LEFT JOIN property_prices pp ON p.id = pp.property_id`; + needsPriceJoin = true; + } + + let needsAddressJoin = false; + if (filters?.city_id) { + query += ` LEFT JOIN property_addresses pa ON p.id = pa.property_id LEFT JOIN addresses a ON pa.address_id = a.id`; + needsAddressJoin = true; + } + + if (filters) { + if (filters.property_type_id !== undefined) { + conditions.push(`p.property_type_id = $${paramIndex++}`); + values.push(filters.property_type_id); + } + if (filters.property_status_id !== undefined) { + conditions.push(`p.property_status_id = $${paramIndex++}`); + values.push(filters.property_status_id); + } + if (filters.visibility_status_id !== undefined) { + conditions.push(`p.visibility_status_id = $${paramIndex++}`); + values.push(filters.visibility_status_id); + } + if (filters.owner_id !== undefined) { + conditions.push(`p.owner_id = $${paramIndex++}`); + values.push(filters.owner_id); + } + if (filters.captured_by_user_id !== undefined) { + conditions.push(`p.captured_by_user_id = $${paramIndex++}`); + values.push(filters.captured_by_user_id); + } + if (filters.city_id !== undefined) { + conditions.push(`a.city_id = $${paramIndex++}`); + values.push(filters.city_id); + } + if (filters.featured_web !== undefined) { + conditions.push(`p.featured_web = $${paramIndex++}`); + values.push(filters.featured_web); + } + if (filters.min_price !== undefined) { + conditions.push(`pp.price >= $${paramIndex++}`); + values.push(filters.min_price); + } + if (filters.max_price !== undefined) { + conditions.push(`pp.price <= $${paramIndex++}`); + values.push(filters.max_price); + } + if (filters.operation_type_id !== undefined) { + conditions.push(`pp.operation_type_id = $${paramIndex++}`); + values.push(filters.operation_type_id); + } + if (filters.currency_type_id !== undefined) { + conditions.push(`pp.currency_type_id = $${paramIndex++}`); + values.push(filters.currency_type_id); + } + if (filters.search) { + conditions.push( + `(p.title ILIKE $${paramIndex} OR p.description ILIKE $${paramIndex})`, + ); + values.push(`%${filters.search}%`); + paramIndex++; + } + } + + if (!filters?.includeArchived) { + conditions.push( + `p.visibility_status_id != (SELECT id FROM visibility_statuses WHERE name = 'Archivada')`, + ); + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + if (needsPriceJoin || needsAddressJoin) { + query += ` GROUP BY p.id, pt.name, ps.name, vs.name, pa_cat.name, o.name, d.name, psit.name, c.first_name, c.last_name, c.email, c.phone`; + } + + query += ` ORDER BY p.publication_date DESC, p.updated_at DESC`; + + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } + + const result = await client.query(query, values); + return result.rows; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + const addField = ( + field: string, + value: string | number | boolean | Date | null | undefined, + ) => { + if (value !== undefined) { + fields.push(`${field} = $${paramIndex++}`); + values.push(value); + } + }; + + addField("title", updateData.title); + addField("description", updateData.description); + addField("publication_date", updateData.publication_date); + addField("featured_web", updateData.featured_web); + addField("visibility_status_id", updateData.visibility_status_id); + addField("captured_by_user_id", updateData.captured_by_user_id); + addField("branch_name", updateData.branch_name); + addField("appraiser", updateData.appraiser); + addField("producer", updateData.producer); + addField("maintenance_user", updateData.maintenance_user); + addField("keys_location", updateData.keys_location); + addField("internal_comments", updateData.internal_comments); + addField("social_media_info", updateData.social_media_info); + addField( + "operation_commission_percentage", + updateData.operation_commission_percentage, + ); + addField( + "producer_commission_percentage", + updateData.producer_commission_percentage, + ); + addField("land_area", updateData.land_area); + addField("semi_covered_area", updateData.semi_covered_area); + addField("covered_area", updateData.covered_area); + addField("total_built_area", updateData.total_built_area); + addField("uncovered_area", updateData.uncovered_area); + addField("total_area", updateData.total_area); + addField("rooms_count", updateData.rooms_count); + addField("bedrooms_count", updateData.bedrooms_count); + addField("bathrooms_count", updateData.bathrooms_count); + addField("toilets_count", updateData.toilets_count); + addField("parking_spaces_count", updateData.parking_spaces_count); + addField("floors_count", updateData.floors_count); + addField("zoning", updateData.zoning); + addField("property_type_id", updateData.property_type_id); + addField("property_status_id", updateData.property_status_id); + addField("owner_id", updateData.owner_id); + addField("situation_id", updateData.situation_id); + addField("age_id", updateData.age_id); + addField("orientation_id", updateData.orientation_id); + addField("disposition_id", updateData.disposition_id); + + if (fields.length === 0) { + return await PropertyModel.findById(id); + } + + fields.push(`updated_at = CURRENT_TIMESTAMP`); + values.push(id); + + const query = ` + UPDATE ${PropertyModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } - - static async archive(id: number): Promise { - const client = PostgresDatabase.getClient(); - - const statusQuery = `SELECT id FROM visibility_statuses WHERE name = 'Archivada' LIMIT 1`; - const statusResult = await client.query(statusQuery); - - if (statusResult.rows.length === 0) { - throw new Error('Visibility status "Archivada" not found. Run: npm run db:seed-catalogs'); - } - - const archivedStatusId = statusResult.rows[0].id; - - const query = ` - UPDATE ${this.TABLE_NAME} - SET visibility_status_id = $1, updated_at = CURRENT_TIMESTAMP - WHERE id = $2 + const result = await client.query(query, values); + return result.rows[0] || null; + } + + static async archive(id: number): Promise { + const client = PostgresDatabase.getClient(); + + const statusQuery = `SELECT id FROM visibility_statuses WHERE name = 'Archivada' LIMIT 1`; + const statusResult = await client.query(statusQuery); + + if (statusResult.rows.length === 0) { + throw new Error( + 'Visibility status "Archivada" not found. Run: npm run db:seed-catalogs', + ); + } + + const archivedStatusId = statusResult.rows[0].id; + + const query = ` + UPDATE ${PropertyModel.TABLE_NAME} + SET visibility_status_id = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2 RETURNING id `; - const result = await client.query(query, [archivedStatusId, id]); - return (result.rowCount ?? 0) > 0; - } - - - static async unarchive(id: number, newVisibilityStatusId?: number): Promise { - const client = PostgresDatabase.getClient(); - - let visibilityStatusId = newVisibilityStatusId; - - if (!visibilityStatusId) { - const statusQuery = `SELECT id FROM visibility_statuses WHERE name = 'Publicado' LIMIT 1`; - const statusResult = await client.query(statusQuery); - - if (statusResult.rows.length === 0) { - throw new Error('Visibility status "Publicado" not found. Run: npm run db:seed-catalogs'); - } - - visibilityStatusId = statusResult.rows[0].id; - } - - const query = ` - UPDATE ${this.TABLE_NAME} - SET visibility_status_id = $1, updated_at = CURRENT_TIMESTAMP - WHERE id = $2 + const result = await client.query(query, [archivedStatusId, id]); + return (result.rowCount ?? 0) > 0; + } + + static async unarchive( + id: number, + newVisibilityStatusId?: number, + ): Promise { + const client = PostgresDatabase.getClient(); + + let visibilityStatusId = newVisibilityStatusId; + + if (!visibilityStatusId) { + const statusQuery = `SELECT id FROM visibility_statuses WHERE name = 'Publicado' LIMIT 1`; + const statusResult = await client.query(statusQuery); + + if (statusResult.rows.length === 0) { + throw new Error( + 'Visibility status "Publicado" not found. Run: npm run db:seed-catalogs', + ); + } + + visibilityStatusId = statusResult.rows[0].id; + } + + const query = ` + UPDATE ${PropertyModel.TABLE_NAME} + SET visibility_status_id = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2 RETURNING id `; - const result = await client.query(query, [visibilityStatusId, id]); - return (result.rowCount ?? 0) > 0; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + const result = await client.query(query, [visibilityStatusId, id]); + return (result.rowCount ?? 0) > 0; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/properties/visibility-status.model.ts b/backend/src/data/postgres/models/properties/visibility-status.model.ts index ec3461f..82bd574 100644 --- a/backend/src/data/postgres/models/properties/visibility-status.model.ts +++ b/backend/src/data/postgres/models/properties/visibility-status.model.ts @@ -1,60 +1,64 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface VisibilityStatus { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateVisibilityStatusDto { - name: string; + name: string; } export class VisibilityStatusModel { - private static readonly TABLE_NAME = 'visibility_statuses'; - - static async create(data: CreateVisibilityStatusDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - return result.rows[0]; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - return result.rows[0] || null; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + private static readonly TABLE_NAME = "visibility_statuses"; + + static async create( + data: CreateVisibilityStatusDto, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${VisibilityStatusModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + return result.rows[0]; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${VisibilityStatusModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${VisibilityStatusModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${VisibilityStatusModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await VisibilityStatusModel.findById(id); + } + const query = `UPDATE ${VisibilityStatusModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + return result.rows[0] || null; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${VisibilityStatusModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/rentals/client-rental.model.ts b/backend/src/data/postgres/models/rentals/client-rental.model.ts index 227995f..b1424ed 100644 --- a/backend/src/data/postgres/models/rentals/client-rental.model.ts +++ b/backend/src/data/postgres/models/rentals/client-rental.model.ts @@ -1,133 +1,141 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface ClientRental { - id?: number; - client_id: number; - property_id: number; - external_reference?: string; - contract_start_date?: Date; - contract_end_date?: Date; - next_increase_date?: Date; - remind_increase?: boolean; - remind_contract_end?: boolean; + id?: number; + client_id: number; + property_id: number; + external_reference?: string; + contract_start_date?: Date; + contract_end_date?: Date; + next_increase_date?: Date; + remind_increase?: boolean; + remind_contract_end?: boolean; } export interface CreateClientRentalDto { - client_id: number; - property_id: number; - external_reference?: string; - contract_start_date?: Date; - contract_end_date?: Date; - next_increase_date?: Date; - remind_increase?: boolean; - remind_contract_end?: boolean; + client_id: number; + property_id: number; + external_reference?: string; + contract_start_date?: Date; + contract_end_date?: Date; + next_increase_date?: Date; + remind_increase?: boolean; + remind_contract_end?: boolean; } export class ClientRentalModel { - private static readonly TABLE_NAME = 'client_rentals'; + private static readonly TABLE_NAME = "client_rentals"; - static async create(rentalData: CreateClientRentalDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create( + rentalData: CreateClientRentalDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${ClientRentalModel.TABLE_NAME} ( client_id, property_id, external_reference, contract_start_date, contract_end_date, next_increase_date, remind_increase, remind_contract_end ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * `; - - const result = await client.query(query, [ - rentalData.client_id, - rentalData.property_id, - rentalData.external_reference || null, - rentalData.contract_start_date || null, - rentalData.contract_end_date || null, - rentalData.next_increase_date || null, - rentalData.remind_increase !== undefined ? rentalData.remind_increase : false, - rentalData.remind_contract_end !== undefined ? rentalData.remind_contract_end : false, - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByClientId(clientId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE client_id = $1 ORDER BY contract_start_date DESC`; - const result = await client.query(query, [clientId]); - return result.rows; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY contract_start_date DESC`; - const result = await client.query(query, [propertyId]); - return result.rows; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.external_reference !== undefined) { - fields.push(`external_reference = $${paramIndex++}`); - values.push(updateData.external_reference || null); - } - if (updateData.contract_start_date !== undefined) { - fields.push(`contract_start_date = $${paramIndex++}`); - values.push(updateData.contract_start_date || null); - } - if (updateData.contract_end_date !== undefined) { - fields.push(`contract_end_date = $${paramIndex++}`); - values.push(updateData.contract_end_date || null); - } - if (updateData.next_increase_date !== undefined) { - fields.push(`next_increase_date = $${paramIndex++}`); - values.push(updateData.next_increase_date || null); - } - if (updateData.remind_increase !== undefined) { - fields.push(`remind_increase = $${paramIndex++}`); - values.push(updateData.remind_increase); - } - if (updateData.remind_contract_end !== undefined) { - fields.push(`remind_contract_end = $${paramIndex++}`); - values.push(updateData.remind_contract_end); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + + const result = await client.query(query, [ + rentalData.client_id, + rentalData.property_id, + rentalData.external_reference || null, + rentalData.contract_start_date || null, + rentalData.contract_end_date || null, + rentalData.next_increase_date || null, + rentalData.remind_increase !== undefined + ? rentalData.remind_increase + : false, + rentalData.remind_contract_end !== undefined + ? rentalData.remind_contract_end + : false, + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientRentalModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByClientId(clientId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientRentalModel.TABLE_NAME} WHERE client_id = $1 ORDER BY contract_start_date DESC`; + const result = await client.query(query, [clientId]); + return result.rows; + } + + static async findByPropertyId(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ClientRentalModel.TABLE_NAME} WHERE property_id = $1 ORDER BY contract_start_date DESC`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.external_reference !== undefined) { + fields.push(`external_reference = $${paramIndex++}`); + values.push(updateData.external_reference || null); + } + if (updateData.contract_start_date !== undefined) { + fields.push(`contract_start_date = $${paramIndex++}`); + values.push(updateData.contract_start_date || null); + } + if (updateData.contract_end_date !== undefined) { + fields.push(`contract_end_date = $${paramIndex++}`); + values.push(updateData.contract_end_date || null); + } + if (updateData.next_increase_date !== undefined) { + fields.push(`next_increase_date = $${paramIndex++}`); + values.push(updateData.next_increase_date || null); + } + if (updateData.remind_increase !== undefined) { + fields.push(`remind_increase = $${paramIndex++}`); + values.push(updateData.remind_increase); + } + if (updateData.remind_contract_end !== undefined) { + fields.push(`remind_contract_end = $${paramIndex++}`); + values.push(updateData.remind_contract_end); + } + + if (fields.length === 0) { + return await ClientRentalModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${ClientRentalModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ClientRentalModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/rentals/index.ts b/backend/src/data/postgres/models/rentals/index.ts index c469d05..4924fd8 100644 --- a/backend/src/data/postgres/models/rentals/index.ts +++ b/backend/src/data/postgres/models/rentals/index.ts @@ -1,4 +1,3 @@ -export * from './client-rental.model'; -export * from './payment.model'; -export * from './rental.model'; - +export * from "./client-rental.model"; +export * from "./payment.model"; +export * from "./rental.model"; diff --git a/backend/src/data/postgres/models/rentals/payment.model.ts b/backend/src/data/postgres/models/rentals/payment.model.ts index dfae4c4..b019cde 100644 --- a/backend/src/data/postgres/models/rentals/payment.model.ts +++ b/backend/src/data/postgres/models/rentals/payment.model.ts @@ -1,169 +1,171 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface Payment { - id?: number; - rental_id: number; - payment_date: Date; - amount: number; - reference?: string; - payment_method_id: number; - payment_status_id: number; + id?: number; + rental_id: number; + payment_date: Date; + amount: number; + reference?: string; + payment_method_id: number; + payment_status_id: number; } export interface CreatePaymentDto { - rental_id: number; - payment_date: Date; - amount: number; - reference?: string; - payment_method_id: number; - payment_status_id: number; + rental_id: number; + payment_date: Date; + amount: number; + reference?: string; + payment_method_id: number; + payment_status_id: number; } export interface PaymentFilters { - rental_id?: number; - payment_status_id?: number; - start_date?: Date; - end_date?: Date; - limit?: number; - offset?: number; + rental_id?: number; + payment_status_id?: number; + start_date?: Date; + end_date?: Date; + limit?: number; + offset?: number; } export class PaymentModel { - private static readonly TABLE_NAME = 'payments'; + private static readonly TABLE_NAME = "payments"; - static async create(paymentData: CreatePaymentDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create(paymentData: CreatePaymentDto): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PaymentModel.TABLE_NAME} ( rental_id, payment_date, amount, reference, payment_method_id, payment_status_id ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; - - const result = await client.query(query, [ - paymentData.rental_id, - paymentData.payment_date, - paymentData.amount, - paymentData.reference || null, - paymentData.payment_method_id, - paymentData.payment_status_id, - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByRentalId(rentalId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE rental_id = $1 ORDER BY payment_date DESC`; - const result = await client.query(query, [rentalId]); - return result.rows; - } - - static async findAll(filters?: PaymentFilters): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME}`; - const conditions: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (filters) { - if (filters.rental_id !== undefined) { - conditions.push(`rental_id = $${paramIndex++}`); - values.push(filters.rental_id); - } - if (filters.payment_status_id !== undefined) { - conditions.push(`payment_status_id = $${paramIndex++}`); - values.push(filters.payment_status_id); - } - if (filters.start_date) { - conditions.push(`payment_date >= $${paramIndex++}`); - values.push(filters.start_date); - } - if (filters.end_date) { - conditions.push(`payment_date <= $${paramIndex++}`); - values.push(filters.end_date); - } - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}`; - } - - query += ` ORDER BY payment_date DESC`; - - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } - - const result = await client.query(query, values); - return result.rows; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.payment_date) { - fields.push(`payment_date = $${paramIndex++}`); - values.push(updateData.payment_date); - } - if (updateData.amount !== undefined) { - fields.push(`amount = $${paramIndex++}`); - values.push(updateData.amount); - } - if (updateData.reference !== undefined) { - fields.push(`reference = $${paramIndex++}`); - values.push(updateData.reference || null); - } - if (updateData.payment_method_id !== undefined) { - fields.push(`payment_method_id = $${paramIndex++}`); - values.push(updateData.payment_method_id); - } - if (updateData.payment_status_id !== undefined) { - fields.push(`payment_status_id = $${paramIndex++}`); - values.push(updateData.payment_status_id); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + + const result = await client.query(query, [ + paymentData.rental_id, + paymentData.payment_date, + paymentData.amount, + paymentData.reference || null, + paymentData.payment_method_id, + paymentData.payment_status_id, + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PaymentModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByRentalId(rentalId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PaymentModel.TABLE_NAME} WHERE rental_id = $1 ORDER BY payment_date DESC`; + const result = await client.query(query, [rentalId]); + return result.rows; + } + + static async findAll(filters?: PaymentFilters): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${PaymentModel.TABLE_NAME}`; + const conditions: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (filters) { + if (filters.rental_id !== undefined) { + conditions.push(`rental_id = $${paramIndex++}`); + values.push(filters.rental_id); + } + if (filters.payment_status_id !== undefined) { + conditions.push(`payment_status_id = $${paramIndex++}`); + values.push(filters.payment_status_id); + } + if (filters.start_date) { + conditions.push(`payment_date >= $${paramIndex++}`); + values.push(filters.start_date); + } + if (filters.end_date) { + conditions.push(`payment_date <= $${paramIndex++}`); + values.push(filters.end_date); + } + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += ` ORDER BY payment_date DESC`; + + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } + + const result = await client.query(query, values); + return result.rows; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.payment_date) { + fields.push(`payment_date = $${paramIndex++}`); + values.push(updateData.payment_date); + } + if (updateData.amount !== undefined) { + fields.push(`amount = $${paramIndex++}`); + values.push(updateData.amount); + } + if (updateData.reference !== undefined) { + fields.push(`reference = $${paramIndex++}`); + values.push(updateData.reference || null); + } + if (updateData.payment_method_id !== undefined) { + fields.push(`payment_method_id = $${paramIndex++}`); + values.push(updateData.payment_method_id); + } + if (updateData.payment_status_id !== undefined) { + fields.push(`payment_status_id = $${paramIndex++}`); + values.push(updateData.payment_status_id); + } + + if (fields.length === 0) { + return await PaymentModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${PaymentModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PaymentModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/rentals/rental.model.ts b/backend/src/data/postgres/models/rentals/rental.model.ts index de903d8..5f4b7ee 100644 --- a/backend/src/data/postgres/models/rentals/rental.model.ts +++ b/backend/src/data/postgres/models/rentals/rental.model.ts @@ -1,50 +1,50 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface Rental { - id?: number; - property_id: number; - client_rental_id: number; - start_date: Date; - end_date?: Date; - next_increase_date?: Date; - monthly_amount: number; - currency_type_id: number; - created_by_user_id: number; - remind_increase?: boolean; - remind_contract_end?: boolean; + id?: number; + property_id: number; + client_rental_id: number; + start_date: Date; + end_date?: Date; + next_increase_date?: Date; + monthly_amount: number; + currency_type_id: number; + created_by_user_id: number; + remind_increase?: boolean; + remind_contract_end?: boolean; } export interface CreateRentalDto { - property_id: number; - client_rental_id: number; - start_date: Date; - end_date?: Date; - next_increase_date?: Date; - monthly_amount: number; - currency_type_id: number; - created_by_user_id: number; - remind_increase?: boolean; - remind_contract_end?: boolean; + property_id: number; + client_rental_id: number; + start_date: Date; + end_date?: Date; + next_increase_date?: Date; + monthly_amount: number; + currency_type_id: number; + created_by_user_id: number; + remind_increase?: boolean; + remind_contract_end?: boolean; } export interface RentalFilters { - property_id?: number; - client_rental_id?: number; - active?: boolean; - created_by_user_id?: number; - limit?: number; - offset?: number; + property_id?: number; + client_rental_id?: number; + active?: boolean; + created_by_user_id?: number; + limit?: number; + offset?: number; } export class RentalModel { - private static readonly TABLE_NAME = 'rentals'; + private static readonly TABLE_NAME = "rentals"; - static async create(rentalData: CreateRentalDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create(rentalData: CreateRentalDto): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${RentalModel.TABLE_NAME} ( property_id, client_rental_id, start_date, end_date, next_increase_date, monthly_amount, currency_type_id, created_by_user_id, remind_increase, remind_contract_end @@ -52,148 +52,156 @@ export class RentalModel { VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING * `; - - const result = await client.query(query, [ - rentalData.property_id, - rentalData.client_rental_id, - rentalData.start_date, - rentalData.end_date || null, - rentalData.next_increase_date || null, - rentalData.monthly_amount, - rentalData.currency_type_id, - rentalData.created_by_user_id, - rentalData.remind_increase !== undefined ? rentalData.remind_increase : false, - rentalData.remind_contract_end !== undefined ? rentalData.remind_contract_end : false, - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findAll(filters?: RentalFilters): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME}`; - const conditions: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (filters) { - if (filters.property_id !== undefined) { - conditions.push(`property_id = $${paramIndex++}`); - values.push(filters.property_id); - } - if (filters.client_rental_id !== undefined) { - conditions.push(`client_rental_id = $${paramIndex++}`); - values.push(filters.client_rental_id); - } - if (filters.active !== undefined) { - if (filters.active) { - conditions.push(`(end_date IS NULL OR end_date >= CURRENT_DATE)`); - } else { - conditions.push(`end_date IS NOT NULL AND end_date < CURRENT_DATE`); - } - } - if (filters.created_by_user_id !== undefined) { - conditions.push(`created_by_user_id = $${paramIndex++}`); - values.push(filters.created_by_user_id); - } - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}`; - } - - query += ` ORDER BY start_date DESC`; - - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } - - const result = await client.query(query, values); - return result.rows; - } - - static async findActiveByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = ` - SELECT * FROM ${this.TABLE_NAME} + + const result = await client.query(query, [ + rentalData.property_id, + rentalData.client_rental_id, + rentalData.start_date, + rentalData.end_date || null, + rentalData.next_increase_date || null, + rentalData.monthly_amount, + rentalData.currency_type_id, + rentalData.created_by_user_id, + rentalData.remind_increase !== undefined + ? rentalData.remind_increase + : false, + rentalData.remind_contract_end !== undefined + ? rentalData.remind_contract_end + : false, + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${RentalModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findAll(filters?: RentalFilters): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${RentalModel.TABLE_NAME}`; + const conditions: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (filters) { + if (filters.property_id !== undefined) { + conditions.push(`property_id = $${paramIndex++}`); + values.push(filters.property_id); + } + if (filters.client_rental_id !== undefined) { + conditions.push(`client_rental_id = $${paramIndex++}`); + values.push(filters.client_rental_id); + } + if (filters.active !== undefined) { + if (filters.active) { + conditions.push(`(end_date IS NULL OR end_date >= CURRENT_DATE)`); + } else { + conditions.push(`end_date IS NOT NULL AND end_date < CURRENT_DATE`); + } + } + if (filters.created_by_user_id !== undefined) { + conditions.push(`created_by_user_id = $${paramIndex++}`); + values.push(filters.created_by_user_id); + } + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += ` ORDER BY start_date DESC`; + + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } + + const result = await client.query(query, values); + return result.rows; + } + + static async findActiveByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = ` + SELECT * FROM ${RentalModel.TABLE_NAME} WHERE property_id = $1 AND (end_date IS NULL OR end_date >= CURRENT_DATE) ORDER BY start_date DESC LIMIT 1 `; - const result = await client.query(query, [propertyId]); - return result.rows[0] || null; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.start_date) { - fields.push(`start_date = $${paramIndex++}`); - values.push(updateData.start_date); - } - if (updateData.end_date !== undefined) { - fields.push(`end_date = $${paramIndex++}`); - values.push(updateData.end_date || null); - } - if (updateData.next_increase_date !== undefined) { - fields.push(`next_increase_date = $${paramIndex++}`); - values.push(updateData.next_increase_date || null); - } - if (updateData.monthly_amount !== undefined) { - fields.push(`monthly_amount = $${paramIndex++}`); - values.push(updateData.monthly_amount); - } - if (updateData.currency_type_id !== undefined) { - fields.push(`currency_type_id = $${paramIndex++}`); - values.push(updateData.currency_type_id); - } - if (updateData.remind_increase !== undefined) { - fields.push(`remind_increase = $${paramIndex++}`); - values.push(updateData.remind_increase); - } - if (updateData.remind_contract_end !== undefined) { - fields.push(`remind_contract_end = $${paramIndex++}`); - values.push(updateData.remind_contract_end); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + const result = await client.query(query, [propertyId]); + return result.rows[0] || null; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.start_date) { + fields.push(`start_date = $${paramIndex++}`); + values.push(updateData.start_date); + } + if (updateData.end_date !== undefined) { + fields.push(`end_date = $${paramIndex++}`); + values.push(updateData.end_date || null); + } + if (updateData.next_increase_date !== undefined) { + fields.push(`next_increase_date = $${paramIndex++}`); + values.push(updateData.next_increase_date || null); + } + if (updateData.monthly_amount !== undefined) { + fields.push(`monthly_amount = $${paramIndex++}`); + values.push(updateData.monthly_amount); + } + if (updateData.currency_type_id !== undefined) { + fields.push(`currency_type_id = $${paramIndex++}`); + values.push(updateData.currency_type_id); + } + if (updateData.remind_increase !== undefined) { + fields.push(`remind_increase = $${paramIndex++}`); + values.push(updateData.remind_increase); + } + if (updateData.remind_contract_end !== undefined) { + fields.push(`remind_contract_end = $${paramIndex++}`); + values.push(updateData.remind_contract_end); + } + + if (fields.length === 0) { + return await RentalModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${RentalModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${RentalModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/revoked-token.model.ts b/backend/src/data/postgres/models/revoked-token.model.ts index 9ae62a4..1317b79 100644 --- a/backend/src/data/postgres/models/revoked-token.model.ts +++ b/backend/src/data/postgres/models/revoked-token.model.ts @@ -1,77 +1,72 @@ -import { PostgresDatabase } from '../database'; +import { PostgresDatabase } from "../database"; export interface RevokedToken { - id: number; - token_jti: string; - user_id: number; - revoked_at: Date; - expires_at: Date; - reason?: string; - ip_address?: string; - user_agent?: string; + id: number; + token_jti: string; + user_id: number; + revoked_at: Date; + expires_at: Date; + reason?: string; + ip_address?: string; + user_agent?: string; } export interface CreateRevokedTokenDto { - token_jti: string; - user_id: number; - expires_at: Date; - reason?: string; - ip_address?: string; - user_agent?: string; + token_jti: string; + user_id: number; + expires_at: Date; + reason?: string; + ip_address?: string; + user_agent?: string; } export class RevokedTokenModel { - private static readonly TABLE_NAME = 'revoked_tokens'; - - - static async create(data: CreateRevokedTokenDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} + private static readonly TABLE_NAME = "revoked_tokens"; + + static async create(data: CreateRevokedTokenDto): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${RevokedTokenModel.TABLE_NAME} (token_jti, user_id, expires_at, reason, ip_address, user_agent) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; - - const result = await client.query(query, [ - data.token_jti, - data.user_id, - data.expires_at, - data.reason || 'logout', - data.ip_address || null, - data.user_agent || null - ]); - - return result.rows[0]; - } - - - static async isRevoked(jti: string): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - SELECT 1 FROM ${this.TABLE_NAME} - WHERE token_jti = $1 + + const result = await client.query(query, [ + data.token_jti, + data.user_id, + data.expires_at, + data.reason || "logout", + data.ip_address || null, + data.user_agent || null, + ]); + + return result.rows[0]; + } + + static async isRevoked(jti: string): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + SELECT 1 FROM ${RevokedTokenModel.TABLE_NAME} + WHERE token_jti = $1 AND expires_at > NOW() LIMIT 1 `; - - const result = await client.query(query, [jti]); - return result.rows.length > 0; - } - - - static async revokeAllUserTokens( - userId: number, - reason: string = 'security' - ): Promise { - const client = PostgresDatabase.getClient(); - - - - const query = ` - INSERT INTO ${this.TABLE_NAME} (token_jti, user_id, expires_at, reason) + + const result = await client.query(query, [jti]); + return result.rows.length > 0; + } + + static async revokeAllUserTokens( + userId: number, + reason: string = "security", + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${RevokedTokenModel.TABLE_NAME} (token_jti, user_id, expires_at, reason) VALUES ( 'revoke_all_' || gen_random_uuid()::text, $1, @@ -80,60 +75,57 @@ export class RevokedTokenModel { ) RETURNING id `; - - const result = await client.query(query, [userId, reason]); - return result.rowCount || 0; - } - - - static async cleanExpiredTokens(): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - DELETE FROM ${this.TABLE_NAME} + + const result = await client.query(query, [userId, reason]); + return result.rowCount || 0; + } + + static async cleanExpiredTokens(): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + DELETE FROM ${RevokedTokenModel.TABLE_NAME} WHERE expires_at < NOW() `; - - const result = await client.query(query); - return result.rowCount || 0; - } - - - static async getUserRevokedTokens(userId: number): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - SELECT * FROM ${this.TABLE_NAME} + + const result = await client.query(query); + return result.rowCount || 0; + } + + static async getUserRevokedTokens(userId: number): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + SELECT * FROM ${RevokedTokenModel.TABLE_NAME} WHERE user_id = $1 AND expires_at > NOW() ORDER BY revoked_at DESC `; - - const result = await client.query(query, [userId]); - return result.rows; - } - - - static async getStats(): Promise<{ - total: number; - expired: number; - active: number; - }> { - const client = PostgresDatabase.getClient(); - - const query = ` - SELECT + + const result = await client.query(query, [userId]); + return result.rows; + } + + static async getStats(): Promise<{ + total: number; + expired: number; + active: number; + }> { + const client = PostgresDatabase.getClient(); + + const query = ` + SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE expires_at < NOW()) as expired, COUNT(*) FILTER (WHERE expires_at >= NOW()) as active - FROM ${this.TABLE_NAME} + FROM ${RevokedTokenModel.TABLE_NAME} `; - - const result = await client.query(query); - return { - total: parseInt(result.rows[0].total), - expired: parseInt(result.rows[0].expired), - active: parseInt(result.rows[0].active) - }; - } + + const result = await client.query(query); + return { + total: parseInt(result.rows[0].total, 10), + expired: parseInt(result.rows[0].expired, 10), + active: parseInt(result.rows[0].active, 10), + }; + } } diff --git a/backend/src/data/postgres/models/sales/index.ts b/backend/src/data/postgres/models/sales/index.ts index 44b4922..b811ede 100644 --- a/backend/src/data/postgres/models/sales/index.ts +++ b/backend/src/data/postgres/models/sales/index.ts @@ -1,2 +1 @@ -export * from './property-sale.model'; - +export * from "./property-sale.model"; diff --git a/backend/src/data/postgres/models/sales/property-sale.model.ts b/backend/src/data/postgres/models/sales/property-sale.model.ts index 101f54e..5b6f1d2 100644 --- a/backend/src/data/postgres/models/sales/property-sale.model.ts +++ b/backend/src/data/postgres/models/sales/property-sale.model.ts @@ -1,166 +1,170 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface PropertySale { - id?: number; - property_id: number; - buyer_client_id: number; - sale_date: Date; - final_amount: number; - currency_type_id: number; - seller_user_id: number; + id?: number; + property_id: number; + buyer_client_id: number; + sale_date: Date; + final_amount: number; + currency_type_id: number; + seller_user_id: number; } export interface CreatePropertySaleDto { - property_id: number; - buyer_client_id: number; - sale_date: Date; - final_amount: number; - currency_type_id: number; - seller_user_id: number; + property_id: number; + buyer_client_id: number; + sale_date: Date; + final_amount: number; + currency_type_id: number; + seller_user_id: number; } export interface PropertySaleFilters { - property_id?: number; - buyer_client_id?: number; - seller_user_id?: number; - start_date?: Date; - end_date?: Date; - limit?: number; - offset?: number; + property_id?: number; + buyer_client_id?: number; + seller_user_id?: number; + start_date?: Date; + end_date?: Date; + limit?: number; + offset?: number; } export class PropertySaleModel { - private static readonly TABLE_NAME = 'property_sales'; + private static readonly TABLE_NAME = "property_sales"; - static async create(saleData: CreatePropertySaleDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create(saleData: CreatePropertySaleDto): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PropertySaleModel.TABLE_NAME} ( property_id, buyer_client_id, sale_date, final_amount, currency_type_id, seller_user_id ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; - - const result = await client.query(query, [ - saleData.property_id, - saleData.buyer_client_id, - saleData.sale_date, - saleData.final_amount, - saleData.currency_type_id, - saleData.seller_user_id, - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY sale_date DESC LIMIT 1`; - const result = await client.query(query, [propertyId]); - return result.rows[0] || null; - } - - static async findAll(filters?: PropertySaleFilters): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME}`; - const conditions: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (filters) { - if (filters.property_id !== undefined) { - conditions.push(`property_id = $${paramIndex++}`); - values.push(filters.property_id); - } - if (filters.buyer_client_id !== undefined) { - conditions.push(`buyer_client_id = $${paramIndex++}`); - values.push(filters.buyer_client_id); - } - if (filters.seller_user_id !== undefined) { - conditions.push(`seller_user_id = $${paramIndex++}`); - values.push(filters.seller_user_id); - } - if (filters.start_date) { - conditions.push(`sale_date >= $${paramIndex++}`); - values.push(filters.start_date); - } - if (filters.end_date) { - conditions.push(`sale_date <= $${paramIndex++}`); - values.push(filters.end_date); - } - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}`; - } - - query += ` ORDER BY sale_date DESC`; - - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } - - const result = await client.query(query, values); - return result.rows; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.sale_date) { - fields.push(`sale_date = $${paramIndex++}`); - values.push(updateData.sale_date); - } - if (updateData.final_amount !== undefined) { - fields.push(`final_amount = $${paramIndex++}`); - values.push(updateData.final_amount); - } - if (updateData.currency_type_id !== undefined) { - fields.push(`currency_type_id = $${paramIndex++}`); - values.push(updateData.currency_type_id); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + + const result = await client.query(query, [ + saleData.property_id, + saleData.buyer_client_id, + saleData.sale_date, + saleData.final_amount, + saleData.currency_type_id, + saleData.seller_user_id, + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertySaleModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertySaleModel.TABLE_NAME} WHERE property_id = $1 ORDER BY sale_date DESC LIMIT 1`; + const result = await client.query(query, [propertyId]); + return result.rows[0] || null; + } + + static async findAll(filters?: PropertySaleFilters): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${PropertySaleModel.TABLE_NAME}`; + const conditions: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (filters) { + if (filters.property_id !== undefined) { + conditions.push(`property_id = $${paramIndex++}`); + values.push(filters.property_id); + } + if (filters.buyer_client_id !== undefined) { + conditions.push(`buyer_client_id = $${paramIndex++}`); + values.push(filters.buyer_client_id); + } + if (filters.seller_user_id !== undefined) { + conditions.push(`seller_user_id = $${paramIndex++}`); + values.push(filters.seller_user_id); + } + if (filters.start_date) { + conditions.push(`sale_date >= $${paramIndex++}`); + values.push(filters.start_date); + } + if (filters.end_date) { + conditions.push(`sale_date <= $${paramIndex++}`); + values.push(filters.end_date); + } + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += ` ORDER BY sale_date DESC`; + + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } + + const result = await client.query(query, values); + return result.rows; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.sale_date) { + fields.push(`sale_date = $${paramIndex++}`); + values.push(updateData.sale_date); + } + if (updateData.final_amount !== undefined) { + fields.push(`final_amount = $${paramIndex++}`); + values.push(updateData.final_amount); + } + if (updateData.currency_type_id !== undefined) { + fields.push(`currency_type_id = $${paramIndex++}`); + values.push(updateData.currency_type_id); + } + + if (fields.length === 0) { + return await PropertySaleModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${PropertySaleModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertySaleModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/shared/audit-log.model.ts b/backend/src/data/postgres/models/shared/audit-log.model.ts index b740870..7c0ca45 100644 --- a/backend/src/data/postgres/models/shared/audit-log.model.ts +++ b/backend/src/data/postgres/models/shared/audit-log.model.ts @@ -1,144 +1,167 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface AuditLog { - id?: number; - affected_table: string; - affected_record_id: number; - action: string; - previous_data?: Record; - new_data?: Record; - changed_at?: Date; - user_id: number; + id?: number; + affected_table: string; + affected_record_id: number; + action: string; + previous_data?: Record; + new_data?: Record; + changed_at?: Date; + user_id: number; } export interface CreateAuditLogDto { - affected_table: string; - affected_record_id: number; - action: string; - previous_data?: Record; - new_data?: Record; - user_id: number; + affected_table: string; + affected_record_id: number; + action: string; + previous_data?: Record; + new_data?: Record; + user_id: number; } export interface AuditLogFilters { - affected_table?: string; - affected_record_id?: number; - action?: string; - user_id?: number; - start_date?: Date; - end_date?: Date; - limit?: number; - offset?: number; + affected_table?: string; + affected_record_id?: number; + action?: string; + user_id?: number; + start_date?: Date; + end_date?: Date; + limit?: number; + offset?: number; } export class AuditLogModel { - private static readonly TABLE_NAME = 'audit_logs'; + private static readonly TABLE_NAME = "audit_logs"; - static async create(logData: CreateAuditLogDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create(logData: CreateAuditLogDto): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${AuditLogModel.TABLE_NAME} ( affected_table, affected_record_id, action, previous_data, new_data, user_id, changed_at ) VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) RETURNING * `; - - const result = await client.query(query, [ - logData.affected_table, - logData.affected_record_id, - logData.action, - logData.previous_data ? JSON.stringify(logData.previous_data) : null, - logData.new_data ? JSON.stringify(logData.new_data) : null, - logData.user_id, - ]); - - const row = result.rows[0]; - if (row.previous_data) row.previous_data = typeof row.previous_data === 'string' ? JSON.parse(row.previous_data) : row.previous_data; - if (row.new_data) row.new_data = typeof row.new_data === 'string' ? JSON.parse(row.new_data) : row.new_data; - - return row; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - - if (result.rows[0]) { - const row = result.rows[0]; - if (row.previous_data) row.previous_data = typeof row.previous_data === 'string' ? JSON.parse(row.previous_data) : row.previous_data; - if (row.new_data) row.new_data = typeof row.new_data === 'string' ? JSON.parse(row.new_data) : row.new_data; - return row; - } - return null; - } - - static async findAll(filters?: AuditLogFilters): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME}`; - const conditions: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (filters) { - if (filters.affected_table) { - conditions.push(`affected_table = $${paramIndex++}`); - values.push(filters.affected_table); - } - if (filters.affected_record_id !== undefined) { - conditions.push(`affected_record_id = $${paramIndex++}`); - values.push(filters.affected_record_id); - } - if (filters.action) { - conditions.push(`action = $${paramIndex++}`); - values.push(filters.action); - } - if (filters.user_id !== undefined) { - conditions.push(`user_id = $${paramIndex++}`); - values.push(filters.user_id); - } - if (filters.start_date) { - conditions.push(`changed_at >= $${paramIndex++}`); - values.push(filters.start_date); - } - if (filters.end_date) { - conditions.push(`changed_at <= $${paramIndex++}`); - values.push(filters.end_date); - } - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}`; - } - - query += ` ORDER BY changed_at DESC`; - - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } - - const result = await client.query(query, values); - - return result.rows.map(row => { - if (row.previous_data) row.previous_data = typeof row.previous_data === 'string' ? JSON.parse(row.previous_data) : row.previous_data; - if (row.new_data) row.new_data = typeof row.new_data === 'string' ? JSON.parse(row.new_data) : row.new_data; - return row; - }); - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + const result = await client.query(query, [ + logData.affected_table, + logData.affected_record_id, + logData.action, + logData.previous_data ? JSON.stringify(logData.previous_data) : null, + logData.new_data ? JSON.stringify(logData.new_data) : null, + logData.user_id, + ]); + + const row = result.rows[0]; + if (row.previous_data) + row.previous_data = + typeof row.previous_data === "string" + ? JSON.parse(row.previous_data) + : row.previous_data; + if (row.new_data) + row.new_data = + typeof row.new_data === "string" + ? JSON.parse(row.new_data) + : row.new_data; + + return row; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${AuditLogModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + + if (result.rows[0]) { + const row = result.rows[0]; + if (row.previous_data) + row.previous_data = + typeof row.previous_data === "string" + ? JSON.parse(row.previous_data) + : row.previous_data; + if (row.new_data) + row.new_data = + typeof row.new_data === "string" + ? JSON.parse(row.new_data) + : row.new_data; + return row; + } + return null; + } + + static async findAll(filters?: AuditLogFilters): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${AuditLogModel.TABLE_NAME}`; + const conditions: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (filters) { + if (filters.affected_table) { + conditions.push(`affected_table = $${paramIndex++}`); + values.push(filters.affected_table); + } + if (filters.affected_record_id !== undefined) { + conditions.push(`affected_record_id = $${paramIndex++}`); + values.push(filters.affected_record_id); + } + if (filters.action) { + conditions.push(`action = $${paramIndex++}`); + values.push(filters.action); + } + if (filters.user_id !== undefined) { + conditions.push(`user_id = $${paramIndex++}`); + values.push(filters.user_id); + } + if (filters.start_date) { + conditions.push(`changed_at >= $${paramIndex++}`); + values.push(filters.start_date); + } + if (filters.end_date) { + conditions.push(`changed_at <= $${paramIndex++}`); + values.push(filters.end_date); + } + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += ` ORDER BY changed_at DESC`; + + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } + + const result = await client.query(query, values); + + return result.rows.map((row) => { + if (row.previous_data) + row.previous_data = + typeof row.previous_data === "string" + ? JSON.parse(row.previous_data) + : row.previous_data; + if (row.new_data) + row.new_data = + typeof row.new_data === "string" + ? JSON.parse(row.new_data) + : row.new_data; + return row; + }); + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${AuditLogModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/shared/expense.model.ts b/backend/src/data/postgres/models/shared/expense.model.ts index d230e5b..906a5b7 100644 --- a/backend/src/data/postgres/models/shared/expense.model.ts +++ b/backend/src/data/postgres/models/shared/expense.model.ts @@ -1,160 +1,162 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface Expense { - id?: number; - property_id: number; - amount: number; - currency_type_id: number; - registered_date?: Date; - frequency?: string; + id?: number; + property_id: number; + amount: number; + currency_type_id: number; + registered_date?: Date; + frequency?: string; } export interface CreateExpenseDto { - property_id: number; - amount: number; - currency_type_id: number; - registered_date?: Date; - frequency?: string; + property_id: number; + amount: number; + currency_type_id: number; + registered_date?: Date; + frequency?: string; } export interface ExpenseFilters { - property_id?: number; - currency_type_id?: number; - start_date?: Date; - end_date?: Date; - limit?: number; - offset?: number; + property_id?: number; + currency_type_id?: number; + start_date?: Date; + end_date?: Date; + limit?: number; + offset?: number; } export class ExpenseModel { - private static readonly TABLE_NAME = 'expenses'; + private static readonly TABLE_NAME = "expenses"; - static async create(expenseData: CreateExpenseDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (property_id, amount, currency_type_id, registered_date, frequency) + static async create(expenseData: CreateExpenseDto): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${ExpenseModel.TABLE_NAME} (property_id, amount, currency_type_id, registered_date, frequency) VALUES ($1, $2, $3, $4, $5) RETURNING * `; - - const result = await client.query(query, [ - expenseData.property_id, - expenseData.amount, - expenseData.currency_type_id, - expenseData.registered_date || new Date(), - expenseData.frequency || null, - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY registered_date DESC`; - const result = await client.query(query, [propertyId]); - return result.rows; - } - - static async findAll(filters?: ExpenseFilters): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME}`; - const conditions: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (filters) { - if (filters.property_id !== undefined) { - conditions.push(`property_id = $${paramIndex++}`); - values.push(filters.property_id); - } - if (filters.currency_type_id !== undefined) { - conditions.push(`currency_type_id = $${paramIndex++}`); - values.push(filters.currency_type_id); - } - if (filters.start_date) { - conditions.push(`registered_date >= $${paramIndex++}`); - values.push(filters.start_date); - } - if (filters.end_date) { - conditions.push(`registered_date <= $${paramIndex++}`); - values.push(filters.end_date); - } - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}`; - } - - query += ` ORDER BY registered_date DESC`; - - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } - - const result = await client.query(query, values); - return result.rows; - } - - static async update(id: number, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.amount !== undefined) { - fields.push(`amount = $${paramIndex++}`); - values.push(updateData.amount); - } - if (updateData.currency_type_id !== undefined) { - fields.push(`currency_type_id = $${paramIndex++}`); - values.push(updateData.currency_type_id); - } - if (updateData.registered_date) { - fields.push(`registered_date = $${paramIndex++}`); - values.push(updateData.registered_date); - } - if (updateData.frequency !== undefined) { - fields.push(`frequency = $${paramIndex++}`); - values.push(updateData.frequency || null); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + + const result = await client.query(query, [ + expenseData.property_id, + expenseData.amount, + expenseData.currency_type_id, + expenseData.registered_date || new Date(), + expenseData.frequency || null, + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ExpenseModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByPropertyId(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ExpenseModel.TABLE_NAME} WHERE property_id = $1 ORDER BY registered_date DESC`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + + static async findAll(filters?: ExpenseFilters): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${ExpenseModel.TABLE_NAME}`; + const conditions: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (filters) { + if (filters.property_id !== undefined) { + conditions.push(`property_id = $${paramIndex++}`); + values.push(filters.property_id); + } + if (filters.currency_type_id !== undefined) { + conditions.push(`currency_type_id = $${paramIndex++}`); + values.push(filters.currency_type_id); + } + if (filters.start_date) { + conditions.push(`registered_date >= $${paramIndex++}`); + values.push(filters.start_date); + } + if (filters.end_date) { + conditions.push(`registered_date <= $${paramIndex++}`); + values.push(filters.end_date); + } + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += ` ORDER BY registered_date DESC`; + + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } + + const result = await client.query(query, values); + return result.rows; + } + + static async update( + id: number, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.amount !== undefined) { + fields.push(`amount = $${paramIndex++}`); + values.push(updateData.amount); + } + if (updateData.currency_type_id !== undefined) { + fields.push(`currency_type_id = $${paramIndex++}`); + values.push(updateData.currency_type_id); + } + if (updateData.registered_date) { + fields.push(`registered_date = $${paramIndex++}`); + values.push(updateData.registered_date); + } + if (updateData.frequency !== undefined) { + fields.push(`frequency = $${paramIndex++}`); + values.push(updateData.frequency || null); + } + + if (fields.length === 0) { + return await ExpenseModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${ExpenseModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; - const result = await client.query(query, values); - return result.rows[0] || null; - } + const result = await client.query(query, values); + return result.rows[0] || null; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ExpenseModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/shared/index.ts b/backend/src/data/postgres/models/shared/index.ts index 759a252..77ae712 100644 --- a/backend/src/data/postgres/models/shared/index.ts +++ b/backend/src/data/postgres/models/shared/index.ts @@ -1,5 +1,4 @@ -export * from './audit-log.model'; -export * from './expense.model'; -export * from './price-history.model'; -export * from './property-activity.model'; - +export * from "./audit-log.model"; +export * from "./expense.model"; +export * from "./price-history.model"; +export * from "./property-activity.model"; diff --git a/backend/src/data/postgres/models/shared/price-history.model.ts b/backend/src/data/postgres/models/shared/price-history.model.ts index 42c1cce..fe70bbb 100644 --- a/backend/src/data/postgres/models/shared/price-history.model.ts +++ b/backend/src/data/postgres/models/shared/price-history.model.ts @@ -1,85 +1,86 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface PriceHistory { - id?: number; - property_id: number; - previous_price?: number; - new_price: number; - currency_type_id: number; - operation_type_id: number; - changed_at?: Date; - responsible_user_id: number; + id?: number; + property_id: number; + previous_price?: number; + new_price: number; + currency_type_id: number; + operation_type_id: number; + changed_at?: Date; + responsible_user_id: number; } export interface CreatePriceHistoryDto { - property_id: number; - previous_price?: number; - new_price: number; - currency_type_id: number; - operation_type_id: number; - responsible_user_id: number; + property_id: number; + previous_price?: number; + new_price: number; + currency_type_id: number; + operation_type_id: number; + responsible_user_id: number; } export class PriceHistoryModel { - private static readonly TABLE_NAME = 'price_history'; + private static readonly TABLE_NAME = "price_history"; - static async create(historyData: CreatePriceHistoryDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create( + historyData: CreatePriceHistoryDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PriceHistoryModel.TABLE_NAME} ( property_id, previous_price, new_price, currency_type_id, operation_type_id, responsible_user_id, changed_at ) VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) RETURNING * `; - - const result = await client.query(query, [ - historyData.property_id, - historyData.previous_price || null, - historyData.new_price, - historyData.currency_type_id, - historyData.operation_type_id, - historyData.responsible_user_id, - ]); - return result.rows[0]; - } + const result = await client.query(query, [ + historyData.property_id, + historyData.previous_price || null, + historyData.new_price, + historyData.currency_type_id, + historyData.operation_type_id, + historyData.responsible_user_id, + ]); + + return result.rows[0]; + } - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PriceHistoryModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY changed_at DESC`; - const result = await client.query(query, [propertyId]); - return result.rows; - } + static async findByPropertyId(propertyId: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PriceHistoryModel.TABLE_NAME} WHERE property_id = $1 ORDER BY changed_at DESC`; + const result = await client.query(query, [propertyId]); + return result.rows; + } - static async findByPropertyAndOperation( - propertyId: number, - operationTypeId: number - ): Promise { - const client = PostgresDatabase.getClient(); - const query = ` - SELECT * FROM ${this.TABLE_NAME} + static async findByPropertyAndOperation( + propertyId: number, + operationTypeId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = ` + SELECT * FROM ${PriceHistoryModel.TABLE_NAME} WHERE property_id = $1 AND operation_type_id = $2 ORDER BY changed_at DESC `; - const result = await client.query(query, [propertyId, operationTypeId]); - return result.rows; - } + const result = await client.query(query, [propertyId, operationTypeId]); + return result.rows; + } - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PriceHistoryModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } } - diff --git a/backend/src/data/postgres/models/shared/property-activity.model.ts b/backend/src/data/postgres/models/shared/property-activity.model.ts index 75e6913..26f084b 100644 --- a/backend/src/data/postgres/models/shared/property-activity.model.ts +++ b/backend/src/data/postgres/models/shared/property-activity.model.ts @@ -1,129 +1,134 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface PropertyActivity { - id?: number; - property_id: number; - user_id: number; - action_type?: string; - title?: string; - description?: string; - activity_date?: Date; + id?: number; + property_id: number; + user_id: number; + action_type?: string; + title?: string; + description?: string; + activity_date?: Date; } export interface CreatePropertyActivityDto { - property_id: number; - user_id: number; - action_type?: string; - title?: string; - description?: string; - activity_date?: Date; + property_id: number; + user_id: number; + action_type?: string; + title?: string; + description?: string; + activity_date?: Date; } export interface PropertyActivityFilters { - property_id?: number; - user_id?: number; - action_type?: string; - start_date?: Date; - end_date?: Date; - limit?: number; - offset?: number; + property_id?: number; + user_id?: number; + action_type?: string; + start_date?: Date; + end_date?: Date; + limit?: number; + offset?: number; } export class PropertyActivityModel { - private static readonly TABLE_NAME = 'property_activities'; + private static readonly TABLE_NAME = "property_activities"; - static async create(activityData: CreatePropertyActivityDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} ( + static async create( + activityData: CreatePropertyActivityDto, + ): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${PropertyActivityModel.TABLE_NAME} ( property_id, user_id, action_type, title, description, activity_date ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; - - const result = await client.query(query, [ - activityData.property_id, - activityData.user_id, - activityData.action_type || null, - activityData.title || null, - activityData.description || null, - activityData.activity_date || new Date(), - ]); - - return result.rows[0]; - } - - static async findById(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return result.rows[0] || null; - } - - static async findByPropertyId(propertyId: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE property_id = $1 ORDER BY activity_date DESC`; - const result = await client.query(query, [propertyId]); - return result.rows; - } - - static async findAll(filters?: PropertyActivityFilters): Promise { - const client = PostgresDatabase.getClient(); - let query = `SELECT * FROM ${this.TABLE_NAME}`; - const conditions: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (filters) { - if (filters.property_id !== undefined) { - conditions.push(`property_id = $${paramIndex++}`); - values.push(filters.property_id); - } - if (filters.user_id !== undefined) { - conditions.push(`user_id = $${paramIndex++}`); - values.push(filters.user_id); - } - if (filters.action_type) { - conditions.push(`action_type = $${paramIndex++}`); - values.push(filters.action_type); - } - if (filters.start_date) { - conditions.push(`activity_date >= $${paramIndex++}`); - values.push(filters.start_date); - } - if (filters.end_date) { - conditions.push(`activity_date <= $${paramIndex++}`); - values.push(filters.end_date); - } - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}`; - } - - query += ` ORDER BY activity_date DESC`; - - if (filters?.limit) { - query += ` LIMIT $${paramIndex++}`; - values.push(filters.limit); - if (filters.offset) { - query += ` OFFSET $${paramIndex++}`; - values.push(filters.offset); - } - } - - const result = await client.query(query, values); - return result.rows; - } - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + const result = await client.query(query, [ + activityData.property_id, + activityData.user_id, + activityData.action_type || null, + activityData.title || null, + activityData.description || null, + activityData.activity_date || new Date(), + ]); + + return result.rows[0]; + } + + static async findById(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyActivityModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return result.rows[0] || null; + } + + static async findByPropertyId( + propertyId: number, + ): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${PropertyActivityModel.TABLE_NAME} WHERE property_id = $1 ORDER BY activity_date DESC`; + const result = await client.query(query, [propertyId]); + return result.rows; + } + + static async findAll( + filters?: PropertyActivityFilters, + ): Promise { + const client = PostgresDatabase.getClient(); + let query = `SELECT * FROM ${PropertyActivityModel.TABLE_NAME}`; + const conditions: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (filters) { + if (filters.property_id !== undefined) { + conditions.push(`property_id = $${paramIndex++}`); + values.push(filters.property_id); + } + if (filters.user_id !== undefined) { + conditions.push(`user_id = $${paramIndex++}`); + values.push(filters.user_id); + } + if (filters.action_type) { + conditions.push(`action_type = $${paramIndex++}`); + values.push(filters.action_type); + } + if (filters.start_date) { + conditions.push(`activity_date >= $${paramIndex++}`); + values.push(filters.start_date); + } + if (filters.end_date) { + conditions.push(`activity_date <= $${paramIndex++}`); + values.push(filters.end_date); + } + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += ` ORDER BY activity_date DESC`; + + if (filters?.limit) { + query += ` LIMIT $${paramIndex++}`; + values.push(filters.limit); + if (filters.offset) { + query += ` OFFSET $${paramIndex++}`; + values.push(filters.offset); + } + } + + const result = await client.query(query, values); + return result.rows; + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${PropertyActivityModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/models/users/index.ts b/backend/src/data/postgres/models/users/index.ts index be6e667..97693f3 100644 --- a/backend/src/data/postgres/models/users/index.ts +++ b/backend/src/data/postgres/models/users/index.ts @@ -1,4 +1,2 @@ -export * from './profile.model'; -export * from './role.model'; - - +export * from "./profile.model"; +export * from "./role.model"; diff --git a/backend/src/data/postgres/models/users/profile.model.ts b/backend/src/data/postgres/models/users/profile.model.ts index 20bea67..57ce9f4 100644 --- a/backend/src/data/postgres/models/users/profile.model.ts +++ b/backend/src/data/postgres/models/users/profile.model.ts @@ -1,190 +1,189 @@ -import { PostgresDatabase } from '../../database'; -import { SqlParams } from '../../../types/sql.types'; +import type { SqlParams } from "../../../types/sql.types"; +import { PostgresDatabase } from "../../database"; export interface Profile { - id?: number; - first_name: string; - last_name: string; - email: string; - password_hash: string; - phone?: string; - active?: boolean; - deleted?: boolean; - created_at?: Date; - role_id: number; + id?: number; + first_name: string; + last_name: string; + email: string; + password_hash: string; + phone?: string; + active?: boolean; + deleted?: boolean; + created_at?: Date; + role_id: number; } export interface CreateProfileDto { - first_name: string; - last_name: string; - email: string; - password_hash: string; - phone?: string; - role_id: number; - active?: boolean; + first_name: string; + last_name: string; + email: string; + password_hash: string; + phone?: string; + role_id: number; + active?: boolean; } export class ProfileModel { - private static readonly TABLE_NAME = 'users'; + private static readonly TABLE_NAME = "users"; - static async create(profileData: CreateProfileDto): Promise { - const client = PostgresDatabase.getClient(); - - const query = ` - INSERT INTO ${this.TABLE_NAME} (first_name, last_name, email, password_hash, phone, role_id, active) + static async create(profileData: CreateProfileDto): Promise { + const client = PostgresDatabase.getClient(); + + const query = ` + INSERT INTO ${ProfileModel.TABLE_NAME} (first_name, last_name, email, password_hash, phone, role_id, active) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `; - - const values = [ - profileData.first_name, - profileData.last_name, - profileData.email, - profileData.password_hash, - profileData.phone || null, - profileData.role_id, - profileData.active !== undefined ? profileData.active : true - ]; - - const result = await client.query(query, values); - const row = result.rows[0]; - - return { - ...row, - password_hash: row.password_hash - }; - } - - static async findByEmail(email: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE email = $1 AND deleted = false`; - const result = await client.query(query, [email]); - const row = result.rows[0]; - if (!row) return null; - - return { - ...row, - password_hash: row.password_hash - }; - } - - static async findById(id: number | string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1 AND deleted = false`; - const result = await client.query(query, [id]); - const row = result.rows[0]; - if (!row) return null; - - return { - ...row, - password_hash: row.password_hash - }; - } - - static async update(id: number | string, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - - const fields: string[] = []; - const values: SqlParams = []; - let paramIndex = 1; - - if (updateData.first_name) { - fields.push(`first_name = $${paramIndex++}`); - values.push(updateData.first_name); - } - if (updateData.last_name) { - fields.push(`last_name = $${paramIndex++}`); - values.push(updateData.last_name); - } - if (updateData.email) { - fields.push(`email = $${paramIndex++}`); - values.push(updateData.email); - } - if (updateData.password_hash) { - fields.push(`password_hash = $${paramIndex++}`); - values.push(updateData.password_hash); - } - if (updateData.phone !== undefined) { - fields.push(`phone = $${paramIndex++}`); - values.push(updateData.phone || null); - } - if (updateData.role_id !== undefined) { - fields.push(`role_id = $${paramIndex++}`); - values.push(updateData.role_id); - } - if (updateData.active !== undefined) { - fields.push(`active = $${paramIndex++}`); - values.push(updateData.active); - } - - if (fields.length === 0) { - return await this.findById(id); - } - - values.push(id); - - const query = ` - UPDATE ${this.TABLE_NAME} - SET ${fields.join(', ')} + + const values = [ + profileData.first_name, + profileData.last_name, + profileData.email, + profileData.password_hash, + profileData.phone || null, + profileData.role_id, + profileData.active !== undefined ? profileData.active : true, + ]; + + const result = await client.query(query, values); + const row = result.rows[0]; + + return { + ...row, + password_hash: row.password_hash, + }; + } + + static async findByEmail(email: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ProfileModel.TABLE_NAME} WHERE email = $1 AND deleted = false`; + const result = await client.query(query, [email]); + const row = result.rows[0]; + if (!row) return null; + + return { + ...row, + password_hash: row.password_hash, + }; + } + + static async findById(id: number | string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ProfileModel.TABLE_NAME} WHERE id = $1 AND deleted = false`; + const result = await client.query(query, [id]); + const row = result.rows[0]; + if (!row) return null; + + return { + ...row, + password_hash: row.password_hash, + }; + } + + static async update( + id: number | string, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + + const fields: string[] = []; + const values: SqlParams = []; + let paramIndex = 1; + + if (updateData.first_name) { + fields.push(`first_name = $${paramIndex++}`); + values.push(updateData.first_name); + } + if (updateData.last_name) { + fields.push(`last_name = $${paramIndex++}`); + values.push(updateData.last_name); + } + if (updateData.email) { + fields.push(`email = $${paramIndex++}`); + values.push(updateData.email); + } + if (updateData.password_hash) { + fields.push(`password_hash = $${paramIndex++}`); + values.push(updateData.password_hash); + } + if (updateData.phone !== undefined) { + fields.push(`phone = $${paramIndex++}`); + values.push(updateData.phone || null); + } + if (updateData.role_id !== undefined) { + fields.push(`role_id = $${paramIndex++}`); + values.push(updateData.role_id); + } + if (updateData.active !== undefined) { + fields.push(`active = $${paramIndex++}`); + values.push(updateData.active); + } + + if (fields.length === 0) { + return await ProfileModel.findById(id); + } + + values.push(id); + + const query = ` + UPDATE ${ProfileModel.TABLE_NAME} + SET ${fields.join(", ")} WHERE id = $${paramIndex} AND deleted = false RETURNING * `; - - const result = await client.query(query, values); - const row = result.rows[0]; - if (!row) return null; - - return { - ...row, - password_hash: row.password_hash - }; - } - - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE deleted = false ORDER BY created_at DESC`; - const result = await client.query(query); - return result.rows.map(row => ({ - ...row, - password_hash: row.password_hash - })); - } - - - static async delete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `UPDATE ${this.TABLE_NAME} SET deleted = true WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } - - - static async restore(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `UPDATE ${this.TABLE_NAME} SET deleted = false WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } - - - static async hardDelete(id: number): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } - - static async findByEmailWithPassword(email: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE email = $1 AND deleted = false`; - const result = await client.query(query, [email]); - const row = result.rows[0]; - if (!row) return null; - - return { - ...row, - password_hash: row.password_hash - }; - } -} + const result = await client.query(query, values); + const row = result.rows[0]; + if (!row) return null; + + return { + ...row, + password_hash: row.password_hash, + }; + } + + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ProfileModel.TABLE_NAME} WHERE deleted = false ORDER BY created_at DESC`; + const result = await client.query(query); + return result.rows.map((row) => ({ + ...row, + password_hash: row.password_hash, + })); + } + + static async delete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `UPDATE ${ProfileModel.TABLE_NAME} SET deleted = true WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } + + static async restore(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `UPDATE ${ProfileModel.TABLE_NAME} SET deleted = false WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } + + static async hardDelete(id: number): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${ProfileModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } + + static async findByEmailWithPassword(email: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${ProfileModel.TABLE_NAME} WHERE email = $1 AND deleted = false`; + const result = await client.query(query, [email]); + const row = result.rows[0]; + if (!row) return null; + + return { + ...row, + password_hash: row.password_hash, + }; + } +} diff --git a/backend/src/data/postgres/models/users/role.model.ts b/backend/src/data/postgres/models/users/role.model.ts index bb849f6..d38bc03 100644 --- a/backend/src/data/postgres/models/users/role.model.ts +++ b/backend/src/data/postgres/models/users/role.model.ts @@ -1,95 +1,96 @@ -import { PostgresDatabase } from '../../database'; +import { PostgresDatabase } from "../../database"; export interface Role { - id?: number; - name: string; + id?: number; + name: string; } export interface CreateRoleDto { - name: string; + name: string; } export class RoleModel { - private static readonly TABLE_NAME = 'roles'; + private static readonly TABLE_NAME = "roles"; - static async create(data: CreateRoleDto): Promise { - const client = PostgresDatabase.getClient(); - const query = `INSERT INTO ${this.TABLE_NAME} (name) VALUES ($1) RETURNING *`; - const result = await client.query(query, [data.name]); - const row = result.rows[0]; - - return { - ...row, - name: row.name - }; - } + static async create(data: CreateRoleDto): Promise { + const client = PostgresDatabase.getClient(); + const query = `INSERT INTO ${RoleModel.TABLE_NAME} (name) VALUES ($1) RETURNING *`; + const result = await client.query(query, [data.name]); + const row = result.rows[0]; - static async findAll(): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} ORDER BY name`; - const result = await client.query(query); - return result.rows.map(row => ({ - ...row, - name: row.name - })); - } + return { + ...row, + name: row.name, + }; + } - static async findById(id: number | string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - const row = result.rows[0]; - if (!row) return null; - - return { - ...row, - name: row.name - }; - } + static async findAll(): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${RoleModel.TABLE_NAME} ORDER BY name`; + const result = await client.query(query); + return result.rows.map((row) => ({ + ...row, + name: row.name, + })); + } - static async findByName(name: string): Promise { - const client = PostgresDatabase.getClient(); - const query = `SELECT * FROM ${this.TABLE_NAME} WHERE name = $1`; - const result = await client.query(query, [name]); - const row = result.rows[0]; - if (!row) return null; - - return { - ...row, - name: row.name - }; - } + static async findById(id: number | string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${RoleModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + const row = result.rows[0]; + if (!row) return null; - static async findByNombre(nombre: string): Promise { - return this.findByName(nombre); - } + return { + ...row, + name: row.name, + }; + } - static async findByTitle(title: string): Promise { - return this.findByName(title); - } + static async findByName(name: string): Promise { + const client = PostgresDatabase.getClient(); + const query = `SELECT * FROM ${RoleModel.TABLE_NAME} WHERE name = $1`; + const result = await client.query(query, [name]); + const row = result.rows[0]; + if (!row) return null; - static async update(id: number | string, updateData: Partial): Promise { - const client = PostgresDatabase.getClient(); - if (!updateData.name) { - return await this.findById(id); - } - const query = `UPDATE ${this.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; - const result = await client.query(query, [updateData.name, id]); - const row = result.rows[0]; - if (!row) return null; - - return { - ...row, - name: row.name - }; - } + return { + ...row, + name: row.name, + }; + } - static async delete(id: number | string): Promise { - const client = PostgresDatabase.getClient(); - const query = `DELETE FROM ${this.TABLE_NAME} WHERE id = $1`; - const result = await client.query(query, [id]); - return (result.rowCount ?? 0) > 0; - } -} + static async findByNombre(nombre: string): Promise { + return RoleModel.findByName(nombre); + } + + static async findByTitle(title: string): Promise { + return RoleModel.findByName(title); + } + static async update( + id: number | string, + updateData: Partial, + ): Promise { + const client = PostgresDatabase.getClient(); + if (!updateData.name) { + return await RoleModel.findById(id); + } + const query = `UPDATE ${RoleModel.TABLE_NAME} SET name = $1 WHERE id = $2 RETURNING *`; + const result = await client.query(query, [updateData.name, id]); + const row = result.rows[0]; + if (!row) return null; + return { + ...row, + name: row.name, + }; + } + + static async delete(id: number | string): Promise { + const client = PostgresDatabase.getClient(); + const query = `DELETE FROM ${RoleModel.TABLE_NAME} WHERE id = $1`; + const result = await client.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } +} diff --git a/backend/src/data/postgres/transaction.helper.ts b/backend/src/data/postgres/transaction.helper.ts index b74c078..6450186 100644 --- a/backend/src/data/postgres/transaction.helper.ts +++ b/backend/src/data/postgres/transaction.helper.ts @@ -1,25 +1,23 @@ -import { PostgresDatabase } from './database'; -import { PoolClient } from 'pg'; +import type { PoolClient } from "pg"; + +import { PostgresDatabase } from "./database"; export class TransactionHelper { - - static async executeInTransaction( - callback: (client: PoolClient) => Promise - ): Promise { - const client = await PostgresDatabase.getPoolClient(); - - try { - await client.query('BEGIN'); - const result = await callback(client); - await client.query('COMMIT'); - return result; - } catch (error) { - await client.query('ROLLBACK').catch(() => { - }); - throw error; - } finally { - client.release(); - } - } -} + static async executeInTransaction( + callback: (client: PoolClient) => Promise, + ): Promise { + const client = await PostgresDatabase.getPoolClient(); + try { + await client.query("BEGIN"); + const result = await callback(client); + await client.query("COMMIT"); + return result; + } catch (error) { + await client.query("ROLLBACK").catch(() => {}); + throw error; + } finally { + client.release(); + } + } +} diff --git a/backend/src/data/types/sql.types.ts b/backend/src/data/types/sql.types.ts index be4fe2f..afa1f02 100644 --- a/backend/src/data/types/sql.types.ts +++ b/backend/src/data/types/sql.types.ts @@ -1,3 +1,2 @@ - export type SqlValue = string | number | boolean | Date | null | undefined; export type SqlParams = SqlValue[]; diff --git a/backend/src/domain/dtos/auth/login-profile.dto.ts b/backend/src/domain/dtos/auth/login-profile.dto.ts index dff13b9..a308d25 100644 --- a/backend/src/domain/dtos/auth/login-profile.dto.ts +++ b/backend/src/domain/dtos/auth/login-profile.dto.ts @@ -1,39 +1,39 @@ import { regularExps } from "../../../config"; export class LoginProfileDto { - constructor( - public readonly email: string, - public readonly password: string, - ){} + constructor( + public readonly email: string, + public readonly password: string, + ) {} - static create( object: { [key: string]: any }): [string?, LoginProfileDto?] { - const { email, password } = object; - - if (!email || email.trim().length === 0) { - return ['Email is required', undefined]; - } - if (!password || password.trim().length === 0) { - return ['Password is required', undefined]; - } - - if (!regularExps.email.test(email.trim())) { - return ['Email format is invalid', undefined]; - } - - if (password.length < 6) { - return ['Password must be at least 6 characters', undefined]; - } - if (password.length > 100) { - return ['Password must be less than 100 characters', undefined]; - } + static create(object: Record): [string?, LoginProfileDto?] { + const { email, password } = object; - return [ - undefined, - new LoginProfileDto( - email.trim().toLowerCase(), - password - ) - ]; - } -} + if (!email || typeof email !== "string" || email.trim().length === 0) { + return ["Email is required", undefined]; + } + if ( + !password || + typeof password !== "string" || + password.trim().length === 0 + ) { + return ["Password is required", undefined]; + } + + if (!regularExps.email.test(email.trim())) { + return ["Email format is invalid", undefined]; + } + if (password.length < 6) { + return ["Password must be at least 6 characters", undefined]; + } + if (password.length > 100) { + return ["Password must be less than 100 characters", undefined]; + } + + return [ + undefined, + new LoginProfileDto(email.trim().toLowerCase(), password), + ]; + } +} diff --git a/backend/src/domain/dtos/auth/register-profile.dto.ts b/backend/src/domain/dtos/auth/register-profile.dto.ts index 5b6820b..9da0255 100644 --- a/backend/src/domain/dtos/auth/register-profile.dto.ts +++ b/backend/src/domain/dtos/auth/register-profile.dto.ts @@ -1,74 +1,101 @@ import { regularExps } from "../../../config"; export class RegisterProfileDto { - constructor( - public readonly first_name: string, - public readonly last_name: string, - public readonly email: string, - public readonly password: string, - public readonly phone?: string, - public readonly role_id?: number, - public readonly whatsapp_number?: string, - ){} + constructor( + public readonly first_name: string, + public readonly last_name: string, + public readonly email: string, + public readonly password: string, + public readonly phone?: string, + public readonly role_id?: number, + public readonly whatsapp_number?: string, + ) {} - static create( object: { [key: string]: any }): [string?, RegisterProfileDto?] { - const { first_name, last_name, email, password, phone, role_id, whatsapp_number } = object; - - if (!first_name || first_name.trim().length === 0) { - return ['First name is required', undefined]; - } - if (!last_name || last_name.trim().length === 0) { - return ['Last name is required', undefined]; - } - if (!email || email.trim().length === 0) { - return ['Email is required', undefined]; - } - if (!password || password.trim().length === 0) { - return ['Password is required', undefined]; - } - - if (!regularExps.email.test(email.trim())) { - return ['Email format is invalid', undefined]; - } - - if (password.length < 6) { - return ['Password must be at least 6 characters', undefined]; - } - if (password.length > 100) { - return ['Password must be less than 100 characters', undefined]; - } - - if (phone && phone.trim().length > 0) { - if (!regularExps.phone.test(phone.trim())) { - return ['Phone format is invalid', undefined]; - } - } - - if (whatsapp_number && whatsapp_number.trim().length > 0) { - if (!regularExps.whatsapp.test(whatsapp_number.trim())) { - return ['WhatsApp number format is invalid', undefined]; - } - } + static create( + object: Record, + ): [string?, RegisterProfileDto?] { + const { + first_name, + last_name, + email, + password, + phone, + role_id, + whatsapp_number, + } = object; - if (first_name.trim().length < 2) { - return ['First name must be at least 2 characters', undefined]; - } - if (last_name.trim().length < 2) { - return ['Last name must be at least 2 characters', undefined]; - } + if ( + !first_name || + typeof first_name !== "string" || + first_name.trim().length === 0 + ) { + return ["First name is required", undefined]; + } + if ( + !last_name || + typeof last_name !== "string" || + last_name.trim().length === 0 + ) { + return ["Last name is required", undefined]; + } + if (!email || typeof email !== "string" || email.trim().length === 0) { + return ["Email is required", undefined]; + } + if ( + !password || + typeof password !== "string" || + password.trim().length === 0 + ) { + return ["Password is required", undefined]; + } - return [ - undefined, - new RegisterProfileDto( - first_name.trim(), - last_name.trim(), - email.trim().toLowerCase(), - password, - phone?.trim() || undefined, - role_id || undefined, - whatsapp_number?.trim() || undefined - ) - ]; - } -} + if (!regularExps.email.test(email.trim())) { + return ["Email format is invalid", undefined]; + } + + if (password.length < 6) { + return ["Password must be at least 6 characters", undefined]; + } + if (password.length > 100) { + return ["Password must be less than 100 characters", undefined]; + } + + if (phone && typeof phone === "string" && phone.trim().length > 0) { + if (!regularExps.phone.test(phone.trim())) { + return ["Phone format is invalid", undefined]; + } + } + if ( + whatsapp_number && + typeof whatsapp_number === "string" && + whatsapp_number.trim().length > 0 + ) { + if (!regularExps.whatsapp.test(whatsapp_number.trim())) { + return ["WhatsApp number format is invalid", undefined]; + } + } + + if (first_name.trim().length < 2) { + return ["First name must be at least 2 characters", undefined]; + } + if (last_name.trim().length < 2) { + return ["Last name must be at least 2 characters", undefined]; + } + + return [ + undefined, + new RegisterProfileDto( + (first_name as string).trim(), + (last_name as string).trim(), + (email as string).trim().toLowerCase(), + password as string, + phone && typeof phone === "string" ? phone.trim() : undefined, + role_id ? Number(role_id) : undefined, + whatsapp_number && typeof whatsapp_number === "string" + ? whatsapp_number.trim() + : undefined, + ), + ]; + } +} diff --git a/backend/src/domain/dtos/clients/create-lead.dto.ts b/backend/src/domain/dtos/clients/create-lead.dto.ts index e8d2a61..da6959e 100644 --- a/backend/src/domain/dtos/clients/create-lead.dto.ts +++ b/backend/src/domain/dtos/clients/create-lead.dto.ts @@ -1,71 +1,73 @@ -import { - validateFirstName, - validateLastName, - validatePhone, - validateEmail, - validatePropertyId -} from '../../utils/client-validation.util'; +import { + validateEmail, + validateFirstName, + validateLastName, + validatePhone, + validatePropertyId, +} from "../../utils/client-validation.util"; export class CreateLeadDto { - constructor( - public readonly first_name: string, - public readonly last_name: string, - public readonly phone: string, - public readonly email: string, - public readonly notes?: string, - public readonly consultation_type_id?: number, - public readonly consultation_type?: string, - public readonly property_id?: number, - ) {} + constructor( + public readonly first_name: string, + public readonly last_name: string, + public readonly phone: string, + public readonly email: string, + public readonly notes?: string, + public readonly consultation_type_id?: number, + public readonly consultation_type?: string, + public readonly property_id?: number, + ) {} - static create(object: Record): [string?, CreateLeadDto?] { - const { - first_name, - last_name, - phone, - email, - notes, - consultation_type_id, - consultation_type, - property_id, - } = object; + static create(object: Record): [string?, CreateLeadDto?] { + const { + first_name, + last_name, + phone, + email, + notes, + consultation_type_id, + consultation_type, + property_id, + } = object; - const firstNameError = validateFirstName(first_name); - if (firstNameError[0]) return [firstNameError[0], undefined]; - - const lastNameError = validateLastName(last_name); - if (lastNameError[0]) return [lastNameError[0], undefined]; - - const phoneError = validatePhone(phone); - if (phoneError[0]) return [phoneError[0], undefined]; + const firstNameError = validateFirstName(first_name); + if (firstNameError[0]) return [firstNameError[0], undefined]; - const emailError = validateEmail(email, true); - if (emailError[0]) return [emailError[0], undefined]; + const lastNameError = validateLastName(last_name); + if (lastNameError[0]) return [lastNameError[0], undefined]; - if (consultation_type_id && consultation_type) { - return ['Provide either consultation_type_id OR consultation_type name, not both', undefined]; - } + const phoneError = validatePhone(phone); + if (phoneError[0]) return [phoneError[0], undefined]; - if (consultation_type_id && isNaN(Number(consultation_type_id))) { - return ['Consultation type ID must be a number', undefined]; - } + const emailError = validateEmail(email, true); + if (emailError[0]) return [emailError[0], undefined]; - const propertyIdError = validatePropertyId(property_id); - if (propertyIdError[0]) return [propertyIdError[0], undefined]; + if (consultation_type_id && consultation_type) { + return [ + "Provide either consultation_type_id OR consultation_type name, not both", + undefined, + ]; + } - return [ - undefined, - new CreateLeadDto( - (first_name as string).trim(), - (last_name as string).trim(), - (phone as string).trim(), - (email as string).trim(), - (notes as string | undefined)?.trim(), - consultation_type_id ? Number(consultation_type_id) : undefined, - (consultation_type as string | undefined)?.trim(), - property_id ? Number(property_id) : undefined - ) - ]; - } -} + if (consultation_type_id && Number.isNaN(Number(consultation_type_id))) { + return ["Consultation type ID must be a number", undefined]; + } + + const propertyIdError = validatePropertyId(property_id); + if (propertyIdError[0]) return [propertyIdError[0], undefined]; + return [ + undefined, + new CreateLeadDto( + (first_name as string).trim(), + (last_name as string).trim(), + (phone as string).trim(), + (email as string).trim(), + (notes as string | undefined)?.trim(), + consultation_type_id ? Number(consultation_type_id) : undefined, + (consultation_type as string | undefined)?.trim(), + property_id ? Number(property_id) : undefined, + ), + ]; + } +} diff --git a/backend/src/domain/dtos/clients/create-owner.dto.ts b/backend/src/domain/dtos/clients/create-owner.dto.ts index 2d780ba..f48ff4f 100644 --- a/backend/src/domain/dtos/clients/create-owner.dto.ts +++ b/backend/src/domain/dtos/clients/create-owner.dto.ts @@ -1,67 +1,66 @@ -import { - validateFirstName, - validateLastName, - validatePhone, - validateEmail, - validateDni, - validatePropertyId -} from '../../utils/client-validation.util'; +import { + validateDni, + validateEmail, + validateFirstName, + validateLastName, + validatePhone, + validatePropertyId, +} from "../../utils/client-validation.util"; export class CreateOwnerDto { - constructor( - public readonly first_name: string, - public readonly last_name: string, - public readonly phone: string, - public readonly email?: string, - public readonly dni?: string, - public readonly address?: string, - public readonly notes?: string, - public readonly property_id?: number, - ) {} + constructor( + public readonly first_name: string, + public readonly last_name: string, + public readonly phone: string, + public readonly email?: string, + public readonly dni?: string, + public readonly address?: string, + public readonly notes?: string, + public readonly property_id?: number, + ) {} - static create(object: Record): [string?, CreateOwnerDto?] { - const { - first_name, - last_name, - phone, - email, - dni, - address, - notes, - property_id, - } = object; + static create(object: Record): [string?, CreateOwnerDto?] { + const { + first_name, + last_name, + phone, + email, + dni, + address, + notes, + property_id, + } = object; - const firstNameError = validateFirstName(first_name); - if (firstNameError[0]) return [firstNameError[0], undefined]; - - const lastNameError = validateLastName(last_name); - if (lastNameError[0]) return [lastNameError[0], undefined]; - - const phoneError = validatePhone(phone); - if (phoneError[0]) return [phoneError[0], undefined]; + const firstNameError = validateFirstName(first_name); + if (firstNameError[0]) return [firstNameError[0], undefined]; - const emailError = validateEmail(email, false); - if (emailError[0]) return [emailError[0], undefined]; + const lastNameError = validateLastName(last_name); + if (lastNameError[0]) return [lastNameError[0], undefined]; - const dniError = validateDni(dni); - if (dniError[0]) return [dniError[0], undefined]; + const phoneError = validatePhone(phone); + if (phoneError[0]) return [phoneError[0], undefined]; - const propertyIdError = validatePropertyId(property_id); - if (propertyIdError[0]) return [propertyIdError[0], undefined]; + const emailError = validateEmail(email, false); + if (emailError[0]) return [emailError[0], undefined]; - return [ - undefined, - new CreateOwnerDto( - (first_name as string).trim(), - (last_name as string).trim(), - (phone as string).trim(), - (email as string | undefined)?.trim(), - (dni as string | undefined)?.trim(), - (address as string | undefined)?.trim(), - (notes as string | undefined)?.trim(), - property_id ? Number(property_id) : undefined - ) - ]; - } -} + const dniError = validateDni(dni); + if (dniError[0]) return [dniError[0], undefined]; + + const propertyIdError = validatePropertyId(property_id); + if (propertyIdError[0]) return [propertyIdError[0], undefined]; + return [ + undefined, + new CreateOwnerDto( + (first_name as string).trim(), + (last_name as string).trim(), + (phone as string).trim(), + (email as string | undefined)?.trim(), + (dni as string | undefined)?.trim(), + (address as string | undefined)?.trim(), + (notes as string | undefined)?.trim(), + property_id ? Number(property_id) : undefined, + ), + ]; + } +} diff --git a/backend/src/domain/dtos/clients/create-tenant.dto.ts b/backend/src/domain/dtos/clients/create-tenant.dto.ts index d152e12..5022c5b 100644 --- a/backend/src/domain/dtos/clients/create-tenant.dto.ts +++ b/backend/src/domain/dtos/clients/create-tenant.dto.ts @@ -1,177 +1,207 @@ -import { - validateFirstName, - validateLastName, - validatePhone, - validateEmail, - validateDni, - validatePropertyId -} from '../../utils/client-validation.util'; +import { + validateDni, + validateEmail, + validateFirstName, + validateLastName, + validatePhone, + validatePropertyId, +} from "../../utils/client-validation.util"; export class CreateTenantDto { - constructor( - public readonly first_name: string, - public readonly last_name: string, - public readonly phone: string, - - public readonly email?: string, - public readonly dni?: string, - public readonly address?: string, - public readonly notes?: string, - - public readonly property_id?: number, - public readonly property_address?: string, - - public readonly contract_start_date?: string | Date, - public readonly contract_end_date?: string | Date, - public readonly next_increase_date?: string | Date, - public readonly monthly_amount?: number, - public readonly currency_type_id?: number, - public readonly currency_type?: string, - - public readonly remind_increase?: boolean, - public readonly remind_contract_end?: boolean, - - public readonly external_reference?: string, - ) {} - - static create(object: Record): [string?, CreateTenantDto?] { - const { - first_name, - last_name, - phone, - email, - dni, - address, - notes, - property_id, - property_address, - contract_start_date, - contract_end_date, - next_increase_date, - monthly_amount, - currency_type_id, - currency_type, - remind_increase, - remind_contract_end, - external_reference, - } = object; - - const firstNameError = validateFirstName(first_name); - if (firstNameError[0]) return [firstNameError[0], undefined]; - - const lastNameError = validateLastName(last_name); - if (lastNameError[0]) return [lastNameError[0], undefined]; - - const phoneError = validatePhone(phone); - if (phoneError[0]) return [phoneError[0], undefined]; - - const emailError = validateEmail(email, false); - if (emailError[0]) return [emailError[0], undefined]; - - const dniError = validateDni(dni); - if (dniError[0]) return [dniError[0], undefined]; - - const propertyIdError = validatePropertyId(property_id); - if (propertyIdError[0]) return [propertyIdError[0], undefined]; - - if (currency_type_id && currency_type) { - return ['Provide either currency_type_id OR currency_type name, not both', undefined]; - } - - if (currency_type_id && isNaN(Number(currency_type_id))) { - return ['Currency type ID must be a number', undefined]; - } - - const validateDateString = (date: any, fieldName: string): [string?, void?] => { - if (!date) return [undefined, undefined]; - - if (typeof date === 'string') { - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { - return [`Invalid ${fieldName} format. Expected YYYY-MM-DD (e.g., 2025-01-01)`, undefined]; - } - const [year, month, day] = date.split('-').map(Number); - const dateObj = new Date(Date.UTC(year, month - 1, day)); - if (dateObj.getUTCFullYear() !== year || - dateObj.getUTCMonth() !== month - 1 || - dateObj.getUTCDate() !== day) { - return [`Invalid ${fieldName}. Date does not exist`, undefined]; - } - } else if (date instanceof Date) { - if (isNaN(date.getTime())) { - return [`Invalid ${fieldName} format`, undefined]; - } - } else { - return [`Invalid ${fieldName} format. Expected string (YYYY-MM-DD) or Date`, undefined]; - } - return [undefined, undefined]; - }; - - const startDateError = validateDateString(contract_start_date, 'contract_start_date'); - if (startDateError[0]) return [startDateError[0], undefined]; - - const endDateError = validateDateString(contract_end_date, 'contract_end_date'); - if (endDateError[0]) return [endDateError[0], undefined]; - - if (contract_start_date && contract_end_date) { - const parseDate = (date: string | Date): Date => { - if (typeof date === 'string') { - const [year, month, day] = date.split('-').map(Number); - return new Date(Date.UTC(year, month - 1, day)); - } - return date; - }; - - const startDate = parseDate(contract_start_date as string | Date); - const endDate = parseDate(contract_end_date as string | Date); - if (endDate < startDate) { - return ['Contract end date must be after start date', undefined]; - } - } - - const increaseDateError = validateDateString(next_increase_date, 'next_increase_date'); - if (increaseDateError[0]) return [increaseDateError[0], undefined]; - - if (monthly_amount !== undefined && monthly_amount !== null) { - if (isNaN(Number(monthly_amount)) || Number(monthly_amount) < 0) { - return ['Monthly amount must be a positive number', undefined]; - } - } - - return [ - undefined, - new CreateTenantDto( - (first_name as string).trim(), - (last_name as string).trim(), - (phone as string).trim(), - (email as string | undefined)?.trim(), - (dni as string | undefined)?.trim(), - (address as string | undefined)?.trim(), - (notes as string | undefined)?.trim(), - property_id ? Number(property_id) : undefined, - (property_address as string | undefined)?.trim(), - contract_start_date ? (typeof contract_start_date === 'string' - ? contract_start_date - : contract_start_date instanceof Date - ? contract_start_date.toISOString().split('T')[0] - : String(contract_start_date)) : undefined, - contract_end_date ? (typeof contract_end_date === 'string' - ? contract_end_date - : contract_end_date instanceof Date - ? contract_end_date.toISOString().split('T')[0] - : String(contract_end_date)) : undefined, - next_increase_date ? (typeof next_increase_date === 'string' - ? next_increase_date - : next_increase_date instanceof Date - ? next_increase_date.toISOString().split('T')[0] - : String(next_increase_date)) : undefined, - monthly_amount ? Number(monthly_amount) : undefined, - currency_type_id ? Number(currency_type_id) : undefined, - (currency_type as string | undefined)?.trim(), - remind_increase !== undefined ? Boolean(remind_increase) : undefined, - remind_contract_end !== undefined ? Boolean(remind_contract_end) : undefined, - (external_reference as string | undefined)?.trim() - ) - ]; - } -} + constructor( + public readonly first_name: string, + public readonly last_name: string, + public readonly phone: string, + + public readonly email?: string, + public readonly dni?: string, + public readonly address?: string, + public readonly notes?: string, + + public readonly property_id?: number, + public readonly property_address?: string, + + public readonly contract_start_date?: string | Date, + public readonly contract_end_date?: string | Date, + public readonly next_increase_date?: string | Date, + public readonly monthly_amount?: number, + public readonly currency_type_id?: number, + public readonly currency_type?: string, + + public readonly remind_increase?: boolean, + public readonly remind_contract_end?: boolean, + + public readonly external_reference?: string, + ) {} + + static create(object: Record): [string?, CreateTenantDto?] { + const { + first_name, + last_name, + phone, + email, + dni, + address, + notes, + property_id, + property_address, + contract_start_date, + contract_end_date, + next_increase_date, + monthly_amount, + currency_type_id, + currency_type, + remind_increase, + remind_contract_end, + external_reference, + } = object; + + const firstNameError = validateFirstName(first_name); + if (firstNameError[0]) return [firstNameError[0], undefined]; + + const lastNameError = validateLastName(last_name); + if (lastNameError[0]) return [lastNameError[0], undefined]; + + const phoneError = validatePhone(phone); + if (phoneError[0]) return [phoneError[0], undefined]; + + const emailError = validateEmail(email, false); + if (emailError[0]) return [emailError[0], undefined]; + + const dniError = validateDni(dni); + if (dniError[0]) return [dniError[0], undefined]; + const propertyIdError = validatePropertyId(property_id); + if (propertyIdError[0]) return [propertyIdError[0], undefined]; + + if (currency_type_id && currency_type) { + return [ + "Provide either currency_type_id OR currency_type name, not both", + undefined, + ]; + } + + if (currency_type_id && Number.isNaN(Number(currency_type_id))) { + return ["Currency type ID must be a number", undefined]; + } + + const validateDateString = ( + date: unknown, + fieldName: string, + ): [string?, undefined?] => { + if (!date) return [undefined, undefined]; + + if (typeof date === "string") { + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + return [ + `Invalid ${fieldName} format. Expected YYYY-MM-DD (e.g., 2025-01-01)`, + undefined, + ]; + } + const [year, month, day] = date.split("-").map(Number); + const dateObj = new Date(Date.UTC(year, month - 1, day)); + if ( + dateObj.getUTCFullYear() !== year || + dateObj.getUTCMonth() !== month - 1 || + dateObj.getUTCDate() !== day + ) { + return [`Invalid ${fieldName}. Date does not exist`, undefined]; + } + } else if (date instanceof Date) { + if (Number.isNaN(date.getTime())) { + return [`Invalid ${fieldName} format`, undefined]; + } + } else { + return [ + `Invalid ${fieldName} format. Expected string (YYYY-MM-DD) or Date`, + undefined, + ]; + } + return [undefined, undefined]; + }; + + const startDateError = validateDateString( + contract_start_date, + "contract_start_date", + ); + if (startDateError[0]) return [startDateError[0], undefined]; + + const endDateError = validateDateString( + contract_end_date, + "contract_end_date", + ); + if (endDateError[0]) return [endDateError[0], undefined]; + + if (contract_start_date && contract_end_date) { + const parseDate = (date: string | Date): Date => { + if (typeof date === "string") { + const [year, month, day] = date.split("-").map(Number); + return new Date(Date.UTC(year, month - 1, day)); + } + return date; + }; + + const startDate = parseDate(contract_start_date as string | Date); + const endDate = parseDate(contract_end_date as string | Date); + if (endDate < startDate) { + return ["Contract end date must be after start date", undefined]; + } + } + + const increaseDateError = validateDateString( + next_increase_date, + "next_increase_date", + ); + if (increaseDateError[0]) return [increaseDateError[0], undefined]; + + if (monthly_amount !== undefined && monthly_amount !== null) { + if (Number.isNaN(Number(monthly_amount)) || Number(monthly_amount) < 0) { + return ["Monthly amount must be a positive number", undefined]; + } + } + + return [ + undefined, + new CreateTenantDto( + (first_name as string).trim(), + (last_name as string).trim(), + (phone as string).trim(), + (email as string | undefined)?.trim(), + (dni as string | undefined)?.trim(), + (address as string | undefined)?.trim(), + (notes as string | undefined)?.trim(), + property_id ? Number(property_id) : undefined, + (property_address as string | undefined)?.trim(), + contract_start_date + ? typeof contract_start_date === "string" + ? contract_start_date + : contract_start_date instanceof Date + ? contract_start_date.toISOString().split("T")[0] + : String(contract_start_date) + : undefined, + contract_end_date + ? typeof contract_end_date === "string" + ? contract_end_date + : contract_end_date instanceof Date + ? contract_end_date.toISOString().split("T")[0] + : String(contract_end_date) + : undefined, + next_increase_date + ? typeof next_increase_date === "string" + ? next_increase_date + : next_increase_date instanceof Date + ? next_increase_date.toISOString().split("T")[0] + : String(next_increase_date) + : undefined, + monthly_amount ? Number(monthly_amount) : undefined, + currency_type_id ? Number(currency_type_id) : undefined, + (currency_type as string | undefined)?.trim(), + remind_increase !== undefined ? Boolean(remind_increase) : undefined, + remind_contract_end !== undefined + ? Boolean(remind_contract_end) + : undefined, + (external_reference as string | undefined)?.trim(), + ), + ]; + } +} diff --git a/backend/src/domain/dtos/clients/index.ts b/backend/src/domain/dtos/clients/index.ts index f1073d1..e818095 100644 --- a/backend/src/domain/dtos/clients/index.ts +++ b/backend/src/domain/dtos/clients/index.ts @@ -1,8 +1,4 @@ -export * from './update-client.dto'; -export * from './create-tenant.dto'; -export * from './create-owner.dto'; -export * from './create-lead.dto'; - - - - +export * from "./create-lead.dto"; +export * from "./create-owner.dto"; +export * from "./create-tenant.dto"; +export * from "./update-client.dto"; diff --git a/backend/src/domain/dtos/clients/update-client.dto.ts b/backend/src/domain/dtos/clients/update-client.dto.ts index cbcc7f8..7cee7d4 100644 --- a/backend/src/domain/dtos/clients/update-client.dto.ts +++ b/backend/src/domain/dtos/clients/update-client.dto.ts @@ -1,157 +1,198 @@ -import { - validateFirstName, - validateLastName, - validatePhone, - validateEmail, - validateDni -} from '../../utils/client-validation.util'; - +import { + validateDni, + validateEmail, + validateFirstName, + validateLastName, + validatePhone, +} from "../../utils/client-validation.util"; export class UpdateClientDto { - constructor( - public readonly first_name?: string, - public readonly last_name?: string, - public readonly phone?: string, - public readonly contact_category_id?: number, - public readonly contact_category?: string, - public readonly email?: string, - public readonly dni?: string, - public readonly property_interest_phone?: string, - public readonly address?: string, - public readonly notes?: string, - public readonly interest_zone?: string, - public readonly purchase_interest?: boolean, - public readonly rental_interest?: boolean, - public readonly property_search_type_id?: number, - public readonly property_search_type?: string, - public readonly city_id?: number, - ) {} - - static create(object: Record): [string?, UpdateClientDto?] { - const { - first_name, - last_name, - phone, - contact_category_id, - contact_category, - email, - dni, - property_interest_phone, - address, - notes, - interest_zone, - purchase_interest, - rental_interest, - property_search_type_id, - property_search_type, - city_id - } = object; - - if (contact_category_id && contact_category) { - return ['Provide either contact_category_id OR contact_category name, not both', undefined]; - } - - if (contact_category_id && isNaN(Number(contact_category_id))) { - return ['Contact category ID must be a number', undefined]; - } - - if (property_search_type_id && property_search_type) { - return ['Provide either property_search_type_id OR property_search_type name, not both', undefined]; - } - - if (property_search_type_id && isNaN(Number(property_search_type_id))) { - return ['Property search type ID must be a number', undefined]; - } - - if (city_id && isNaN(Number(city_id))) { - return ['City ID must be a number', undefined]; - } - - if (first_name !== undefined) { - if (!first_name || typeof first_name !== 'string' || first_name.trim().length === 0) { - return ['First name cannot be empty', undefined]; - } - const firstNameError = validateFirstName(first_name); - if (firstNameError[0]) return [firstNameError[0], undefined]; - } - - if (last_name !== undefined) { - if (!last_name || typeof last_name !== 'string' || last_name.trim().length === 0) { - return ['Last name cannot be empty', undefined]; - } - const lastNameError = validateLastName(last_name); - if (lastNameError[0]) return [lastNameError[0], undefined]; - } - - if (phone !== undefined && phone !== null) { - if (typeof phone !== 'string' || phone.trim().length === 0) { - return ['Phone cannot be empty', undefined]; - } - const phoneError = validatePhone(phone); - if (phoneError[0]) return [phoneError[0], undefined]; - } - - if (email !== undefined && email !== null) { - const emailError = validateEmail(email, false); - if (emailError[0]) return [emailError[0], undefined]; - } - - if (property_interest_phone !== undefined && property_interest_phone !== null) { - if (typeof property_interest_phone === 'string' && property_interest_phone.trim().length > 0) { - const trimmedInterestPhone = property_interest_phone.trim(); - const validCharsRegex = /^[\d\s\-\(\)\+\.]+$/; - if (!validCharsRegex.test(trimmedInterestPhone)) { - return ['Invalid property interest phone format: phone can only contain numbers, spaces, dashes, parentheses, plus sign, and dots', undefined]; - } - const interestDigitsOnly = trimmedInterestPhone.replace(/\D/g, ''); - if (interestDigitsOnly.length < 10) { - return ['Invalid property interest phone format: phone must contain at least 10 digits', undefined]; - } - if (interestDigitsOnly.length > 15) { - return ['Invalid property interest phone format: phone must contain at most 15 digits', undefined]; - } - } - } - - if (dni !== undefined && dni !== null) { - const dniError = validateDni(dni); - if (dniError[0]) return [dniError[0], undefined]; - } - - const hasAnyField = first_name || last_name || phone || contact_category_id || contact_category || - email !== undefined || dni !== undefined || property_interest_phone !== undefined || - address !== undefined || notes !== undefined || interest_zone !== undefined || - purchase_interest !== undefined || rental_interest !== undefined || - property_search_type_id || property_search_type || city_id; - - if (!hasAnyField) { - return ['At least one field must be provided for update', undefined]; - } - - return [ - undefined, - new UpdateClientDto( - (first_name as string | undefined)?.trim(), - (last_name as string | undefined)?.trim(), - (phone as string | undefined)?.trim(), - contact_category_id ? Number(contact_category_id) : undefined, - (contact_category as string | undefined)?.trim(), - (email as string | undefined)?.trim(), - (dni as string | undefined)?.trim(), - (property_interest_phone as string | undefined)?.trim(), - (address as string | undefined)?.trim(), - (notes as string | undefined)?.trim(), - (interest_zone as string | undefined)?.trim(), - purchase_interest !== undefined ? Boolean(purchase_interest) : undefined, - rental_interest !== undefined ? Boolean(rental_interest) : undefined, - property_search_type_id ? Number(property_search_type_id) : undefined, - (property_search_type as string | undefined)?.trim(), - city_id ? Number(city_id) : undefined - ) - ]; - } + constructor( + public readonly first_name?: string, + public readonly last_name?: string, + public readonly phone?: string, + public readonly contact_category_id?: number, + public readonly contact_category?: string, + public readonly email?: string, + public readonly dni?: string, + public readonly property_interest_phone?: string, + public readonly address?: string, + public readonly notes?: string, + public readonly interest_zone?: string, + public readonly purchase_interest?: boolean, + public readonly rental_interest?: boolean, + public readonly property_search_type_id?: number, + public readonly property_search_type?: string, + public readonly city_id?: number, + ) {} + + static create(object: Record): [string?, UpdateClientDto?] { + const { + first_name, + last_name, + phone, + contact_category_id, + contact_category, + email, + dni, + property_interest_phone, + address, + notes, + interest_zone, + purchase_interest, + rental_interest, + property_search_type_id, + property_search_type, + city_id, + } = object; + + if (contact_category_id && contact_category) { + return [ + "Provide either contact_category_id OR contact_category name, not both", + undefined, + ]; + } + + if (contact_category_id && Number.isNaN(Number(contact_category_id))) { + return ["Contact category ID must be a number", undefined]; + } + + if (property_search_type_id && property_search_type) { + return [ + "Provide either property_search_type_id OR property_search_type name, not both", + undefined, + ]; + } + + if ( + property_search_type_id && + Number.isNaN(Number(property_search_type_id)) + ) { + return ["Property search type ID must be a number", undefined]; + } + + if (city_id && Number.isNaN(Number(city_id))) { + return ["City ID must be a number", undefined]; + } + + if (first_name !== undefined) { + if ( + !first_name || + typeof first_name !== "string" || + first_name.trim().length === 0 + ) { + return ["First name cannot be empty", undefined]; + } + const firstNameError = validateFirstName(first_name); + if (firstNameError[0]) return [firstNameError[0], undefined]; + } + + if (last_name !== undefined) { + if ( + !last_name || + typeof last_name !== "string" || + last_name.trim().length === 0 + ) { + return ["Last name cannot be empty", undefined]; + } + const lastNameError = validateLastName(last_name); + if (lastNameError[0]) return [lastNameError[0], undefined]; + } + + if (phone !== undefined && phone !== null) { + if (typeof phone !== "string" || phone.trim().length === 0) { + return ["Phone cannot be empty", undefined]; + } + const phoneError = validatePhone(phone); + if (phoneError[0]) return [phoneError[0], undefined]; + } + + if (email !== undefined && email !== null) { + const emailError = validateEmail(email, false); + if (emailError[0]) return [emailError[0], undefined]; + } + + if ( + property_interest_phone !== undefined && + property_interest_phone !== null + ) { + if ( + typeof property_interest_phone === "string" && + property_interest_phone.trim().length > 0 + ) { + const trimmedInterestPhone = property_interest_phone.trim(); + const validCharsRegex = /^[\d\s\-()+.]+$/; + if (!validCharsRegex.test(trimmedInterestPhone)) { + return [ + "Invalid property interest phone format: phone can only contain numbers, spaces, dashes, parentheses, plus sign, and dots", + undefined, + ]; + } + const interestDigitsOnly = trimmedInterestPhone.replace(/\D/g, ""); + if (interestDigitsOnly.length < 10) { + return [ + "Invalid property interest phone format: phone must contain at least 10 digits", + undefined, + ]; + } + if (interestDigitsOnly.length > 15) { + return [ + "Invalid property interest phone format: phone must contain at most 15 digits", + undefined, + ]; + } + } + } + + if (dni !== undefined && dni !== null) { + const dniError = validateDni(dni); + if (dniError[0]) return [dniError[0], undefined]; + } + + const hasAnyField = + first_name || + last_name || + phone || + contact_category_id || + contact_category || + email !== undefined || + dni !== undefined || + property_interest_phone !== undefined || + address !== undefined || + notes !== undefined || + interest_zone !== undefined || + purchase_interest !== undefined || + rental_interest !== undefined || + property_search_type_id || + property_search_type || + city_id; + + if (!hasAnyField) { + return ["At least one field must be provided for update", undefined]; + } + + return [ + undefined, + new UpdateClientDto( + (first_name as string | undefined)?.trim(), + (last_name as string | undefined)?.trim(), + (phone as string | undefined)?.trim(), + contact_category_id ? Number(contact_category_id) : undefined, + (contact_category as string | undefined)?.trim(), + (email as string | undefined)?.trim(), + (dni as string | undefined)?.trim(), + (property_interest_phone as string | undefined)?.trim(), + (address as string | undefined)?.trim(), + (notes as string | undefined)?.trim(), + (interest_zone as string | undefined)?.trim(), + purchase_interest !== undefined + ? Boolean(purchase_interest) + : undefined, + rental_interest !== undefined ? Boolean(rental_interest) : undefined, + property_search_type_id ? Number(property_search_type_id) : undefined, + (property_search_type as string | undefined)?.trim(), + city_id ? Number(city_id) : undefined, + ), + ]; + } } - - - - diff --git a/backend/src/domain/dtos/consultations/create-general-consultation.dto.ts b/backend/src/domain/dtos/consultations/create-general-consultation.dto.ts index 0c12f53..a0c26df 100644 --- a/backend/src/domain/dtos/consultations/create-general-consultation.dto.ts +++ b/backend/src/domain/dtos/consultations/create-general-consultation.dto.ts @@ -9,9 +9,9 @@ export class CreateGeneralConsultationDto { public readonly email?: string, ) {} - static create(object: { - [key: string]: any; - }): [string?, CreateGeneralConsultationDto?] { + static create( + object: Record, + ): [string?, CreateGeneralConsultationDto?] { const { first_name, last_name, phone, email, message } = object; if (!first_name) return ["First name is required"]; diff --git a/backend/src/domain/dtos/consultations/create-property-consultation.dto.ts b/backend/src/domain/dtos/consultations/create-property-consultation.dto.ts index de801eb..89df4c5 100644 --- a/backend/src/domain/dtos/consultations/create-property-consultation.dto.ts +++ b/backend/src/domain/dtos/consultations/create-property-consultation.dto.ts @@ -10,9 +10,9 @@ export class CreatePropertyConsultationDto { public readonly email?: string, ) {} - static create(object: { - [key: string]: any; - }): [string?, CreatePropertyConsultationDto?] { + static create( + object: Record, + ): [string?, CreatePropertyConsultationDto?] { const { property_id, first_name, last_name, phone, email, message } = object; diff --git a/backend/src/domain/dtos/consultations/delete-multiple-consultations.dto.ts b/backend/src/domain/dtos/consultations/delete-multiple-consultations.dto.ts index b9bdc41..d95c05d 100644 --- a/backend/src/domain/dtos/consultations/delete-multiple-consultations.dto.ts +++ b/backend/src/domain/dtos/consultations/delete-multiple-consultations.dto.ts @@ -1,40 +1,37 @@ export class DeleteMultipleConsultationsDto { - private constructor( - public readonly consultation_ids: number[] - ) {} + private constructor(public readonly consultation_ids: number[]) {} - static create(object: Record): [string?, DeleteMultipleConsultationsDto?] { - const { consultation_ids } = object; + static create( + object: Record, + ): [string?, DeleteMultipleConsultationsDto?] { + const { consultation_ids } = object; - if (!consultation_ids) { - return ['consultation_ids is required']; - } - if (!Array.isArray(consultation_ids)) { - return ['consultation_ids must be an array']; - } + if (!consultation_ids) { + return ["consultation_ids is required"]; + } + if (!Array.isArray(consultation_ids)) { + return ["consultation_ids must be an array"]; + } - if (consultation_ids.length === 0) { - return ['At least one consultation ID is required']; - } - if (consultation_ids.length > 100) { - return ['Cannot delete more than 100 consultations at once']; - } + if (consultation_ids.length === 0) { + return ["At least one consultation ID is required"]; + } + if (consultation_ids.length > 100) { + return ["Cannot delete more than 100 consultations at once"]; + } - for (const id of consultation_ids) { - if (typeof id !== 'number') { - return ['All consultation IDs must be numbers']; - } - if (!Number.isInteger(id)) { - return ['All consultation IDs must be integers']; - } - if (id <= 0) { - return ['All consultation IDs must be positive numbers']; - } - } + for (const id of consultation_ids) { + if (typeof id !== "number") { + return ["All consultation IDs must be numbers"]; + } + if (!Number.isInteger(id)) { + return ["All consultation IDs must be integers"]; + } + if (id <= 0) { + return ["All consultation IDs must be positive numbers"]; + } + } - return [ - undefined, - new DeleteMultipleConsultationsDto(consultation_ids) - ]; - } + return [undefined, new DeleteMultipleConsultationsDto(consultation_ids)]; + } } diff --git a/backend/src/domain/dtos/consultations/index.ts b/backend/src/domain/dtos/consultations/index.ts index bbcc9c1..3a301b1 100644 --- a/backend/src/domain/dtos/consultations/index.ts +++ b/backend/src/domain/dtos/consultations/index.ts @@ -1,3 +1,3 @@ -export * from './create-property-consultation.dto'; -export * from './create-general-consultation.dto'; -export * from './delete-multiple-consultations.dto'; \ No newline at end of file +export * from "./create-general-consultation.dto"; +export * from "./create-property-consultation.dto"; +export * from "./delete-multiple-consultations.dto"; diff --git a/backend/src/domain/dtos/properties/create-property-address.dto.ts b/backend/src/domain/dtos/properties/create-property-address.dto.ts index 8a5f8e4..52f614e 100644 --- a/backend/src/domain/dtos/properties/create-property-address.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-address.dto.ts @@ -1,43 +1,54 @@ - export class CreatePropertyAddressDto { - constructor( - public readonly street: string, - public readonly number?: string, - public readonly neighborhood?: string, - public readonly postal_code?: string, - public readonly latitude?: number, - public readonly longitude?: number, - ) {} - - static create(object: Record): [string?, CreatePropertyAddressDto?] { - const { street, number, neighborhood, postal_code, latitude, longitude } = object; - - if (!street || typeof street !== 'string' || street.trim().length === 0) { - return ['Street is required', undefined]; - } - - if (latitude !== undefined && latitude !== null && (typeof latitude !== 'number' || isNaN(Number(latitude)) || Number(latitude) < -90 || Number(latitude) > 90)) { - return ['Latitude must be a number between -90 and 90', undefined]; - } - if (longitude !== undefined && longitude !== null && (typeof longitude !== 'number' || isNaN(Number(longitude)) || Number(longitude) < -180 || Number(longitude) > 180)) { - return ['Longitude must be a number between -180 and 180', undefined]; - } - - return [ - undefined, - new CreatePropertyAddressDto( - street.trim(), - (number as string | undefined)?.trim(), - (neighborhood as string | undefined)?.trim(), - (postal_code as string | undefined)?.trim(), - latitude !== undefined ? Number(latitude) : undefined, - longitude !== undefined ? Number(longitude) : undefined, - ) - ]; - } + constructor( + public readonly street: string, + public readonly number?: string, + public readonly neighborhood?: string, + public readonly postal_code?: string, + public readonly latitude?: number, + public readonly longitude?: number, + ) {} + + static create( + object: Record, + ): [string?, CreatePropertyAddressDto?] { + const { street, number, neighborhood, postal_code, latitude, longitude } = + object; + + if (!street || typeof street !== "string" || street.trim().length === 0) { + return ["Street is required", undefined]; + } + + if ( + latitude !== undefined && + latitude !== null && + (typeof latitude !== "number" || + Number.isNaN(Number(latitude)) || + Number(latitude) < -90 || + Number(latitude) > 90) + ) { + return ["Latitude must be a number between -90 and 90", undefined]; + } + if ( + longitude !== undefined && + longitude !== null && + (typeof longitude !== "number" || + Number.isNaN(Number(longitude)) || + Number(longitude) < -180 || + Number(longitude) > 180) + ) { + return ["Longitude must be a number between -180 and 180", undefined]; + } + + return [ + undefined, + new CreatePropertyAddressDto( + street.trim(), + (number as string | undefined)?.trim(), + (neighborhood as string | undefined)?.trim(), + (postal_code as string | undefined)?.trim(), + latitude !== undefined ? Number(latitude) : undefined, + longitude !== undefined ? Number(longitude) : undefined, + ), + ]; + } } - - - - - diff --git a/backend/src/domain/dtos/properties/create-property-basic.dto.ts b/backend/src/domain/dtos/properties/create-property-basic.dto.ts index aa74ca4..8173cb6 100644 --- a/backend/src/domain/dtos/properties/create-property-basic.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-basic.dto.ts @@ -1,17 +1,13 @@ export interface CreatePropertyBasicDto { - title: string; - description?: string; - property_type?: string; - property_type_id?: number; - property_status?: string; - property_status_id?: number; - visibility_status?: string; - visibility_status_id?: number; - owner_id?: number; - featured_web?: boolean; - publication_date?: Date | string; + title: string; + description?: string; + property_type?: string; + property_type_id?: number; + property_status?: string; + property_status_id?: number; + visibility_status?: string; + visibility_status_id?: number; + owner_id?: number; + featured_web?: boolean; + publication_date?: Date | string; } - - - - diff --git a/backend/src/domain/dtos/properties/create-property-characteristics.dto.ts b/backend/src/domain/dtos/properties/create-property-characteristics.dto.ts index 82e0d18..837f1a8 100644 --- a/backend/src/domain/dtos/properties/create-property-characteristics.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-characteristics.dto.ts @@ -1,26 +1,23 @@ export interface CreatePropertyCharacteristicsDto { - property_type?: string; - property_type_id?: number; - property_status?: string; - property_status_id?: number; - visibility_status?: string; - visibility_status_id?: number; - - rooms_count?: number; - bedrooms_count?: number; - bathrooms_count?: number; - toilets_count?: number; - parking_spaces_count?: number; - floors_count?: number; - age?: string; - age_id?: number; - orientation?: string; - orientation_id?: number; - disposition?: string; - disposition_id?: number; - situation?: string; - situation_id?: number; -} - - + property_type?: string; + property_type_id?: number; + property_status?: string; + property_status_id?: number; + visibility_status?: string; + visibility_status_id?: number; + rooms_count?: number; + bedrooms_count?: number; + bathrooms_count?: number; + toilets_count?: number; + parking_spaces_count?: number; + floors_count?: number; + age?: string; + age_id?: number; + orientation?: string; + orientation_id?: number; + disposition?: string; + disposition_id?: number; + situation?: string; + situation_id?: number; +} diff --git a/backend/src/domain/dtos/properties/create-property-documents.dto.ts b/backend/src/domain/dtos/properties/create-property-documents.dto.ts index c28c842..adb6059 100644 --- a/backend/src/domain/dtos/properties/create-property-documents.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-documents.dto.ts @@ -1,7 +1,3 @@ export interface CreatePropertyDocumentDto { - document_name: string; + document_name: string; } - - - - diff --git a/backend/src/domain/dtos/properties/create-property-geography.dto.ts b/backend/src/domain/dtos/properties/create-property-geography.dto.ts index 0967098..763c13d 100644 --- a/backend/src/domain/dtos/properties/create-property-geography.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-geography.dto.ts @@ -1,35 +1,40 @@ export class CreatePropertyGeographyDto { - constructor( - public readonly country: string, - public readonly province: string, - public readonly city: string, - ) {} - - static create(object: Record): [string?, CreatePropertyGeographyDto?] { - const { country, province, city } = object; - - if (!country || typeof country !== 'string' || country.trim().length === 0) { - return ['Country is required', undefined]; - } - if (!province || typeof province !== 'string' || province.trim().length === 0) { - return ['Province is required', undefined]; - } - if (!city || typeof city !== 'string' || city.trim().length === 0) { - return ['City is required', undefined]; - } - - return [ - undefined, - new CreatePropertyGeographyDto( - country.trim(), - province.trim(), - city.trim() - ) - ]; - } + constructor( + public readonly country: string, + public readonly province: string, + public readonly city: string, + ) {} + + static create( + object: Record, + ): [string?, CreatePropertyGeographyDto?] { + const { country, province, city } = object; + + if ( + !country || + typeof country !== "string" || + country.trim().length === 0 + ) { + return ["Country is required", undefined]; + } + if ( + !province || + typeof province !== "string" || + province.trim().length === 0 + ) { + return ["Province is required", undefined]; + } + if (!city || typeof city !== "string" || city.trim().length === 0) { + return ["City is required", undefined]; + } + + return [ + undefined, + new CreatePropertyGeographyDto( + country.trim(), + province.trim(), + city.trim(), + ), + ]; + } } - - - - - diff --git a/backend/src/domain/dtos/properties/create-property-grouped.dto.ts b/backend/src/domain/dtos/properties/create-property-grouped.dto.ts index 7df1a98..d14b452 100644 --- a/backend/src/domain/dtos/properties/create-property-grouped.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-grouped.dto.ts @@ -1,182 +1,234 @@ -import { CreatePropertyBasicDto } from './create-property-basic.dto'; -import { CreatePropertyGeographyDto } from './create-property-geography.dto'; -import { CreatePropertyAddressDto } from './create-property-address.dto'; -import { CreatePropertyCharacteristicsDto } from './create-property-characteristics.dto'; -import { CreatePropertySurfaceDto } from './create-property-surface.dto'; -import { CreatePropertyServicesDto } from './create-property-services.dto'; -import { CreatePropertyValuesDto } from './create-property-values.dto'; -import { CreatePropertyInternalDto } from './create-property-internal.dto'; - +import type { CreatePropertyAddressDto } from "./create-property-address.dto"; +import type { CreatePropertyBasicDto } from "./create-property-basic.dto"; +import type { CreatePropertyCharacteristicsDto } from "./create-property-characteristics.dto"; +import type { CreatePropertyGeographyDto } from "./create-property-geography.dto"; +import type { CreatePropertyInternalDto } from "./create-property-internal.dto"; +import type { CreatePropertyServicesDto } from "./create-property-services.dto"; +import type { CreatePropertySurfaceDto } from "./create-property-surface.dto"; +import type { CreatePropertyValuesDto } from "./create-property-values.dto"; export class CreatePropertyGroupedDto { - constructor( - public readonly basic: CreatePropertyBasicDto, - public readonly geography: CreatePropertyGeographyDto, - public readonly address: CreatePropertyAddressDto, - public readonly values: CreatePropertyValuesDto, - public readonly characteristics?: CreatePropertyCharacteristicsDto, - public readonly surface?: CreatePropertySurfaceDto, - public readonly services?: CreatePropertyServicesDto, - public readonly internal?: CreatePropertyInternalDto, - ) {} - - static create(object: Record): [string?, CreatePropertyGroupedDto?] { - try { - const parseJsonField = (field: any, fieldName: string): [string?, any?] => { - if (!field) { - return [undefined, undefined]; - } - - if (typeof field === 'string') { - try { - return [undefined, JSON.parse(field)]; - } catch (error) { - try { - const fixed = field.replace(/'/g, '"'); - return [undefined, JSON.parse(fixed)]; - } catch (e) { - const errorMessage = error instanceof Error ? error.message : 'Invalid JSON format'; - return [`Invalid JSON format in ${fieldName}: ${errorMessage}`, undefined]; - } - } - } - - return [undefined, field]; - }; - - let basic: any = {}; - let geography: any = {}; - let address: any = {}; - let characteristics: CreatePropertyCharacteristicsDto | undefined = undefined; - let surface: CreatePropertySurfaceDto | undefined = undefined; - let services: CreatePropertyServicesDto | undefined = undefined; - let values: CreatePropertyValuesDto = { prices: [] }; - let internal: CreatePropertyInternalDto | undefined = undefined; - - const basicField = object.basic || object.Basic; - if (!basicField) { - return ['basic is required', undefined]; - } - - const [basicError, basicParsed] = parseJsonField(basicField, 'basic'); - if (basicError) { - return [basicError, undefined]; - } - basic = basicParsed || {}; - console.log('[CreatePropertyGroupedDto] basic after parsing:', JSON.stringify(basic)); - console.log('[CreatePropertyGroupedDto] basic.owner_id:', basic.owner_id, 'type:', typeof basic.owner_id); - - if (!basic.title || !basic.title.trim()) { - return ['title is required in basic', undefined]; - } - - const geographyField = object.geography || object.Geography; - if (!geographyField) { - return ['geography is required', undefined]; - } - - const [geographyError, geographyParsed] = parseJsonField(geographyField, 'geography'); - if (geographyError) { - return [geographyError, undefined]; - } - geography = geographyParsed || {}; - - if (!geography.country || !geography.province || !geography.city) { - return ['geography must include country, province and city', undefined]; - } - const addressField = object.address || object.Address; - if (!addressField) { - return ['address is required', undefined]; - } - - const [addressError, addressParsed] = parseJsonField(addressField, 'address'); - if (addressError) { - return [addressError, undefined]; - } - address = addressParsed || {}; - - if (!address.street || !address.street.trim()) { - return ['Address.street is required', undefined]; - } - const valuesField = object.values || object.Values; - if (valuesField) { - const [valuesError, valuesParsed] = parseJsonField(valuesField, 'values'); - if (valuesError) { - return [valuesError, undefined]; - } - values = valuesParsed || { prices: [] }; - } else if (object.prices) { - const [pricesError, pricesParsed] = parseJsonField(object.prices, 'prices'); - if (pricesError) { - return [pricesError, undefined]; - } - values = { prices: Array.isArray(pricesParsed) ? pricesParsed : [pricesParsed] }; - } - - if (!values.prices || !Array.isArray(values.prices) || values.prices.length === 0) { - return ['values.prices is required and must contain at least one price', undefined]; - } - - const characteristicsField = object.characteristics || object.Characteristics; - if (characteristicsField) { - const [charError, charParsed] = parseJsonField(characteristicsField, 'characteristics'); - if (charError) { - return [charError, undefined]; - } - characteristics = charParsed; - } - - const surfaceField = object.surface || object.Surface; - if (surfaceField) { - const [surfaceError, surfaceParsed] = parseJsonField(surfaceField, 'surface'); - if (surfaceError) { - return [surfaceError, undefined]; - } - surface = surfaceParsed; - } - - const servicesField = object.services || object.Services; - if (servicesField) { - const [servicesError, servicesParsed] = parseJsonField(servicesField, 'services'); - if (servicesError) { - return [servicesError, undefined]; - } - services = servicesParsed; - } - - const internalField = object.internal || object.Internal; - if (internalField) { - const [internalError, internalParsed] = parseJsonField(internalField, 'internal'); - if (internalError) { - return [internalError, undefined]; - } - internal = internalParsed; - } - - return [ - undefined, - new CreatePropertyGroupedDto( - basic, - geography, - address, - values, - characteristics, - surface, - services, - internal - ) - ]; - } catch (error) { - console.error('Error parsing CreatePropertyGroupedDto:', error); - console.error('Request body keys:', Object.keys(object)); - console.error('Request body sample:', JSON.stringify(object).substring(0, 500)); - - const message = error instanceof Error ? error.message : 'Error parsing property data. Please check that all JSON fields are properly formatted.'; - return [ - message, - undefined - ]; - } - } + constructor( + public readonly basic: CreatePropertyBasicDto, + public readonly geography: CreatePropertyGeographyDto, + public readonly address: CreatePropertyAddressDto, + public readonly values: CreatePropertyValuesDto, + public readonly characteristics?: CreatePropertyCharacteristicsDto, + public readonly surface?: CreatePropertySurfaceDto, + public readonly services?: CreatePropertyServicesDto, + public readonly internal?: CreatePropertyInternalDto, + ) {} + + static create( + object: Record, + ): [string?, CreatePropertyGroupedDto?] { + try { + const parseJsonField = ( + field: unknown, + fieldName: string, + ): [string?, unknown?] => { + if (!field) { + return [undefined, undefined]; + } + + if (typeof field === "string") { + try { + return [undefined, JSON.parse(field)]; + } catch (error) { + try { + const fixed = field.replace(/'/g, '"'); + return [undefined, JSON.parse(fixed)]; + } catch { + const errorMessage = + error instanceof Error ? error.message : "Invalid JSON format"; + return [ + `Invalid JSON format in ${fieldName}: ${errorMessage}`, + undefined, + ]; + } + } + } + + return [undefined, field]; + }; + + let basic: Record = {}; + let geography: Record = {}; + let address: Record = {}; + let characteristics: CreatePropertyCharacteristicsDto | undefined; + let surface: CreatePropertySurfaceDto | undefined; + let services: CreatePropertyServicesDto | undefined; + let values: CreatePropertyValuesDto = { prices: [] }; + let internal: CreatePropertyInternalDto | undefined; + + const basicField = object.basic || object.Basic; + if (!basicField) { + return ["basic is required", undefined]; + } + + const [basicError, basicParsed] = parseJsonField(basicField, "basic"); + if (basicError) { + return [basicError, undefined]; + } + basic = (basicParsed as Record) || {}; + console.log( + "[CreatePropertyGroupedDto] basic after parsing:", + JSON.stringify(basic), + ); + console.log( + "[CreatePropertyGroupedDto] basic.owner_id:", + basic.owner_id, + "type:", + typeof basic.owner_id, + ); + + if (!basic.title || !(basic.title as string).trim()) { + return ["title is required in basic", undefined]; + } + + const geographyField = object.geography || object.Geography; + if (!geographyField) { + return ["geography is required", undefined]; + } + + const [geographyError, geographyParsed] = parseJsonField( + geographyField, + "geography", + ); + if (geographyError) { + return [geographyError, undefined]; + } + geography = (geographyParsed as Record) || {}; + + if (!geography.country || !geography.province || !geography.city) { + return ["geography must include country, province and city", undefined]; + } + const addressField = object.address || object.Address; + if (!addressField) { + return ["address is required", undefined]; + } + + const [addressError, addressParsed] = parseJsonField( + addressField, + "address", + ); + if (addressError) { + return [addressError, undefined]; + } + address = (addressParsed as Record) || {}; + + if (!address.street || !(address.street as string).trim()) { + return ["Address.street is required", undefined]; + } + const valuesField = object.values || object.Values; + if (valuesField) { + const [valuesError, valuesParsed] = parseJsonField( + valuesField, + "values", + ); + if (valuesError) { + return [valuesError, undefined]; + } + values = (valuesParsed as CreatePropertyValuesDto) || { prices: [] }; + } else if (object.prices) { + const [pricesError, pricesParsed] = parseJsonField( + object.prices, + "prices", + ); + if (pricesError) { + return [pricesError, undefined]; + } + values = { + prices: Array.isArray(pricesParsed) ? pricesParsed : [pricesParsed], + } as CreatePropertyValuesDto; + } + + if ( + !values.prices || + !Array.isArray(values.prices) || + values.prices.length === 0 + ) { + return [ + "values.prices is required and must contain at least one price", + undefined, + ]; + } + + const characteristicsField = + object.characteristics || object.Characteristics; + if (characteristicsField) { + const [charError, charParsed] = parseJsonField( + characteristicsField, + "characteristics", + ); + if (charError) { + return [charError, undefined]; + } + characteristics = charParsed as CreatePropertyCharacteristicsDto; + } + + const surfaceField = object.surface || object.Surface; + if (surfaceField) { + const [surfaceError, surfaceParsed] = parseJsonField( + surfaceField, + "surface", + ); + if (surfaceError) { + return [surfaceError, undefined]; + } + surface = surfaceParsed as CreatePropertySurfaceDto; + } + + const servicesField = object.services || object.Services; + if (servicesField) { + const [servicesError, servicesParsed] = parseJsonField( + servicesField, + "services", + ); + if (servicesError) { + return [servicesError, undefined]; + } + services = servicesParsed as CreatePropertyServicesDto; + } + + const internalField = object.internal || object.Internal; + if (internalField) { + const [internalError, internalParsed] = parseJsonField( + internalField, + "internal", + ); + if (internalError) { + return [internalError, undefined]; + } + internal = internalParsed as CreatePropertyInternalDto; + } + + return [ + undefined, + new CreatePropertyGroupedDto( + basic as unknown as CreatePropertyBasicDto, + geography as unknown as CreatePropertyGeographyDto, + address as unknown as CreatePropertyAddressDto, + values, + characteristics, + surface, + services, + internal, + ), + ]; + } catch (error) { + console.error("Error parsing CreatePropertyGroupedDto:", error); + console.error("Request body keys:", Object.keys(object)); + console.error( + "Request body sample:", + JSON.stringify(object).substring(0, 500), + ); + + const message = + error instanceof Error + ? error.message + : "Error parsing property data. Please check that all JSON fields are properly formatted."; + return [message, undefined]; + } + } } - diff --git a/backend/src/domain/dtos/properties/create-property-internal.dto.ts b/backend/src/domain/dtos/properties/create-property-internal.dto.ts index fc1c542..ce1bbed 100644 --- a/backend/src/domain/dtos/properties/create-property-internal.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-internal.dto.ts @@ -1,15 +1,11 @@ export interface CreatePropertyInternalDto { - branch_name?: string; - appraiser?: string; - producer?: string; - maintenance_user?: string; - keys_location?: string; - internal_comments?: string; - social_media_info?: string; - operation_commission_percentage?: number; - producer_commission_percentage?: number; + branch_name?: string; + appraiser?: string; + producer?: string; + maintenance_user?: string; + keys_location?: string; + internal_comments?: string; + social_media_info?: string; + operation_commission_percentage?: number; + producer_commission_percentage?: number; } - - - - diff --git a/backend/src/domain/dtos/properties/create-property-price.dto.ts b/backend/src/domain/dtos/properties/create-property-price.dto.ts index 9ed6cc6..e265d79 100644 --- a/backend/src/domain/dtos/properties/create-property-price.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-price.dto.ts @@ -1,78 +1,113 @@ - export class CreatePropertyPriceDto { - constructor( - public readonly price: number, - public readonly currency_type_id?: number, - public readonly currency_symbol?: string, - public readonly operation_type_id?: number, - public readonly operation_type?: string, - ) {} + constructor( + public readonly price: number, + public readonly currency_type_id?: number, + public readonly currency_symbol?: string, + public readonly operation_type_id?: number, + public readonly operation_type?: string, + ) {} - static create(object: Record): [string?, CreatePropertyPriceDto?] { - const { - price, - currency_type_id, - currency, - currency_symbol, - operation_type_id, - operation_type - } = object; + static create( + object: Record, + ): [string?, CreatePropertyPriceDto?] { + const { + price, + currency_type_id, + currency, + currency_symbol, + operation_type_id, + operation_type, + } = object; - if (price === undefined || price === null) { - return ['Price is required', undefined]; - } - if (isNaN(Number(price)) || Number(price) <= 0) { - return ['Price must be a positive number', undefined]; - } + if (price === undefined || price === null) { + return ["Price is required", undefined]; + } + if (Number.isNaN(Number(price)) || Number(price) <= 0) { + return ["Price must be a positive number", undefined]; + } - const hasCurrencyId = currency_type_id !== undefined && currency_type_id !== null; - const hasCurrencySymbol = (currency_symbol || currency) !== undefined && (currency_symbol || currency) !== null; - - if (!hasCurrencyId && !hasCurrencySymbol) { - return ['Currency type ID or currency symbol (ARS, USD, EUR) is required', undefined]; - } + const hasCurrencyId = + currency_type_id !== undefined && currency_type_id !== null; + const hasCurrencySymbol = + (currency_symbol || currency) !== undefined && + (currency_symbol || currency) !== null; - if (hasCurrencyId && hasCurrencySymbol) { - return ['Provide either currency_type_id OR currency symbol, not both', undefined]; - } + if (!hasCurrencyId && !hasCurrencySymbol) { + return [ + "Currency type ID or currency symbol (ARS, USD, EUR) is required", + undefined, + ]; + } - if (hasCurrencyId && (isNaN(Number(currency_type_id)) || Number(currency_type_id) <= 0)) { - return ['Currency type ID must be a positive number', undefined]; - } + if (hasCurrencyId && hasCurrencySymbol) { + return [ + "Provide either currency_type_id OR currency symbol, not both", + undefined, + ]; + } - if (hasCurrencySymbol && typeof (currency_symbol || currency) !== 'string') { - return ['Currency symbol must be a string (e.g., "ARS", "USD", "EUR")', undefined]; - } + if ( + hasCurrencyId && + (Number.isNaN(Number(currency_type_id)) || Number(currency_type_id) <= 0) + ) { + return ["Currency type ID must be a positive number", undefined]; + } - const hasOperationId = operation_type_id !== undefined && operation_type_id !== null; - const hasOperationName = operation_type !== undefined && operation_type !== null; - - if (!hasOperationId && !hasOperationName) { - return ['Operation type ID or operation type name (Venta, Alquiler, etc.) is required', undefined]; - } + if ( + hasCurrencySymbol && + typeof (currency_symbol || currency) !== "string" + ) { + return [ + 'Currency symbol must be a string (e.g., "ARS", "USD", "EUR")', + undefined, + ]; + } - if (hasOperationId && hasOperationName) { - return ['Provide either operation_type_id OR operation_type name, not both', undefined]; - } + const hasOperationId = + operation_type_id !== undefined && operation_type_id !== null; + const hasOperationName = + operation_type !== undefined && operation_type !== null; - if (hasOperationId && (isNaN(Number(operation_type_id)) || Number(operation_type_id) <= 0)) { - return ['Operation type ID must be a positive number', undefined]; - } + if (!hasOperationId && !hasOperationName) { + return [ + "Operation type ID or operation type name (Venta, Alquiler, etc.) is required", + undefined, + ]; + } - if (hasOperationName && typeof operation_type !== 'string') { - return ['Operation type must be a string (e.g., "Venta", "Alquiler")', undefined]; - } + if (hasOperationId && hasOperationName) { + return [ + "Provide either operation_type_id OR operation_type name, not both", + undefined, + ]; + } - return [ - undefined, - new CreatePropertyPriceDto( - Number(price), - hasCurrencyId ? Number(currency_type_id) : undefined, - hasCurrencySymbol ? ((currency_symbol || currency) as string) : undefined, - hasOperationId ? Number(operation_type_id) : undefined, - hasOperationName ? (operation_type as string) : undefined, - ) - ]; - } -} + if ( + hasOperationId && + (Number.isNaN(Number(operation_type_id)) || + Number(operation_type_id) <= 0) + ) { + return ["Operation type ID must be a positive number", undefined]; + } + if (hasOperationName && typeof operation_type !== "string") { + return [ + 'Operation type must be a string (e.g., "Venta", "Alquiler")', + undefined, + ]; + } + + return [ + undefined, + new CreatePropertyPriceDto( + Number(price), + hasCurrencyId ? Number(currency_type_id) : undefined, + hasCurrencySymbol + ? ((currency_symbol || currency) as string) + : undefined, + hasOperationId ? Number(operation_type_id) : undefined, + hasOperationName ? (operation_type as string) : undefined, + ), + ]; + } +} diff --git a/backend/src/domain/dtos/properties/create-property-services.dto.ts b/backend/src/domain/dtos/properties/create-property-services.dto.ts index f4e3978..1a8b641 100644 --- a/backend/src/domain/dtos/properties/create-property-services.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-services.dto.ts @@ -1,8 +1,3 @@ - export interface CreatePropertyServicesDto { - services?: string[]; + services?: string[]; } - - - - diff --git a/backend/src/domain/dtos/properties/create-property-surface.dto.ts b/backend/src/domain/dtos/properties/create-property-surface.dto.ts index 424d5f7..7c45d5d 100644 --- a/backend/src/domain/dtos/properties/create-property-surface.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-surface.dto.ts @@ -1,14 +1,9 @@ - export interface CreatePropertySurfaceDto { - land_area?: number; - semi_covered_area?: number; - covered_area?: number; - total_built_area?: number; - uncovered_area?: number; - total_area?: number; - zoning?: string; + land_area?: number; + semi_covered_area?: number; + covered_area?: number; + total_built_area?: number; + uncovered_area?: number; + total_area?: number; + zoning?: string; } - - - - diff --git a/backend/src/domain/dtos/properties/create-property-values.dto.ts b/backend/src/domain/dtos/properties/create-property-values.dto.ts index 0cfd64e..5562634 100644 --- a/backend/src/domain/dtos/properties/create-property-values.dto.ts +++ b/backend/src/domain/dtos/properties/create-property-values.dto.ts @@ -1,18 +1,13 @@ -import { CreatePropertyPriceDto } from './create-property-price.dto'; - +import type { CreatePropertyPriceDto } from "./create-property-price.dto"; export interface CreatePropertyExpenseDto { - amount: number; - currency_symbol?: string; - currency_type_id?: number; - frequency?: string; + amount: number; + currency_symbol?: string; + currency_type_id?: number; + frequency?: string; } export interface CreatePropertyValuesDto { - prices: CreatePropertyPriceDto[]; - expenses?: CreatePropertyExpenseDto[]; + prices: CreatePropertyPriceDto[]; + expenses?: CreatePropertyExpenseDto[]; } - - - - diff --git a/backend/src/domain/dtos/properties/create-property.dto.ts b/backend/src/domain/dtos/properties/create-property.dto.ts index cf48dc9..e68c10f 100644 --- a/backend/src/domain/dtos/properties/create-property.dto.ts +++ b/backend/src/domain/dtos/properties/create-property.dto.ts @@ -1,301 +1,363 @@ -import { CreatePropertyGeographyDto } from './create-property-geography.dto'; -import { CreatePropertyAddressDto } from './create-property-address.dto'; -import { CreatePropertyPriceDto } from './create-property-price.dto'; +import { CreatePropertyAddressDto } from "./create-property-address.dto"; +import { CreatePropertyGeographyDto } from "./create-property-geography.dto"; +import { CreatePropertyPriceDto } from "./create-property-price.dto"; export class CreatePropertyDto { - constructor( - public readonly title: string, - public readonly property_type_id: number | undefined, - public readonly property_type: string | undefined, - public readonly property_status_id: number | undefined, - public readonly property_status: string | undefined, - public readonly visibility_status_id: number | undefined, - public readonly visibility_status: string | undefined, - - public readonly geography: CreatePropertyGeographyDto, - public readonly address: CreatePropertyAddressDto, - - public readonly prices: CreatePropertyPriceDto[], - public readonly owner_id?: number, - public readonly description?: string, - public readonly publication_date?: Date | string, - public readonly featured_web?: boolean, - public readonly bedrooms_count?: number, - public readonly bathrooms_count?: number, - public readonly rooms_count?: number, - public readonly toilets_count?: number, - public readonly parking_spaces_count?: number, - public readonly floors_count?: number, - public readonly land_area?: number, - public readonly semi_covered_area?: number, - public readonly covered_area?: number, - public readonly total_built_area?: number, - public readonly uncovered_area?: number, - public readonly total_area?: number, - public readonly zoning?: string, - public readonly situation_id?: number, - public readonly situation?: string, - public readonly age_id?: number, - public readonly age?: string, - public readonly orientation_id?: number, - public readonly orientation?: string, - public readonly disposition_id?: number, - public readonly disposition?: string, - public readonly branch_name?: string, - public readonly appraiser?: string, - public readonly producer?: string, - public readonly maintenance_user?: string, - public readonly keys_location?: string, - public readonly internal_comments?: string, - public readonly social_media_info?: string, - public readonly operation_commission_percentage?: number, - public readonly producer_commission_percentage?: number, - ) {} + constructor( + public readonly title: string, + public readonly property_type_id: number | undefined, + public readonly property_type: string | undefined, + public readonly property_status_id: number | undefined, + public readonly property_status: string | undefined, + public readonly visibility_status_id: number | undefined, + public readonly visibility_status: string | undefined, - static create(object: Record): [string?, CreatePropertyDto?] { - let propertyDetails: any = {}; - if (typeof object.propertyDetails === 'string') { - try { - propertyDetails = JSON.parse(object.propertyDetails); - } catch (error) { - return ['Invalid propertyDetails JSON format', undefined]; - } - } else if (object.propertyDetails) { - propertyDetails = object.propertyDetails; - } + public readonly geography: CreatePropertyGeographyDto, + public readonly address: CreatePropertyAddressDto, - let geographyData: any = {}; - if (typeof object.geography === 'string') { - try { - geographyData = JSON.parse(object.geography); - } catch (error) { - return ['Invalid geography JSON format', undefined]; - } - } else if (object.geography) { - geographyData = object.geography; - } + public readonly prices: CreatePropertyPriceDto[], + public readonly owner_id?: number, + public readonly description?: string, + public readonly publication_date?: Date | string, + public readonly featured_web?: boolean, + public readonly bedrooms_count?: number, + public readonly bathrooms_count?: number, + public readonly rooms_count?: number, + public readonly toilets_count?: number, + public readonly parking_spaces_count?: number, + public readonly floors_count?: number, + public readonly land_area?: number, + public readonly semi_covered_area?: number, + public readonly covered_area?: number, + public readonly total_built_area?: number, + public readonly uncovered_area?: number, + public readonly total_area?: number, + public readonly zoning?: string, + public readonly situation_id?: number, + public readonly situation?: string, + public readonly age_id?: number, + public readonly age?: string, + public readonly orientation_id?: number, + public readonly orientation?: string, + public readonly disposition_id?: number, + public readonly disposition?: string, + public readonly branch_name?: string, + public readonly appraiser?: string, + public readonly producer?: string, + public readonly maintenance_user?: string, + public readonly keys_location?: string, + public readonly internal_comments?: string, + public readonly social_media_info?: string, + public readonly operation_commission_percentage?: number, + public readonly producer_commission_percentage?: number, + ) {} - let addressData: any = {}; - if (typeof object.address === 'string') { - try { - addressData = JSON.parse(object.address); - } catch (error) { - return ['Invalid address JSON format', undefined]; - } - } else if (object.address) { - addressData = object.address; - } + static create( + object: Record, + ): [string?, CreatePropertyDto?] { + let propertyDetails: Record = {}; + if (typeof object.propertyDetails === "string") { + try { + propertyDetails = JSON.parse(object.propertyDetails); + } catch { + return ["Invalid propertyDetails format", undefined]; + } + } else if ( + typeof object.propertyDetails === "object" && + object.propertyDetails !== null + ) { + propertyDetails = object.propertyDetails as Record; + } - let pricesData: any[] = []; - if (typeof object.prices === 'string') { - try { - pricesData = JSON.parse(object.prices); - } catch (error) { - return ['Invalid prices JSON format', undefined]; - } - } else if (Array.isArray(object.prices)) { - pricesData = object.prices; - } else if (object.prices) { - pricesData = [object.prices]; - } + let geographyData: Record = {}; + if (typeof object.geography === "string") { + try { + geographyData = JSON.parse(object.geography); + } catch { + return ["Invalid geography JSON format", undefined]; + } + } else if ( + typeof object.geography === "object" && + object.geography !== null + ) { + geographyData = object.geography as Record; + } - const title = propertyDetails.title || object.title; - if (!title || title.trim().length === 0) { - return ['Title is required', undefined]; - } - const propertyTypeId = propertyDetails.property_type_id || object.property_type_id; - const propertyTypeName = propertyDetails.property_type || object.property_type; - const hasPropertyTypeId = propertyTypeId !== undefined && propertyTypeId !== null; - const hasPropertyTypeName = propertyTypeName !== undefined && propertyTypeName !== null; - - if (!hasPropertyTypeId && !hasPropertyTypeName) { - return ['Property type ID or property type name is required', undefined]; - } - if (hasPropertyTypeId && hasPropertyTypeName) { - return ['Provide either property_type_id OR property_type name, not both', undefined]; - } - if (hasPropertyTypeId && isNaN(Number(propertyTypeId))) { - return ['Property type ID must be a number', undefined]; - } + let addressData: Record = {}; + if (typeof object.address === "string") { + try { + addressData = JSON.parse(object.address); + } catch { + return ["Invalid address JSON format", undefined]; + } + } else if (typeof object.address === "object" && object.address !== null) { + addressData = object.address as Record; + } - const propertyStatusId = propertyDetails.property_status_id || object.property_status_id; - const propertyStatusName = propertyDetails.property_status || object.property_status; - const hasPropertyStatusId = propertyStatusId !== undefined && propertyStatusId !== null; - const hasPropertyStatusName = propertyStatusName !== undefined && propertyStatusName !== null; - - if (!hasPropertyStatusId && !hasPropertyStatusName) { - return ['Property status ID or property status name is required', undefined]; - } - if (hasPropertyStatusId && hasPropertyStatusName) { - return ['Provide either property_status_id OR property_status name, not both', undefined]; - } - if (hasPropertyStatusId && isNaN(Number(propertyStatusId))) { - return ['Property status ID must be a number', undefined]; - } + let pricesData: unknown[] = []; + if (typeof object.prices === "string") { + try { + const parsed = JSON.parse(object.prices); + if (Array.isArray(parsed)) { + pricesData = parsed; + } else { + return ["Prices must be an array", undefined]; + } + } catch { + return ["Invalid prices JSON format", undefined]; + } + } else if (Array.isArray(object.prices)) { + pricesData = object.prices; + } - const visibilityStatusId = propertyDetails.visibility_status_id || object.visibility_status_id; - const visibilityStatusName = propertyDetails.visibility_status || object.visibility_status; - const hasVisibilityStatusId = visibilityStatusId !== undefined && visibilityStatusId !== null; - const hasVisibilityStatusName = visibilityStatusName !== undefined && visibilityStatusName !== null; - - if (!hasVisibilityStatusId && !hasVisibilityStatusName) { - return ['Visibility status ID or visibility status name is required', undefined]; - } - if (hasVisibilityStatusId && hasVisibilityStatusName) { - return ['Provide either visibility_status_id OR visibility_status name, not both', undefined]; - } - if (hasVisibilityStatusId && isNaN(Number(visibilityStatusId))) { - return ['Visibility status ID must be a number', undefined]; - } + // Helper to safely get string from either source + const getString = (key: string): string | undefined => { + const val = propertyDetails[key] || object[key]; + return typeof val === "string" ? val : undefined; + }; + // Helper to safely get number from either source + const getNumber = (key: string): number | undefined => { + const val = propertyDetails[key] || object[key]; + if (typeof val === "number") return val; + if (typeof val === "string" && val.trim() !== "") { + const num = Number(val); + return Number.isNaN(num) ? undefined : num; + } + return undefined; + }; - const [geoError, geography] = CreatePropertyGeographyDto.create(geographyData); - if (geoError || !geography) { - return [geoError || 'Invalid geography data', undefined]; - } - const [addrError, address] = CreatePropertyAddressDto.create(addressData); - if (addrError || !address) { - return [addrError || 'Invalid address data', undefined]; - } - if (!pricesData || pricesData.length === 0) { - return ['At least one price is required', undefined]; - } - const ownerId = propertyDetails.owner_id || object.owner_id; - if (ownerId !== undefined && ownerId !== null && isNaN(Number(ownerId))) { - return ['Owner ID must be a number if provided. This should be a client (propietario) ID.', undefined]; - } + const title = getString("title"); + if (!title || title.trim().length === 0) { + return ["Title is required", undefined]; + } - const prices: CreatePropertyPriceDto[] = []; - for (let i = 0; i < pricesData.length; i++) { - const [priceError, price] = CreatePropertyPriceDto.create(pricesData[i]); - if (priceError || !price) { - return [`Price ${i + 1}: ${priceError || 'Invalid price data'}`, undefined]; - } - prices.push(price); - } + const propertyTypeId = getNumber("property_type_id"); + const propertyTypeName = getString("property_type"); + const hasPropertyTypeId = propertyTypeId !== undefined; + const hasPropertyTypeName = propertyTypeName !== undefined; - const validateOptionalNumber = (value: any, fieldName: string): number | undefined => { - if (value === undefined || value === null || value === '') { - return undefined; - } - const num = Number(value); - if (isNaN(num) || num < 0) { - throw new Error(`${fieldName} must be a positive number`); - } - return num; - }; + if (!hasPropertyTypeId && !hasPropertyTypeName) { + return ["Property type ID or property type name is required", undefined]; + } + if (hasPropertyTypeId && hasPropertyTypeName) { + return [ + "Provide either property_type_id OR property_type name, not both", + undefined, + ]; + } - try { - const situationId = propertyDetails.situation_id || object.situation_id; - const situationName = propertyDetails.situation || object.situation; - const hasSituationId = situationId !== undefined && situationId !== null; - const hasSituationName = situationName !== undefined && situationName !== null; - if (hasSituationId && hasSituationName) { - return ['Provide either situation_id OR situation name, not both', undefined]; - } + const propertyStatusId = getNumber("property_status_id"); + const propertyStatusName = getString("property_status"); + const hasPropertyStatusId = propertyStatusId !== undefined; + const hasPropertyStatusName = propertyStatusName !== undefined; - const ageId = propertyDetails.age_id || object.age_id; - const ageName = propertyDetails.age || object.age; - const hasAgeId = ageId !== undefined && ageId !== null; - const hasAgeName = ageName !== undefined && ageName !== null; - if (hasAgeId && hasAgeName) { - return ['Provide either age_id OR age name, not both', undefined]; - } + if (!hasPropertyStatusId && !hasPropertyStatusName) { + return [ + "Property status ID or property status name is required", + undefined, + ]; + } + if (hasPropertyStatusId && hasPropertyStatusName) { + return [ + "Provide either property_status_id OR property_status name, not both", + undefined, + ]; + } - const orientationId = propertyDetails.orientation_id || object.orientation_id; - const orientationName = propertyDetails.orientation || object.orientation; - const hasOrientationId = orientationId !== undefined && orientationId !== null; - const hasOrientationName = orientationName !== undefined && orientationName !== null; - if (hasOrientationId && hasOrientationName) { - return ['Provide either orientation_id OR orientation name, not both', undefined]; - } + const visibilityStatusId = getNumber("visibility_status_id"); + const visibilityStatusName = getString("visibility_status"); + const hasVisibilityStatusId = visibilityStatusId !== undefined; + const hasVisibilityStatusName = visibilityStatusName !== undefined; - const dispositionId = propertyDetails.disposition_id || object.disposition_id; - const dispositionName = propertyDetails.disposition || object.disposition; - const hasDispositionId = dispositionId !== undefined && dispositionId !== null; - const hasDispositionName = dispositionName !== undefined && dispositionName !== null; - if (hasDispositionId && hasDispositionName) { - return ['Provide either disposition_id OR disposition name, not both', undefined]; - } + if (!hasVisibilityStatusId && !hasVisibilityStatusName) { + return [ + "Visibility status ID or visibility status name is required", + undefined, + ]; + } + if (hasVisibilityStatusId && hasVisibilityStatusName) { + return [ + "Provide either visibility_status_id OR visibility_status name, not both", + undefined, + ]; + } - let publicationDate: Date | undefined = undefined; - if (propertyDetails.publication_date || object.publication_date) { - const dateValue = propertyDetails.publication_date || object.publication_date; - if (typeof dateValue === 'string') { - publicationDate = new Date(dateValue); - if (isNaN(publicationDate.getTime())) { - return ['Invalid publication_date format. Use ISO date string (YYYY-MM-DD)', undefined]; - } - } else if (dateValue instanceof Date) { - publicationDate = dateValue; - } - } + const [geoError, geography] = + CreatePropertyGeographyDto.create(geographyData); + if (geoError || !geography) { + return [geoError || "Invalid geography data", undefined]; + } + const [addrError, address] = CreatePropertyAddressDto.create(addressData); + if (addrError || !address) { + return [addrError || "Invalid address data", undefined]; + } + if (!pricesData || pricesData.length === 0) { + return ["At least one price is required", undefined]; + } - let featuredWeb: boolean | undefined = undefined; - if (propertyDetails.featured_web !== undefined || object.featured_web !== undefined) { - const value = propertyDetails.featured_web !== undefined ? propertyDetails.featured_web : object.featured_web; - if (typeof value === 'boolean') { - featuredWeb = value; - } else if (typeof value === 'string') { - featuredWeb = value.toLowerCase() === 'true'; - } else { - featuredWeb = Boolean(value); - } - } + const ownerId = getNumber("owner_id"); - return [ - undefined, - new CreatePropertyDto( - title.trim(), - hasPropertyTypeId ? Number(propertyTypeId) : undefined, - hasPropertyTypeName ? propertyTypeName.trim() : undefined, - hasPropertyStatusId ? Number(propertyStatusId) : undefined, - hasPropertyStatusName ? propertyStatusName.trim() : undefined, - hasVisibilityStatusId ? Number(visibilityStatusId) : undefined, - hasVisibilityStatusName ? visibilityStatusName.trim() : undefined, - geography, - address, - prices, - ownerId ? Number(ownerId) : undefined, - (propertyDetails.description as string | undefined)?.trim() || (object.description as string | undefined)?.trim(), - publicationDate, - featuredWeb, - validateOptionalNumber(propertyDetails.bedrooms_count || object.bedrooms_count, 'bedrooms_count'), - validateOptionalNumber(propertyDetails.bathrooms_count || object.bathrooms_count, 'bathrooms_count'), - validateOptionalNumber(propertyDetails.rooms_count || object.rooms_count, 'rooms_count'), - validateOptionalNumber(propertyDetails.toilets_count || object.toilets_count, 'toilets_count'), - validateOptionalNumber(propertyDetails.parking_spaces_count || object.parking_spaces_count, 'parking_spaces_count'), - validateOptionalNumber(propertyDetails.floors_count || object.floors_count, 'floors_count'), - validateOptionalNumber(propertyDetails.land_area || object.land_area, 'land_area'), - validateOptionalNumber(propertyDetails.semi_covered_area || object.semi_covered_area, 'semi_covered_area'), - validateOptionalNumber(propertyDetails.covered_area || object.covered_area, 'covered_area'), - validateOptionalNumber(propertyDetails.total_built_area || object.total_built_area, 'total_built_area'), - validateOptionalNumber(propertyDetails.uncovered_area || object.uncovered_area, 'uncovered_area'), - validateOptionalNumber(propertyDetails.total_area || object.total_area, 'total_area'), - (propertyDetails.zoning as string | undefined)?.trim() || (object.zoning as string | undefined)?.trim(), - hasSituationId ? Number(situationId) : undefined, - hasSituationName ? situationName.trim() : undefined, - hasAgeId ? Number(ageId) : undefined, - hasAgeName ? ageName.trim() : undefined, - hasOrientationId ? Number(orientationId) : undefined, - hasOrientationName ? orientationName.trim() : undefined, - hasDispositionId ? Number(dispositionId) : undefined, - hasDispositionName ? dispositionName.trim() : undefined, - (propertyDetails.branch_name as string | undefined)?.trim() || (object.branch_name as string | undefined)?.trim(), - (propertyDetails.appraiser as string | undefined)?.trim() || (object.appraiser as string | undefined)?.trim(), - (propertyDetails.producer as string | undefined)?.trim() || (object.producer as string | undefined)?.trim(), - (propertyDetails.maintenance_user as string | undefined)?.trim() || (object.maintenance_user as string | undefined)?.trim(), - (propertyDetails.keys_location as string | undefined)?.trim() || (object.keys_location as string | undefined)?.trim(), - (propertyDetails.internal_comments as string | undefined)?.trim() || (object.internal_comments as string | undefined)?.trim(), - (propertyDetails.social_media_info as string | undefined)?.trim() || (object.social_media_info as string | undefined)?.trim(), - propertyDetails.operation_commission_percentage ? Number(propertyDetails.operation_commission_percentage) : undefined, - propertyDetails.producer_commission_percentage ? Number(propertyDetails.producer_commission_percentage) : undefined, - ) - ]; - } catch (error) { - const message = error instanceof Error ? error.message : 'Invalid property data'; - return [message, undefined]; - } - } -} + const prices: CreatePropertyPriceDto[] = []; + for (let i = 0; i < pricesData.length; i++) { + const item = pricesData[i]; + if (typeof item !== "object" || item === null) { + return [`Price ${i + 1}: Invalid format`, undefined]; + } + const [priceError, price] = CreatePropertyPriceDto.create( + item as Record, + ); + if (priceError || !price) { + return [ + `Price ${i + 1}: ${priceError || "Invalid price data"}`, + undefined, + ]; + } + prices.push(price); + } + + const validateOptionalNumber = (key: string): number | undefined => { + const val = propertyDetails[key] || object[key]; + if (val === undefined || val === null || val === "") { + return undefined; + } + const num = Number(val); + if (Number.isNaN(num) || num < 0) { + // We can't throw here because we want to return the error message pair + // But original code threw error. We will mimic strict check by returning undefined if invalid? + // Actually the original code threw error which was caught later. + // Let's throw to match structure or handle gracefully. + throw new Error(`${key} must be a positive number`); + } + return num; + }; + + try { + const situationId = getNumber("situation_id"); + const situationName = getString("situation"); + const hasSituationId = situationId !== undefined; + const hasSituationName = situationName !== undefined; + if (hasSituationId && hasSituationName) { + return [ + "Provide either situation_id OR situation name, not both", + undefined, + ]; + } + + const ageId = getNumber("age_id"); + const ageName = getString("age"); + const hasAgeId = ageId !== undefined; + const hasAgeName = ageName !== undefined; + if (hasAgeId && hasAgeName) { + return ["Provide either age_id OR age name, not both", undefined]; + } + + const orientationId = getNumber("orientation_id"); + const orientationName = getString("orientation"); + const hasOrientationId = orientationId !== undefined; + const hasOrientationName = orientationName !== undefined; + if (hasOrientationId && hasOrientationName) { + return [ + "Provide either orientation_id OR orientation name, not both", + undefined, + ]; + } + const dispositionId = getNumber("disposition_id"); + const dispositionName = getString("disposition"); + const hasDispositionId = dispositionId !== undefined; + const hasDispositionName = dispositionName !== undefined; + if (hasDispositionId && hasDispositionName) { + return [ + "Provide either disposition_id OR disposition name, not both", + undefined, + ]; + } + + let publicationDate: Date | undefined; + const dateValue = + propertyDetails.publication_date || object.publication_date; + if (dateValue) { + if (typeof dateValue === "string") { + publicationDate = new Date(dateValue); + if (Number.isNaN(publicationDate.getTime())) { + return [ + "Invalid publication_date format. Use ISO date string (YYYY-MM-DD)", + undefined, + ]; + } + } else if (dateValue instanceof Date) { + publicationDate = dateValue; + } + } + + let featuredWeb: boolean | undefined; + const featVal = + propertyDetails.featured_web !== undefined + ? propertyDetails.featured_web + : object.featured_web; + + if (featVal !== undefined) { + if (typeof featVal === "boolean") { + featuredWeb = featVal; + } else if (typeof featVal === "string") { + featuredWeb = featVal.toLowerCase() === "true"; + } + } + + return [ + undefined, + new CreatePropertyDto( + title?.trim(), + propertyTypeId, + propertyTypeName?.trim(), + propertyStatusId, + propertyStatusName?.trim(), + visibilityStatusId, + visibilityStatusName?.trim(), + geography, + address, + prices, + ownerId, + getString("description")?.trim(), + publicationDate, + featuredWeb, + validateOptionalNumber("bedrooms_count"), + validateOptionalNumber("bathrooms_count"), + validateOptionalNumber("rooms_count"), + validateOptionalNumber("toilets_count"), + validateOptionalNumber("parking_spaces_count"), + validateOptionalNumber("floors_count"), + validateOptionalNumber("land_area"), + validateOptionalNumber("semi_covered_area"), + validateOptionalNumber("covered_area"), + validateOptionalNumber("total_built_area"), + validateOptionalNumber("uncovered_area"), + validateOptionalNumber("total_area"), + getString("zoning")?.trim(), + situationId, + situationName?.trim(), + ageId, + ageName?.trim(), + orientationId, + orientationName?.trim(), + dispositionId, + dispositionName?.trim(), + getString("branch_name")?.trim(), + getString("appraiser")?.trim(), + getString("producer")?.trim(), + getString("maintenance_user")?.trim(), + getString("keys_location")?.trim(), + getString("internal_comments")?.trim(), + getString("social_media_info")?.trim(), + validateOptionalNumber("operation_commission_percentage"), + validateOptionalNumber("producer_commission_percentage"), + ), + ]; + } catch (error) { + const message = + error instanceof Error ? error.message : "Invalid property data"; + return [message, undefined]; + } + } +} diff --git a/backend/src/domain/dtos/properties/index.ts b/backend/src/domain/dtos/properties/index.ts index 1210f1d..c86ce89 100644 --- a/backend/src/domain/dtos/properties/index.ts +++ b/backend/src/domain/dtos/properties/index.ts @@ -1,15 +1,13 @@ -export * from './create-property.dto'; -export * from './create-property-address.dto'; -export * from './create-property-geography.dto'; -export * from './create-property-price.dto'; - -export * from './create-property-basic.dto'; -export * from './create-property-characteristics.dto'; -export * from './create-property-surface.dto'; -export * from './create-property-services.dto'; -export * from './create-property-values.dto'; -export * from './create-property-documents.dto'; -export * from './create-property-internal.dto'; -export * from './create-property-grouped.dto'; -export * from './update-property-grouped.dto'; - +export * from "./create-property.dto"; +export * from "./create-property-address.dto"; +export * from "./create-property-basic.dto"; +export * from "./create-property-characteristics.dto"; +export * from "./create-property-documents.dto"; +export * from "./create-property-geography.dto"; +export * from "./create-property-grouped.dto"; +export * from "./create-property-internal.dto"; +export * from "./create-property-price.dto"; +export * from "./create-property-services.dto"; +export * from "./create-property-surface.dto"; +export * from "./create-property-values.dto"; +export * from "./update-property-grouped.dto"; diff --git a/backend/src/domain/dtos/properties/update-property-grouped.dto.ts b/backend/src/domain/dtos/properties/update-property-grouped.dto.ts index 63f2c39..3dffff1 100644 --- a/backend/src/domain/dtos/properties/update-property-grouped.dto.ts +++ b/backend/src/domain/dtos/properties/update-property-grouped.dto.ts @@ -1,188 +1,237 @@ -import { CreatePropertyBasicDto } from './create-property-basic.dto'; -import { CreatePropertyGeographyDto } from './create-property-geography.dto'; -import { CreatePropertyAddressDto } from './create-property-address.dto'; -import { CreatePropertyCharacteristicsDto } from './create-property-characteristics.dto'; -import { CreatePropertySurfaceDto } from './create-property-surface.dto'; -import { CreatePropertyServicesDto } from './create-property-services.dto'; -import { CreatePropertyValuesDto } from './create-property-values.dto'; -import { CreatePropertyInternalDto } from './create-property-internal.dto'; +import type { CreatePropertyAddressDto } from "./create-property-address.dto"; +import type { CreatePropertyBasicDto } from "./create-property-basic.dto"; +import type { CreatePropertyCharacteristicsDto } from "./create-property-characteristics.dto"; +import type { CreatePropertyGeographyDto } from "./create-property-geography.dto"; +import type { CreatePropertyInternalDto } from "./create-property-internal.dto"; +import type { CreatePropertyServicesDto } from "./create-property-services.dto"; +import type { CreatePropertySurfaceDto } from "./create-property-surface.dto"; +import type { CreatePropertyValuesDto } from "./create-property-values.dto"; export interface ImageOrderItem { - id: number; - is_primary?: boolean; + id: number; + is_primary?: boolean; } - export class UpdatePropertyGroupedDto { - constructor( - public readonly basic?: Partial, - public readonly geography?: CreatePropertyGeographyDto, - public readonly address?: CreatePropertyAddressDto, - public readonly values?: CreatePropertyValuesDto, - public readonly characteristics?: CreatePropertyCharacteristicsDto, - public readonly surface?: CreatePropertySurfaceDto, - public readonly services?: CreatePropertyServicesDto, - public readonly internal?: CreatePropertyInternalDto, - public readonly imageOrder?: ImageOrderItem[], - ) {} - - static create(object: Record): [string?, UpdatePropertyGroupedDto?] { - try { - const parseJsonField = (field: any, fieldName: string): [string?, any?] => { - if (!field) { - return [undefined, undefined]; - } - - if (typeof field === 'string') { - try { - return [undefined, JSON.parse(field)]; - } catch (error) { - try { - const fixed = field.replace(/'/g, '"'); - return [undefined, JSON.parse(fixed)]; - } catch (e) { - const errorMessage = error instanceof Error ? error.message : 'Invalid JSON format'; - return [`Invalid JSON format in ${fieldName}: ${errorMessage}`, undefined]; - } - } - } - - return [undefined, field]; - }; - - let basic: any = undefined; - let geography: any = undefined; - let address: any = undefined; - let characteristics: CreatePropertyCharacteristicsDto | undefined = undefined; - let surface: CreatePropertySurfaceDto | undefined = undefined; - let services: CreatePropertyServicesDto | undefined = undefined; - let values: CreatePropertyValuesDto | undefined = undefined; - let internal: CreatePropertyInternalDto | undefined = undefined; - - const basicField = object.basic || object.Basic; - if (basicField) { - const [basicError, basicParsed] = parseJsonField(basicField, 'basic'); - if (basicError) { - return [basicError, undefined]; - } - basic = basicParsed; - } - - const geographyField = object.geography || object.Geography; - if (geographyField) { - const [geographyError, geographyParsed] = parseJsonField(geographyField, 'geography'); - if (geographyError) { - return [geographyError, undefined]; - } - geography = geographyParsed; - } - - const addressField = object.address || object.Address; - if (addressField) { - const [addressError, addressParsed] = parseJsonField(addressField, 'address'); - if (addressError) { - return [addressError, undefined]; - } - address = addressParsed; - } - - const valuesField = object.values || object.Values; - if (valuesField) { - const [valuesError, valuesParsed] = parseJsonField(valuesField, 'values'); - if (valuesError) { - return [valuesError, undefined]; - } - values = valuesParsed; - } - - const characteristicsField = object.characteristics || object.Characteristics; - if (characteristicsField) { - const [charError, charParsed] = parseJsonField(characteristicsField, 'characteristics'); - if (charError) { - return [charError, undefined]; - } - characteristics = charParsed; - } - - const surfaceField = object.surface || object.Surface; - if (surfaceField) { - const [surfaceError, surfaceParsed] = parseJsonField(surfaceField, 'surface'); - if (surfaceError) { - return [surfaceError, undefined]; - } - surface = surfaceParsed; - } - - const servicesField = object.services || object.Services; - if (servicesField) { - const [servicesError, servicesParsed] = parseJsonField(servicesField, 'services'); - if (servicesError) { - return [servicesError, undefined]; - } - services = servicesParsed; - } - - const internalField = object.internal || object.Internal; - if (internalField) { - const [internalError, internalParsed] = parseJsonField(internalField, 'internal'); - if (internalError) { - return [internalError, undefined]; - } - internal = internalParsed; - } - - let imageOrder: ImageOrderItem[] | undefined = undefined; - const imageOrderField = object.imageOrder || object.image_order || object.ImageOrder; - if (imageOrderField) { - const [imageOrderError, imageOrderParsed] = parseJsonField(imageOrderField, 'imageOrder'); - if (imageOrderError) { - return [imageOrderError, undefined]; - } - - if (imageOrderParsed) { - if (!Array.isArray(imageOrderParsed)) { - return ['imageOrder must be an array', undefined]; - } - - for (const item of imageOrderParsed) { - if (!item || typeof item.id !== 'number') { - return ['Each item in imageOrder must have a numeric id', undefined]; - } - } - - imageOrder = imageOrderParsed; - } - } - - if (!basic && !geography && !address && !values && !characteristics && !surface && !services && !internal && !imageOrder) { - return ['At least one field must be provided for update', undefined]; - } - - return [ - undefined, - new UpdatePropertyGroupedDto( - basic, - geography, - address, - values, - characteristics, - surface, - services, - internal, - imageOrder - ) - ]; - } catch (error) { - console.error('Error parsing UpdatePropertyGroupedDto:', error); - console.error('Request body keys:', Object.keys(object)); - console.error('Request body sample:', JSON.stringify(object).substring(0, 500)); - - const message = error instanceof Error ? error.message : 'Error parsing property update data. Please check that all JSON fields are properly formatted.'; - return [ - message, - undefined - ]; - } - } + constructor( + public readonly basic?: Partial, + public readonly geography?: CreatePropertyGeographyDto, + public readonly address?: CreatePropertyAddressDto, + public readonly values?: CreatePropertyValuesDto, + public readonly characteristics?: CreatePropertyCharacteristicsDto, + public readonly surface?: CreatePropertySurfaceDto, + public readonly services?: CreatePropertyServicesDto, + public readonly internal?: CreatePropertyInternalDto, + public readonly imageOrder?: ImageOrderItem[], + ) {} + + static create( + object: Record, + ): [string?, UpdatePropertyGroupedDto?] { + try { + const parseJsonField = ( + field: unknown, + fieldName: string, + ): [string?, unknown?] => { + if (!field) { + return [undefined, undefined]; + } + + if (typeof field === "string") { + try { + return [undefined, JSON.parse(field)]; + } catch (error) { + try { + const fixed = field.replace(/'/g, '"'); + return [undefined, JSON.parse(fixed)]; + } catch { + const errorMessage = + error instanceof Error ? error.message : "Invalid JSON format"; + return [ + `Invalid JSON format in ${fieldName}: ${errorMessage}`, + undefined, + ]; + } + } + } + + return [undefined, field]; + }; + + let basic: Record | undefined; + let geography: Record | undefined; + let address: Record | undefined; + let characteristics: CreatePropertyCharacteristicsDto | undefined; + let surface: CreatePropertySurfaceDto | undefined; + let services: CreatePropertyServicesDto | undefined; + let values: CreatePropertyValuesDto | undefined; + let internal: CreatePropertyInternalDto | undefined; + + const basicField = object.basic || object.Basic; + if (basicField) { + const [basicError, basicParsed] = parseJsonField(basicField, "basic"); + if (basicError) { + return [basicError, undefined]; + } + basic = basicParsed as Record; + } + + const geographyField = object.geography || object.Geography; + if (geographyField) { + const [geographyError, geographyParsed] = parseJsonField( + geographyField, + "geography", + ); + if (geographyError) { + return [geographyError, undefined]; + } + geography = geographyParsed as Record; + } + + const addressField = object.address || object.Address; + if (addressField) { + const [addressError, addressParsed] = parseJsonField( + addressField, + "address", + ); + if (addressError) { + return [addressError, undefined]; + } + address = addressParsed as Record; + } + + const valuesField = object.values || object.Values; + if (valuesField) { + const [valuesError, valuesParsed] = parseJsonField( + valuesField, + "values", + ); + if (valuesError) { + return [valuesError, undefined]; + } + values = valuesParsed as CreatePropertyValuesDto; + } + + const characteristicsField = + object.characteristics || object.Characteristics; + if (characteristicsField) { + const [charError, charParsed] = parseJsonField( + characteristicsField, + "characteristics", + ); + if (charError) { + return [charError, undefined]; + } + characteristics = charParsed as CreatePropertyCharacteristicsDto; + } + + const surfaceField = object.surface || object.Surface; + if (surfaceField) { + const [surfaceError, surfaceParsed] = parseJsonField( + surfaceField, + "surface", + ); + if (surfaceError) { + return [surfaceError, undefined]; + } + surface = surfaceParsed as CreatePropertySurfaceDto; + } + + const servicesField = object.services || object.Services; + if (servicesField) { + const [servicesError, servicesParsed] = parseJsonField( + servicesField, + "services", + ); + if (servicesError) { + return [servicesError, undefined]; + } + services = servicesParsed as CreatePropertyServicesDto; + } + + const internalField = object.internal || object.Internal; + if (internalField) { + const [internalError, internalParsed] = parseJsonField( + internalField, + "internal", + ); + if (internalError) { + return [internalError, undefined]; + } + internal = internalParsed as CreatePropertyInternalDto; + } + + let imageOrder: ImageOrderItem[] | undefined; + const imageOrderField = + object.imageOrder || object.image_order || object.ImageOrder; + if (imageOrderField) { + const [imageOrderError, imageOrderParsed] = parseJsonField( + imageOrderField, + "imageOrder", + ); + if (imageOrderError) { + return [imageOrderError, undefined]; + } + + if (imageOrderParsed) { + if (!Array.isArray(imageOrderParsed)) { + return ["imageOrder must be an array", undefined]; + } + + for (const item of imageOrderParsed) { + if (!item || typeof item.id !== "number") { + return [ + "Each item in imageOrder must have a numeric id", + undefined, + ]; + } + } + + imageOrder = imageOrderParsed; + } + } + + if ( + !basic && + !geography && + !address && + !values && + !characteristics && + !surface && + !services && + !internal && + !imageOrder + ) { + return ["At least one field must be provided for update", undefined]; + } + + return [ + undefined, + new UpdatePropertyGroupedDto( + basic as unknown as Partial, + geography as unknown as CreatePropertyGeographyDto, + address as unknown as CreatePropertyAddressDto, + values, + characteristics, + surface, + services, + internal, + imageOrder, + ), + ]; + } catch (error) { + console.error("Error parsing UpdatePropertyGroupedDto:", error); + console.error("Request body keys:", Object.keys(object)); + console.error( + "Request body sample:", + JSON.stringify(object).substring(0, 500), + ); + + const message = + error instanceof Error + ? error.message + : "Error parsing property update data. Please check that all JSON fields are properly formatted."; + return [message, undefined]; + } + } } - diff --git a/backend/src/domain/dtos/users/index.ts b/backend/src/domain/dtos/users/index.ts index 58fd331..d665dec 100644 --- a/backend/src/domain/dtos/users/index.ts +++ b/backend/src/domain/dtos/users/index.ts @@ -1,3 +1 @@ -export * from './update-user.dto'; - - +export * from "./update-user.dto"; diff --git a/backend/src/domain/dtos/users/update-user.dto.ts b/backend/src/domain/dtos/users/update-user.dto.ts index d35ea8a..0d636ff 100644 --- a/backend/src/domain/dtos/users/update-user.dto.ts +++ b/backend/src/domain/dtos/users/update-user.dto.ts @@ -1,95 +1,121 @@ import { regularExps } from "../../../config"; export class UpdateUserDto { - constructor( - public readonly first_name?: string, - public readonly last_name?: string, - public readonly email?: string, - public readonly phone?: string, - public readonly password?: string, - public readonly role?: string, - public readonly role_id?: number, - public readonly active?: boolean, - ) {} + constructor( + public readonly first_name?: string, + public readonly last_name?: string, + public readonly email?: string, + public readonly phone?: string, + public readonly password?: string, + public readonly role?: string, + public readonly role_id?: number, + public readonly active?: boolean, + ) {} - static create(object: Record): [string?, UpdateUserDto?] { - const { - first_name, - last_name, - email, - phone, - password, - role, - role_id, - active - } = object; + static create(object: Record): [string?, UpdateUserDto?] { + const { + first_name, + last_name, + email, + phone, + password, + role, + role_id, + active, + } = object; - if (!first_name && !last_name && !email && !phone && !password && !role && role_id === undefined && active === undefined) { - return ['At least one field must be provided for update', undefined]; - } - if (email && typeof email === 'string' && email.trim().length > 0) { - if (!regularExps.email.test(email.trim())) { - return ['Email format is invalid', undefined]; - } - } + if ( + !first_name && + !last_name && + !email && + !phone && + !password && + !role && + role_id === undefined && + active === undefined + ) { + return ["At least one field must be provided for update", undefined]; + } + if (email && typeof email === "string" && email.trim().length > 0) { + if (!regularExps.email.test(email.trim())) { + return ["Email format is invalid", undefined]; + } + } - if (phone && typeof phone === 'string' && phone.trim().length > 0) { - if (!regularExps.phone.test(phone.trim())) { - return ['Phone format is invalid', undefined]; - } - } + if (phone && typeof phone === "string" && phone.trim().length > 0) { + if (!regularExps.phone.test(phone.trim())) { + return ["Phone format is invalid", undefined]; + } + } - if (password && typeof password === 'string') { - if (password.length < 6) { - return ['Password must be at least 6 characters', undefined]; - } - if (password.length > 100) { - return ['Password must be less than 100 characters', undefined]; - } - } + if (password && typeof password === "string") { + if (password.length < 6) { + return ["Password must be at least 6 characters", undefined]; + } + if (password.length > 100) { + return ["Password must be less than 100 characters", undefined]; + } + } - if (first_name && typeof first_name === 'string' && first_name.trim().length > 0 && first_name.trim().length < 2) { - return ['First name must be at least 2 characters', undefined]; - } - if (last_name && typeof last_name === 'string' && last_name.trim().length > 0 && last_name.trim().length < 2) { - return ['Last name must be at least 2 characters', undefined]; - } + if ( + first_name && + typeof first_name === "string" && + first_name.trim().length > 0 && + first_name.trim().length < 2 + ) { + return ["First name must be at least 2 characters", undefined]; + } + if ( + last_name && + typeof last_name === "string" && + last_name.trim().length > 0 && + last_name.trim().length < 2 + ) { + return ["Last name must be at least 2 characters", undefined]; + } - let normalizedRole: string | undefined = undefined; - if (role !== undefined && role !== null) { - const roleStr = String(role).trim().toLowerCase(); - if (roleStr !== 'admin' && roleStr !== 'agent') { - return ['Role must be "admin" or "agent"', undefined]; - } - normalizedRole = roleStr; - } - - let normalizedRoleId: number | undefined = undefined; - if (normalizedRole === undefined && role_id !== undefined) { - if (isNaN(Number(role_id))) { - return ['Role ID must be a number', undefined]; - } - normalizedRoleId = Number(role_id); - } + let normalizedRole: string | undefined; + if (role !== undefined && role !== null) { + const roleStr = String(role).trim().toLowerCase(); + if (roleStr !== "admin" && roleStr !== "agent") { + return ['Role must be "admin" or "agent"', undefined]; + } + normalizedRole = roleStr; + } - if (active !== undefined && typeof active !== 'boolean' && active !== 'true' && active !== 'false') { - return ['Active must be a boolean', undefined]; - } - - return [ - undefined, - new UpdateUserDto( - (first_name as string | undefined)?.trim(), - (last_name as string | undefined)?.trim(), - (email as string | undefined)?.trim().toLowerCase(), - (phone as string | undefined)?.trim(), - password as string | undefined, - normalizedRole, - normalizedRoleId, - active !== undefined ? (typeof active === 'boolean' ? active : active === 'true') : undefined - ) - ]; - } -} + let normalizedRoleId: number | undefined; + if (normalizedRole === undefined && role_id !== undefined) { + if (Number.isNaN(Number(role_id))) { + return ["Role ID must be a number", undefined]; + } + normalizedRoleId = Number(role_id); + } + if ( + active !== undefined && + typeof active !== "boolean" && + active !== "true" && + active !== "false" + ) { + return ["Active must be a boolean", undefined]; + } + return [ + undefined, + new UpdateUserDto( + (first_name as string | undefined)?.trim(), + (last_name as string | undefined)?.trim(), + (email as string | undefined)?.trim().toLowerCase(), + (phone as string | undefined)?.trim(), + password as string | undefined, + normalizedRole, + normalizedRoleId, + active !== undefined + ? typeof active === "boolean" + ? active + : active === "true" + : undefined, + ), + ]; + } +} diff --git a/backend/src/domain/entities/client.entity.ts b/backend/src/domain/entities/client.entity.ts index 7899871..8a05b8a 100644 --- a/backend/src/domain/entities/client.entity.ts +++ b/backend/src/domain/entities/client.entity.ts @@ -1,290 +1,292 @@ -import { CustomError } from '../errors/custom.error'; +import { CustomError } from "../errors/custom.error"; export class ClientEntity { - constructor( - public id: number, - public first_name: string, - public last_name: string, - public phone: string, - public contact_category_id: number, - public email?: string, - public dni?: string, - public property_interest_phone?: string, - public address?: string, - public notes?: string, - public interest_zone?: string, - public purchase_interest?: boolean, - public rental_interest?: boolean, - public property_search_type_id?: number, - public city_id?: number, - public registered_at?: Date, - public deleted?: boolean, - ) {} - - static fromDatabaseObject(object: { [key: string]: any }): ClientEntity { - const { - id, - first_name, - last_name, - phone, - contact_category_id, - email, - dni, - property_interest_phone, - address, - notes, - interest_zone, - purchase_interest, - rental_interest, - property_search_type_id, - city_id, - registered_at, - deleted, - } = object; - - if (!id) { - throw CustomError.badRequest('Client ID is required'); - } - - if (!first_name) { - throw CustomError.badRequest('First name is required'); - } - - if (!last_name) { - throw CustomError.badRequest('Last name is required'); - } - - if (!phone) { - throw CustomError.badRequest('Phone is required'); - } - - if (!contact_category_id) { - throw CustomError.badRequest('Contact category ID is required'); - } - - let registeredDate: Date | undefined = undefined; - if (registered_at) { - registeredDate = registered_at instanceof Date - ? registered_at - : new Date(registered_at); - if (isNaN(registeredDate.getTime())) { - registeredDate = undefined; - } - } - - return new ClientEntity( - Number(id), - String(first_name).trim(), - String(last_name).trim(), - String(phone).trim(), - Number(contact_category_id), - email ? String(email).trim() : undefined, - dni ? String(dni).trim() : undefined, - property_interest_phone ? String(property_interest_phone).trim() : undefined, - address ? String(address).trim() : undefined, - notes ? String(notes).trim() : undefined, - interest_zone ? String(interest_zone).trim() : undefined, - purchase_interest !== undefined ? Boolean(purchase_interest) : undefined, - rental_interest !== undefined ? Boolean(rental_interest) : undefined, - property_search_type_id !== undefined && property_search_type_id !== null - ? Number(property_search_type_id) - : undefined, - city_id !== undefined && city_id !== null - ? Number(city_id) - : undefined, - registeredDate, - deleted !== undefined ? Boolean(deleted) : false, - ); - } - - static fromObject(object: { [key: string]: any }): ClientEntity { - const { - id, - first_name, - last_name, - phone, - contact_category_id, - email, - dni, - property_interest_phone, - address, - notes, - interest_zone, - purchase_interest, - rental_interest, - property_search_type_id, - city_id, - registered_at, - deleted, - } = object; - - if (!id) { - throw CustomError.badRequest('Client ID is required'); - } - - if (!first_name || first_name.trim().length === 0) { - throw CustomError.badRequest('First name is required'); - } - - if (first_name.trim().length < 2) { - throw CustomError.badRequest('First name must be at least 2 characters'); - } - - if (first_name.trim().length > 100) { - throw CustomError.badRequest('First name must be less than 100 characters'); - } - - if (!last_name || last_name.trim().length === 0) { - throw CustomError.badRequest('Last name is required'); - } - - if (last_name.trim().length < 2) { - throw CustomError.badRequest('Last name must be at least 2 characters'); - } - - if (last_name.trim().length > 100) { - throw CustomError.badRequest('Last name must be less than 100 characters'); - } - - if (!phone || phone.trim().length === 0) { - throw CustomError.badRequest('Phone is required'); - } - - if (!contact_category_id) { - throw CustomError.badRequest('Contact category ID is required'); - } - - if (email && email.trim().length > 0) { - if (!ClientEntity.isValidEmail(email)) { - throw CustomError.badRequest('Invalid email format'); - } - } - - if (!ClientEntity.isValidPhone(phone)) { - throw CustomError.badRequest('Invalid phone format'); - } - if (property_interest_phone && property_interest_phone.trim().length > 0) { - if (!ClientEntity.isValidPhone(property_interest_phone)) { - throw CustomError.badRequest('Invalid property interest phone format'); - } - } - - if (dni && dni.trim().length > 0) { - if (!ClientEntity.isValidDni(dni)) { - throw CustomError.badRequest('Invalid DNI format'); - } - } - - let registeredDate: Date | undefined = undefined; - if (registered_at) { - registeredDate = registered_at instanceof Date - ? registered_at - : new Date(registered_at); - if (isNaN(registeredDate.getTime())) { - throw CustomError.badRequest('Invalid registered_at date'); - } - } - - return new ClientEntity( - Number(id), - first_name.trim(), - last_name.trim(), - phone.trim(), - Number(contact_category_id), - email?.trim() || undefined, - dni?.trim() || undefined, - property_interest_phone?.trim() || undefined, - address?.trim() || undefined, - notes?.trim() || undefined, - interest_zone?.trim() || undefined, - purchase_interest !== undefined ? Boolean(purchase_interest) : undefined, - rental_interest !== undefined ? Boolean(rental_interest) : undefined, - property_search_type_id !== undefined && property_search_type_id !== null - ? Number(property_search_type_id) - : undefined, - city_id !== undefined && city_id !== null - ? Number(city_id) - : undefined, - registeredDate, - deleted !== undefined ? Boolean(deleted) : false, - ); - } - - - get fullName(): string { - return `${this.first_name} ${this.last_name}`.trim(); - } - - get primaryPhone(): string { - return this.property_interest_phone || this.phone; - } - - hasInterest(): boolean { - return this.purchase_interest === true || this.rental_interest === true; - } - - canBeDeleted(): boolean { - return !this.deleted; - } - - - canBeUpdated(): boolean { - return !this.deleted; - } - - - toPublicObject() { - return { - id: this.id, - first_name: this.first_name, - last_name: this.last_name, - email: this.email || null, - dni: this.dni || null, - phone: this.phone, - property_interest_phone: this.property_interest_phone || null, - address: this.address || null, - notes: this.notes || null, - contact_category_id: this.contact_category_id, - interest_zone: this.interest_zone || null, - purchase_interest: this.purchase_interest ?? false, - rental_interest: this.rental_interest ?? false, - property_search_type_id: this.property_search_type_id || null, - city_id: this.city_id || null, - registered_at: this.registered_at || null, - }; - } - - private static isValidEmail(email: string): boolean { - const trimmedEmail = email.trim(); - const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - return emailRegex.test(trimmedEmail) && trimmedEmail.length <= 255; - } - - private static isValidPhone(phone: string): boolean { - const trimmedPhone = phone.trim(); - - const validCharsRegex = /^[\d\s\-\(\)\+\.]+$/; - if (!validCharsRegex.test(trimmedPhone)) { - return false; - } - - const digitsOnly = trimmedPhone.replace(/\D/g, ''); - - if (digitsOnly.length < 10) { - return false; - } - - if (digitsOnly.length > 15) { - return false; - } - - return true; - } - - private static isValidDni(dni: string): boolean { - const dniRegex = /^[\d\s\-]{7,}$/; - const digitsOnly = dni.replace(/\D/g, ''); - return dniRegex.test(dni) && digitsOnly.length >= 7 && digitsOnly.length <= 15; - } + constructor( + public id: number, + public first_name: string, + public last_name: string, + public phone: string, + public contact_category_id: number, + public email?: string, + public dni?: string, + public property_interest_phone?: string, + public address?: string, + public notes?: string, + public interest_zone?: string, + public purchase_interest?: boolean, + public rental_interest?: boolean, + public property_search_type_id?: number, + public city_id?: number, + public registered_at?: Date, + public deleted?: boolean, + ) {} + + static fromDatabaseObject(object: Record): ClientEntity { + const { + id, + first_name, + last_name, + phone, + contact_category_id, + email, + dni, + property_interest_phone, + address, + notes, + interest_zone, + purchase_interest, + rental_interest, + property_search_type_id, + city_id, + registered_at, + deleted, + } = object; + + if (!id) { + throw CustomError.badRequest("Client ID is required"); + } + + if (!first_name) { + throw CustomError.badRequest("First name is required"); + } + + if (!last_name) { + throw CustomError.badRequest("Last name is required"); + } + + if (!phone) { + throw CustomError.badRequest("Phone is required"); + } + + if (!contact_category_id) { + throw CustomError.badRequest("Contact category ID is required"); + } + + let registeredDate: Date | undefined; + if (registered_at) { + registeredDate = + registered_at instanceof Date ? registered_at : new Date(registered_at); + if (Number.isNaN(registeredDate.getTime())) { + registeredDate = undefined; + } + } + + return new ClientEntity( + Number(id), + String(first_name).trim(), + String(last_name).trim(), + String(phone).trim(), + Number(contact_category_id), + email ? String(email).trim() : undefined, + dni ? String(dni).trim() : undefined, + property_interest_phone + ? String(property_interest_phone).trim() + : undefined, + address ? String(address).trim() : undefined, + notes ? String(notes).trim() : undefined, + interest_zone ? String(interest_zone).trim() : undefined, + purchase_interest !== undefined ? Boolean(purchase_interest) : undefined, + rental_interest !== undefined ? Boolean(rental_interest) : undefined, + property_search_type_id !== undefined && property_search_type_id !== null + ? Number(property_search_type_id) + : undefined, + city_id !== undefined && city_id !== null ? Number(city_id) : undefined, + registeredDate, + deleted !== undefined ? Boolean(deleted) : false, + ); + } + + static fromObject(object: Record): ClientEntity { + const { + id, + first_name, + last_name, + phone, + contact_category_id, + email, + dni, + property_interest_phone, + address, + notes, + interest_zone, + purchase_interest, + rental_interest, + property_search_type_id, + city_id, + registered_at, + deleted, + } = object; + + if (!id) { + throw CustomError.badRequest("Client ID is required"); + } + + if (!first_name || (first_name as string).trim().length === 0) { + throw CustomError.badRequest("First name is required"); + } + + if ((first_name as string).trim().length < 2) { + throw CustomError.badRequest("First name must be at least 2 characters"); + } + + if ((first_name as string).trim().length > 100) { + throw CustomError.badRequest( + "First name must be less than 100 characters", + ); + } + + if (!last_name || (last_name as string).trim().length === 0) { + throw CustomError.badRequest("Last name is required"); + } + + if ((last_name as string).trim().length < 2) { + throw CustomError.badRequest("Last name must be at least 2 characters"); + } + + if ((last_name as string).trim().length > 100) { + throw CustomError.badRequest( + "Last name must be less than 100 characters", + ); + } + + if (!phone || (phone as string).trim().length === 0) { + throw CustomError.badRequest("Phone is required"); + } + + if (!contact_category_id) { + throw CustomError.badRequest("Contact category ID is required"); + } + + if (email && (email as string).trim().length > 0) { + if (!ClientEntity.isValidEmail(email as string)) { + throw CustomError.badRequest("Invalid email format"); + } + } + + if (!ClientEntity.isValidPhone(phone as string)) { + throw CustomError.badRequest("Invalid phone format"); + } + if ( + property_interest_phone && + (property_interest_phone as string).trim().length > 0 + ) { + if (!ClientEntity.isValidPhone(property_interest_phone as string)) { + throw CustomError.badRequest("Invalid property interest phone format"); + } + } + + if (dni && (dni as string).trim().length > 0) { + if (!ClientEntity.isValidDni(dni as string)) { + throw CustomError.badRequest("Invalid DNI format"); + } + } + + let registeredDate: Date | undefined; + if (registered_at) { + registeredDate = + registered_at instanceof Date ? registered_at : new Date(registered_at); + if (Number.isNaN(registeredDate.getTime())) { + throw CustomError.badRequest("Invalid registered_at date"); + } + } + + return new ClientEntity( + Number(id), + (first_name as string).trim(), + (last_name as string).trim(), + (phone as string).trim(), + Number(contact_category_id), + email ? (email as string).trim() : undefined, + dni ? (dni as string).trim() : undefined, + property_interest_phone + ? (property_interest_phone as string).trim() + : undefined, + address ? (address as string).trim() : undefined, + notes ? (notes as string).trim() : undefined, + interest_zone ? (interest_zone as string).trim() : undefined, + purchase_interest !== undefined ? Boolean(purchase_interest) : undefined, + rental_interest !== undefined ? Boolean(rental_interest) : undefined, + property_search_type_id !== undefined && property_search_type_id !== null + ? Number(property_search_type_id) + : undefined, + city_id !== undefined && city_id !== null ? Number(city_id) : undefined, + registeredDate, + deleted !== undefined ? Boolean(deleted) : false, + ); + } + + get fullName(): string { + return `${this.first_name} ${this.last_name}`.trim(); + } + + get primaryPhone(): string { + return this.property_interest_phone || this.phone; + } + + hasInterest(): boolean { + return this.purchase_interest === true || this.rental_interest === true; + } + + canBeDeleted(): boolean { + return !this.deleted; + } + + canBeUpdated(): boolean { + return !this.deleted; + } + + toPublicObject() { + return { + id: this.id, + first_name: this.first_name, + last_name: this.last_name, + email: this.email || null, + dni: this.dni || null, + phone: this.phone, + property_interest_phone: this.property_interest_phone || null, + address: this.address || null, + notes: this.notes || null, + contact_category_id: this.contact_category_id, + interest_zone: this.interest_zone || null, + purchase_interest: this.purchase_interest ?? false, + rental_interest: this.rental_interest ?? false, + property_search_type_id: this.property_search_type_id || null, + city_id: this.city_id || null, + registered_at: this.registered_at || null, + }; + } + + private static isValidEmail(email: string): boolean { + const trimmedEmail = email.trim(); + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return emailRegex.test(trimmedEmail) && trimmedEmail.length <= 255; + } + + private static isValidPhone(phone: string): boolean { + const trimmedPhone = phone.trim(); + + const validCharsRegex = /^[\d\s\-()+.]+$/; + if (!validCharsRegex.test(trimmedPhone)) { + return false; + } + + const digitsOnly = trimmedPhone.replace(/\D/g, ""); + + if (digitsOnly.length < 10) { + return false; + } + + if (digitsOnly.length > 15) { + return false; + } + + return true; + } + + private static isValidDni(dni: string): boolean { + const dniRegex = /^[\d\s-]{7,}$/; + const digitsOnly = dni.replace(/\D/g, ""); + return ( + dniRegex.test(dni) && digitsOnly.length >= 7 && digitsOnly.length <= 15 + ); + } } - - diff --git a/backend/src/domain/entities/profile.entity.ts b/backend/src/domain/entities/profile.entity.ts index adfa54e..56e1018 100644 --- a/backend/src/domain/entities/profile.entity.ts +++ b/backend/src/domain/entities/profile.entity.ts @@ -1,124 +1,135 @@ import { CustomError } from "../errors/custom.error"; export class ProfileEntity { + constructor( + public id: number, + public first_name: string, + public last_name: string, + public email: string, + public password_hash: string, + public role_id: number, + public phone?: string, + public active?: boolean, + public deleted?: boolean, + public created_at?: Date, + ) {} - constructor( - public id: number, - public first_name: string, - public last_name: string, - public email: string, - public password_hash: string, - public role_id: number, - public phone?: string, - public active?: boolean, - public deleted?: boolean, - public created_at?: Date, - ) {} + static fromObject(object: Record): ProfileEntity { + const { + id, + first_name, + nombre, + last_name, + apellido, + email, + password_hash, + password, + phone, + telefono, + role_id, + id_rol, + active, + activo, + deleted, + borrado_logico, + created_at, + fecha_creacion, + } = object; - static fromObject(object: { [key: string]: any }): ProfileEntity { - const { - id, - first_name, - nombre, - last_name, - apellido, - email, - password_hash, - password, - phone, - telefono, - role_id, - id_rol, - active, - activo, - deleted, - borrado_logico, - created_at, - fecha_creacion, - } = object; - - const profileId = id; - const profileFirstName = first_name || nombre; - const profileLastName = last_name || apellido; - const profilePassword = password_hash || password; - const profilePhone = phone || telefono; - const profileRole = role_id || id_rol; - const profileActive = active !== undefined ? active : (activo !== undefined ? activo : true); - const profileDeleted = deleted !== undefined ? deleted : (borrado_logico !== undefined ? borrado_logico : false); - const profileCreatedAt = created_at || fecha_creacion; - - if (!profileId) throw CustomError.badRequest('Id is required'); - if (!profileFirstName || profileFirstName.trim().length === 0) { - throw CustomError.badRequest('First name is required'); - } - if (!profileLastName || profileLastName.trim().length === 0) { - throw CustomError.badRequest('Last name is required'); - } - if (!email || email.trim().length === 0) { - throw CustomError.badRequest('Email is required'); - } - if (!this.isValidEmail(email)) { - throw CustomError.badRequest('Email format is invalid'); - } - if (!profilePassword || profilePassword.trim().length === 0) { - throw CustomError.badRequest('Password is required'); - } - if (!profileRole) { - throw CustomError.badRequest('Role is required'); - } - - let createdAt: Date | undefined; - - if (profileCreatedAt) { - createdAt = profileCreatedAt instanceof Date ? profileCreatedAt : new Date(profileCreatedAt); - if (isNaN(createdAt.getTime())) { - throw CustomError.badRequest('Invalid created_at date'); - } - } + const profileId = id; + const profileFirstName = first_name || nombre; + const profileLastName = last_name || apellido; + const profilePassword = password_hash || password; + const profilePhone = phone || telefono; + const profileRole = role_id || id_rol; + const profileActive = + active !== undefined ? active : activo !== undefined ? activo : true; + const profileDeleted = + deleted !== undefined + ? deleted + : borrado_logico !== undefined + ? borrado_logico + : false; + const profileCreatedAt = created_at || fecha_creacion; - const finalId = typeof profileId === 'string' ? Number(profileId) : profileId; - const finalRoleId = typeof profileRole === 'string' ? Number(profileRole) : profileRole; - - if (isNaN(finalId) || isNaN(finalRoleId)) { - throw CustomError.badRequest('Invalid id or role_id format'); - } - - return new ProfileEntity( - finalId, - profileFirstName.trim(), - profileLastName.trim(), - email.trim().toLowerCase(), - profilePassword, - finalRoleId, - profilePhone?.trim() || undefined, - profileActive, - profileDeleted, - createdAt - ); - } + if (!profileId) throw CustomError.badRequest("Id is required"); + if (!profileFirstName || (profileFirstName as string).trim().length === 0) { + throw CustomError.badRequest("First name is required"); + } + if (!profileLastName || (profileLastName as string).trim().length === 0) { + throw CustomError.badRequest("Last name is required"); + } + if (!email || (email as string).trim().length === 0) { + throw CustomError.badRequest("Email is required"); + } + if (!ProfileEntity.isValidEmail(email as string)) { + throw CustomError.badRequest("Email format is invalid"); + } + if (!profilePassword || (profilePassword as string).trim().length === 0) { + throw CustomError.badRequest("Password is required"); + } + if (!profileRole) { + throw CustomError.badRequest("Role is required"); + } - get fullName(): string { - return `${this.first_name} ${this.last_name}`.trim(); - } + let createdAt: Date | undefined; - toPublicObject() { - const { password_hash: _, ...publicProfile } = this; - return { - id: this.id, - first_name: this.first_name, - last_name: this.last_name, - email: this.email, - phone: this.phone, - role_id: this.role_id, - active: this.active, - created_at: this.created_at, - }; - } + if (profileCreatedAt) { + createdAt = + profileCreatedAt instanceof Date + ? profileCreatedAt + : new Date(profileCreatedAt as string | number); + if (Number.isNaN(createdAt.getTime())) { + throw CustomError.badRequest("Invalid created_at date"); + } + } - private static isValidEmail(email: string): boolean { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - } -} + // Ensure number using casts before checking + const finalId = + typeof profileId === "string" ? Number(profileId) : (profileId as number); + const finalRoleId = + typeof profileRole === "string" + ? Number(profileRole) + : (profileRole as number); + + if (Number.isNaN(finalId) || Number.isNaN(finalRoleId)) { + throw CustomError.badRequest("Invalid id or role_id format"); + } + + return new ProfileEntity( + finalId, + (profileFirstName as string).trim(), + (profileLastName as string).trim(), + (email as string).trim().toLowerCase(), + profilePassword as string, + finalRoleId, + profilePhone ? (profilePhone as string).trim() : undefined, + profileActive, + profileDeleted, + createdAt, + ); + } + get fullName(): string { + return `${this.first_name} ${this.last_name}`.trim(); + } + toPublicObject() { + // const { password_hash: _, ...publicProfile } = this; + return { + id: this.id, + first_name: this.first_name, + last_name: this.last_name, + email: this.email, + phone: this.phone, + role_id: this.role_id, + active: this.active, + created_at: this.created_at, + }; + } + + private static isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } +} diff --git a/backend/src/domain/entities/property.entity.ts b/backend/src/domain/entities/property.entity.ts index fdcc61c..b11eb1b 100644 --- a/backend/src/domain/entities/property.entity.ts +++ b/backend/src/domain/entities/property.entity.ts @@ -1,92 +1,114 @@ -import { CustomError } from '../errors/custom.error'; +import { CustomError } from "../errors/custom.error"; export class PropertyEntity { - constructor( - public id: number, - public title: string, - public property_type_id: number, - public property_status_id: number, - public visibility_status_id: number, - public captured_by_user_id: number, - public owner_id?: number, - public description?: string, - public bedrooms_count?: number, - public bathrooms_count?: number, - public rooms_count?: number, - public parking_spaces_count?: number, - public land_area?: number, - public covered_area?: number, - public total_area?: number, - public publication_date?: Date, - public updated_at?: Date, - ) {} + constructor( + public id: number, + public title: string, + public property_type_id: number, + public property_status_id: number, + public visibility_status_id: number, + public captured_by_user_id: number, + public owner_id?: number, + public description?: string, + public bedrooms_count?: number, + public bathrooms_count?: number, + public rooms_count?: number, + public parking_spaces_count?: number, + public land_area?: number, + public covered_area?: number, + public total_area?: number, + public publication_date?: Date, + public updated_at?: Date, + ) {} - static fromObject(object: { [key: string]: any }): PropertyEntity { - const { - id, - title, - description, - property_type_id, - property_status_id, - visibility_status_id, - owner_id, - captured_by_user_id, - bedrooms_count, - bathrooms_count, - rooms_count, - parking_spaces_count, - land_area, - covered_area, - total_area, - publication_date, - updated_at, - } = object; + static fromObject(object: Record): PropertyEntity { + const { + id, + title, + description, + property_type_id, + property_status_id, + visibility_status_id, + owner_id, + captured_by_user_id, + bedrooms_count, + bathrooms_count, + rooms_count, + parking_spaces_count, + land_area, + covered_area, + total_area, + publication_date, + updated_at, + } = object; - if (!id) throw CustomError.badRequest('Property ID is required'); - if (!title || title.trim().length === 0) { - throw CustomError.badRequest('Property title is required'); - } - if (!property_type_id) { - throw CustomError.badRequest('Property type ID is required'); - } - if (!property_status_id) { - throw CustomError.badRequest('Property status ID is required'); - } - if (!visibility_status_id) { - throw CustomError.badRequest('Visibility status ID is required'); - } - if (!captured_by_user_id) { - throw CustomError.badRequest('Captured by user ID is required'); - } + if (!id) throw CustomError.badRequest("Property ID is required"); + if (!title || (title as string).trim().length === 0) { + throw CustomError.badRequest("Title is required"); + } - return new PropertyEntity( - Number(id), - title.trim(), - Number(property_type_id), - Number(property_status_id), - Number(visibility_status_id), - Number(captured_by_user_id), - owner_id ? Number(owner_id) : undefined, - description?.trim(), - bedrooms_count !== undefined ? Number(bedrooms_count) : undefined, - bathrooms_count !== undefined ? Number(bathrooms_count) : undefined, - rooms_count !== undefined ? Number(rooms_count) : undefined, - parking_spaces_count !== undefined ? Number(parking_spaces_count) : undefined, - land_area !== undefined ? Number(land_area) : undefined, - covered_area !== undefined ? Number(covered_area) : undefined, - total_area !== undefined ? Number(total_area) : undefined, - publication_date ? new Date(publication_date) : undefined, - updated_at ? new Date(updated_at) : undefined, - ); - } + let pubDate: Date | undefined; + if (publication_date) { + pubDate = + publication_date instanceof Date + ? publication_date + : new Date(publication_date as string | number); + if (Number.isNaN(pubDate.getTime())) { + throw CustomError.badRequest("Invalid publication date"); + } + } - canBeUpdated(): boolean { - return true; - } + let updatedDate: Date | undefined; + if (updated_at) { + updatedDate = + updated_at instanceof Date + ? updated_at + : new Date(updated_at as string | number); + if (Number.isNaN(updatedDate.getTime())) { + throw CustomError.badRequest("Invalid updated_at date"); + } + } + if (!property_type_id) { + throw CustomError.badRequest("Property type ID is required"); + } + if (!property_status_id) { + throw CustomError.badRequest("Property status ID is required"); + } + if (!visibility_status_id) { + throw CustomError.badRequest("Visibility status ID is required"); + } + if (!captured_by_user_id) { + throw CustomError.badRequest("Captured by user ID is required"); + } + return new PropertyEntity( + Number(id), + (title as string).trim(), + Number(property_type_id), + Number(property_status_id), + Number(visibility_status_id), + Number(captured_by_user_id), + owner_id ? Number(owner_id) : undefined, + description?.trim(), + bedrooms_count !== undefined ? Number(bedrooms_count) : undefined, + bathrooms_count !== undefined ? Number(bathrooms_count) : undefined, + rooms_count !== undefined ? Number(rooms_count) : undefined, + parking_spaces_count !== undefined + ? Number(parking_spaces_count) + : undefined, + land_area !== undefined ? Number(land_area) : undefined, + covered_area !== undefined ? Number(covered_area) : undefined, + total_area !== undefined ? Number(total_area) : undefined, + publication_date ? new Date(publication_date) : undefined, + updated_at ? new Date(updated_at) : undefined, + ); + } - canBeArchived(): boolean { - return true; - } -} + canBeUpdated(): boolean { + return true; + } + canBeArchived(): boolean { + return true; + } +} diff --git a/backend/src/domain/errors/custom.error.ts b/backend/src/domain/errors/custom.error.ts index 302ea2d..9a782a2 100644 --- a/backend/src/domain/errors/custom.error.ts +++ b/backend/src/domain/errors/custom.error.ts @@ -1,60 +1,56 @@ - - - export class CustomError extends Error { - - private constructor( - public readonly statusCode: number, - public readonly message: string, - ) { - super(message); - } - - static badRequest(message: string): CustomError { - return new CustomError(400, message); - } - - static unauthorized(message: string): CustomError { - return new CustomError(401, message); - } - - static forbidden(message: string): CustomError { - return new CustomError(403, message); - } - - static notFound(message: string): CustomError { - return new CustomError(404, message); - } - - static internalServerError(message: string): CustomError { - return new CustomError(500, message); - } - - static badGateway(message: string): CustomError { - return new CustomError(502, message); - } - - static serviceUnavailable(message: string): CustomError { - return new CustomError(503, message); - } - - static gatewayTimeout(message: string): CustomError { - return new CustomError(504, message); - } - - static conflict(message: string): CustomError { - return new CustomError(409, message); - } - - static unprocessableEntity(message: string): CustomError { - return new CustomError(422, message); - } - - static tooManyRequests(message: string): CustomError { - return new CustomError(429, message); - } - - static preconditionFailed(message: string): CustomError { - return new CustomError(412, message); - } -} \ No newline at end of file + private constructor( + public readonly statusCode: number, + public readonly message: string, + ) { + super(message); + } + + static badRequest(message: string): CustomError { + return new CustomError(400, message); + } + + static unauthorized(message: string): CustomError { + return new CustomError(401, message); + } + + static forbidden(message: string): CustomError { + return new CustomError(403, message); + } + + static notFound(message: string): CustomError { + return new CustomError(404, message); + } + + static internalServerError(message: string): CustomError { + return new CustomError(500, message); + } + + static badGateway(message: string): CustomError { + return new CustomError(502, message); + } + + static serviceUnavailable(message: string): CustomError { + return new CustomError(503, message); + } + + static gatewayTimeout(message: string): CustomError { + return new CustomError(504, message); + } + + static conflict(message: string): CustomError { + return new CustomError(409, message); + } + + static unprocessableEntity(message: string): CustomError { + return new CustomError(422, message); + } + + static tooManyRequests(message: string): CustomError { + return new CustomError(429, message); + } + + static preconditionFailed(message: string): CustomError { + return new CustomError(412, message); + } +} diff --git a/backend/src/domain/index.ts b/backend/src/domain/index.ts index 04fbef5..2ee6af9 100644 --- a/backend/src/domain/index.ts +++ b/backend/src/domain/index.ts @@ -1,16 +1,16 @@ -export * from './errors/custom.error'; -export * from './entities/profile.entity'; -export * from './entities/property.entity'; -export * from './entities/client.entity'; -export * from './dtos/auth/register-profile.dto'; -export * from './dtos/auth/login-profile.dto'; -export * from './dtos/properties'; -export * from './dtos/clients'; -export * from './dtos/users'; -export * from './interfaces/file-upload.adapter'; -export * from './use-cases'; -export * from './interfaces/hash.adapter'; -export * from './interfaces/jwt.adapter'; -export * from './interfaces/jwt-payload'; -export * from './interfaces/database-rows'; -export * from './utils'; \ No newline at end of file +export * from "./dtos/auth/login-profile.dto"; +export * from "./dtos/auth/register-profile.dto"; +export * from "./dtos/clients"; +export * from "./dtos/properties"; +export * from "./dtos/users"; +export * from "./entities/client.entity"; +export * from "./entities/profile.entity"; +export * from "./entities/property.entity"; +export * from "./errors/custom.error"; +export * from "./interfaces/database-rows"; +export * from "./interfaces/file-upload.adapter"; +export * from "./interfaces/hash.adapter"; +export * from "./interfaces/jwt.adapter"; +export * from "./interfaces/jwt-payload"; +export * from "./use-cases"; +export * from "./utils"; diff --git a/backend/src/domain/interfaces/database-rows.ts b/backend/src/domain/interfaces/database-rows.ts index 6c046d2..8c0b306 100644 --- a/backend/src/domain/interfaces/database-rows.ts +++ b/backend/src/domain/interfaces/database-rows.ts @@ -1,4 +1,3 @@ - export interface PropertyPriceRow { id: number; property_id: number; diff --git a/backend/src/domain/interfaces/enriched-data.ts b/backend/src/domain/interfaces/enriched-data.ts index fcebc53..5791ec1 100644 --- a/backend/src/domain/interfaces/enriched-data.ts +++ b/backend/src/domain/interfaces/enriched-data.ts @@ -1,4 +1,3 @@ - export interface CurrencyInfo { id: number | undefined; name: string; @@ -72,7 +71,6 @@ export interface ContactCategoryInfo { name: string; } - export interface EnrichedPropertyPrice { id?: number; property_id: number; @@ -224,12 +222,14 @@ export interface PropertyDetails { bedrooms?: number; bathrooms?: number; garage: boolean; - address: (EnrichedPropertyAddress & { - location?: { - latitude: number | null; - longitude: number | null; - }; - }) | null; + address: + | (EnrichedPropertyAddress & { + location?: { + latitude: number | null; + longitude: number | null; + }; + }) + | null; prices: Array<{ amount: number; currency: CurrencyInfo | null; @@ -289,4 +289,3 @@ export interface RentedPropertyWithDetails extends EnrichedPropertyWithDetails { export interface OwnedPropertyWithDetails extends EnrichedPropertyWithDetails { publication_date?: Date; } - diff --git a/backend/src/domain/interfaces/file-upload.adapter.ts b/backend/src/domain/interfaces/file-upload.adapter.ts index 116b765..4d0f54d 100644 --- a/backend/src/domain/interfaces/file-upload.adapter.ts +++ b/backend/src/domain/interfaces/file-upload.adapter.ts @@ -1,16 +1,13 @@ - export interface FileUploadAdapter { - - uploadFile( - file: Buffer | string, - options?: { - folder?: string; - publicId?: string; - resourceType?: 'image' | 'video' | 'raw' | 'auto'; - mimeType?: string; - } - ): Promise; - - deleteFile(identifier: string): Promise; + uploadFile( + file: Buffer | string, + options?: { + folder?: string; + publicId?: string; + resourceType?: "image" | "video" | "raw" | "auto"; + mimeType?: string; + }, + ): Promise; + + deleteFile(identifier: string): Promise; } - diff --git a/backend/src/domain/interfaces/hash.adapter.ts b/backend/src/domain/interfaces/hash.adapter.ts index 8503d44..8bdcbc5 100644 --- a/backend/src/domain/interfaces/hash.adapter.ts +++ b/backend/src/domain/interfaces/hash.adapter.ts @@ -1,5 +1,4 @@ export interface HashAdapter { - hash(password: string): Promise; - compare(password: string, hash: string): Promise; + hash(password: string): Promise; + compare(password: string, hash: string): Promise; } - diff --git a/backend/src/domain/interfaces/jwt-payload.ts b/backend/src/domain/interfaces/jwt-payload.ts index 6713f2d..dcce923 100644 --- a/backend/src/domain/interfaces/jwt-payload.ts +++ b/backend/src/domain/interfaces/jwt-payload.ts @@ -1,4 +1,3 @@ - export interface JwtPayload { id: string; email: string; diff --git a/backend/src/domain/interfaces/jwt.adapter.ts b/backend/src/domain/interfaces/jwt.adapter.ts index 57156d5..44dfc89 100644 --- a/backend/src/domain/interfaces/jwt.adapter.ts +++ b/backend/src/domain/interfaces/jwt.adapter.ts @@ -1,8 +1,7 @@ -import type { JwtPayload } from './jwt-payload'; +import type { JwtPayload } from "./jwt-payload"; export interface JwtAdapter { generateToken(payload: JwtPayload, duration?: string): Promise; verifyToken(token: string): Promise; validateToken(token: string): Promise; } - diff --git a/backend/src/domain/use-cases/auth/validate-email.use-case.ts b/backend/src/domain/use-cases/auth/validate-email.use-case.ts index c9eacd0..48f92bd 100644 --- a/backend/src/domain/use-cases/auth/validate-email.use-case.ts +++ b/backend/src/domain/use-cases/auth/validate-email.use-case.ts @@ -1,11 +1,10 @@ export class ValidateEmailUseCase { - static execute(email: string): boolean { - if (!email || email.trim().length === 0) { - return false; - } - - const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; - return emailRegex.test(email.trim()); - } -} + static execute(email: string): boolean { + if (!email || email.trim().length === 0) { + return false; + } + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; + return emailRegex.test(email.trim()); + } +} diff --git a/backend/src/domain/use-cases/auth/validate-password.use-case.ts b/backend/src/domain/use-cases/auth/validate-password.use-case.ts index 4486f78..a001897 100644 --- a/backend/src/domain/use-cases/auth/validate-password.use-case.ts +++ b/backend/src/domain/use-cases/auth/validate-password.use-case.ts @@ -1,24 +1,23 @@ export class ValidatePasswordUseCase { - static execute(password: string): { isValid: boolean; errors: string[] } { - const errors: string[] = []; - - if (!password || password.length === 0) { - errors.push('Password is required'); - return { isValid: false, errors }; - } - - if (password.length < 6) { - errors.push('Password must be at least 6 characters'); - } - - if (password.length > 100) { - errors.push('Password must be less than 100 characters'); - } - - return { - isValid: errors.length === 0, - errors - }; - } -} + static execute(password: string): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!password || password.length === 0) { + errors.push("Password is required"); + return { isValid: false, errors }; + } + + if (password.length < 6) { + errors.push("Password must be at least 6 characters"); + } + if (password.length > 100) { + errors.push("Password must be less than 100 characters"); + } + + return { + isValid: errors.length === 0, + errors, + }; + } +} diff --git a/backend/src/domain/use-cases/index.ts b/backend/src/domain/use-cases/index.ts index 222b5e4..8b1f98b 100644 --- a/backend/src/domain/use-cases/index.ts +++ b/backend/src/domain/use-cases/index.ts @@ -1,3 +1,2 @@ -export * from './auth/validate-email.use-case'; -export * from './auth/validate-password.use-case'; - +export * from "./auth/validate-email.use-case"; +export * from "./auth/validate-password.use-case"; diff --git a/backend/src/domain/utils/client-validation.util.ts b/backend/src/domain/utils/client-validation.util.ts index b671534..fab0e44 100644 --- a/backend/src/domain/utils/client-validation.util.ts +++ b/backend/src/domain/utils/client-validation.util.ts @@ -1,111 +1,140 @@ +export function validateFirstName(first_name: unknown): [string?, undefined?] { + if ( + !first_name || + typeof first_name !== "string" || + first_name.trim().length === 0 + ) { + return ["First name is required", undefined]; + } -export function validateFirstName(first_name: unknown): [string?, void?] { - if (!first_name || typeof first_name !== 'string' || first_name.trim().length === 0) { - return ['First name is required', undefined]; - } - - if (first_name.trim().length < 2) { - return ['First name must be at least 2 characters', undefined]; - } - - if (first_name.trim().length > 100) { - return ['First name must be less than 100 characters', undefined]; - } - - return [undefined, undefined]; -} + if (first_name.trim().length < 2) { + return ["First name must be at least 2 characters", undefined]; + } + if (first_name.trim().length > 100) { + return ["First name must be less than 100 characters", undefined]; + } -export function validateLastName(last_name: unknown): [string?, void?] { - if (!last_name || typeof last_name !== 'string' || last_name.trim().length === 0) { - return ['Last name is required', undefined]; - } - - if (last_name.trim().length < 2) { - return ['Last name must be at least 2 characters', undefined]; - } - - if (last_name.trim().length > 100) { - return ['Last name must be less than 100 characters', undefined]; - } - - return [undefined, undefined]; + return [undefined, undefined]; } -export function validatePhone(phone: unknown): [string?, void?] { - if (!phone || typeof phone !== 'string' || phone.trim().length === 0) { - return ['Phone is required', undefined]; - } - - const trimmedPhone = phone.trim(); - const validCharsRegex = /^[\d\s\-\(\)\+\.]+$/; - if (!validCharsRegex.test(trimmedPhone)) { - return ['Invalid phone format: phone can only contain numbers, spaces, dashes, parentheses, plus sign, and dots', undefined]; - } - - const digitsOnly = trimmedPhone.replace(/\D/g, ''); - if (digitsOnly.length < 10) { - return ['Invalid phone format: phone must contain at least 10 digits', undefined]; - } - - if (digitsOnly.length > 15) { - return ['Invalid phone format: phone must contain at most 15 digits', undefined]; - } - - const normalized = trimmedPhone.startsWith('+') - ? '+' + trimmedPhone.substring(1).replace(/[\s\-\(\)\.]/g, '') - : digitsOnly; - - if (normalized.length > 15) { - return ['Invalid phone format: phone number is too long. Please use a shorter format (e.g., +541199990000 instead of +54 11 9999-0000)', undefined]; - } - - return [undefined, undefined]; +export function validateLastName(last_name: unknown): [string?, undefined?] { + if ( + !last_name || + typeof last_name !== "string" || + last_name.trim().length === 0 + ) { + return ["Last name is required", undefined]; + } + + if (last_name.trim().length < 2) { + return ["Last name must be at least 2 characters", undefined]; + } + + if (last_name.trim().length > 100) { + return ["Last name must be less than 100 characters", undefined]; + } + + return [undefined, undefined]; } +export function validatePhone(phone: unknown): [string?, undefined?] { + if (!phone || typeof phone !== "string" || phone.trim().length === 0) { + return ["Phone is required", undefined]; + } + + const trimmedPhone = phone.trim(); + const validCharsRegex = /^[\d\s\-()+.]+$/; + if (!validCharsRegex.test(trimmedPhone)) { + return [ + "Invalid phone format: phone can only contain numbers, spaces, dashes, parentheses, plus sign, and dots", + undefined, + ]; + } + + const digitsOnly = trimmedPhone.replace(/\D/g, ""); + if (digitsOnly.length < 10) { + return [ + "Invalid phone format: phone must contain at least 10 digits", + undefined, + ]; + } + + if (digitsOnly.length > 15) { + return [ + "Invalid phone format: phone must contain at most 15 digits", + undefined, + ]; + } -export function validateEmail(email: unknown, required: boolean = false): [string?, void?] { - if (required && (!email || typeof email !== 'string' || email.trim().length === 0)) { - return ['Email is required', undefined]; - } - - if (email && typeof email === 'string' && email.trim().length > 0) { - const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - if (!emailRegex.test(email.trim())) { - return ['Invalid email format', undefined]; - } - if (email.trim().length > 255) { - return ['Email must be less than 255 characters', undefined]; - } - } - - return [undefined, undefined]; + const normalized = trimmedPhone.startsWith("+") + ? `+${trimmedPhone.substring(1).replace(/[\s\-().]/g, "")}` + : digitsOnly; + + if (normalized.length > 15) { + return [ + "Invalid phone format: phone number is too long. Please use a shorter format (e.g., +541199990000 instead of +54 11 9999-0000)", + undefined, + ]; + } + + return [undefined, undefined]; } -export function validateDni(dni: unknown): [string?, void?] { - if (dni && typeof dni === 'string' && dni.trim().length > 0) { - const trimmedDni = dni.trim(); - const dniRegex = /^[\d\s\-]{7,}$/; - if (!dniRegex.test(trimmedDni)) { - return ['Invalid DNI format: DNI can only contain numbers, spaces, and dashes', undefined]; - } - const dniDigitsOnly = trimmedDni.replace(/\D/g, ''); - if (dniDigitsOnly.length < 7 || dniDigitsOnly.length > 15) { - return ['Invalid DNI format: DNI must contain between 7 and 15 digits', undefined]; - } - } - - return [undefined, undefined]; +export function validateEmail( + email: unknown, + required: boolean = false, +): [string?, undefined?] { + if ( + required && + (!email || typeof email !== "string" || email.trim().length === 0) + ) { + return ["Email is required", undefined]; + } + + if (email && typeof email === "string" && email.trim().length > 0) { + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + if (!emailRegex.test(email.trim())) { + return ["Invalid email format", undefined]; + } + if (email.trim().length > 255) { + return ["Email must be less than 255 characters", undefined]; + } + } + + return [undefined, undefined]; } +export function validateDni(dni: unknown): [string?, undefined?] { + if (dni && typeof dni === "string" && dni.trim().length > 0) { + const trimmedDni = dni.trim(); + const dniRegex = /^[\d\s-]{7,}$/; + if (!dniRegex.test(trimmedDni)) { + return [ + "Invalid DNI format: DNI can only contain numbers, spaces, and dashes", + undefined, + ]; + } + const dniDigitsOnly = trimmedDni.replace(/\D/g, ""); + if (dniDigitsOnly.length < 7 || dniDigitsOnly.length > 15) { + return [ + "Invalid DNI format: DNI must contain between 7 and 15 digits", + undefined, + ]; + } + } -export function validatePropertyId(property_id: unknown): [string?, void?] { - if (property_id !== undefined && property_id !== null) { - if (isNaN(Number(property_id))) { - return ['Property ID must be a number', undefined]; - } - } - - return [undefined, undefined]; + return [undefined, undefined]; } +export function validatePropertyId( + property_id: unknown, +): [string?, undefined?] { + if (property_id !== undefined && property_id !== null) { + if (Number.isNaN(Number(property_id))) { + return ["Property ID must be a number", undefined]; + } + } + + return [undefined, undefined]; +} diff --git a/backend/src/domain/utils/index.ts b/backend/src/domain/utils/index.ts index 953d4b5..cfb375c 100644 --- a/backend/src/domain/utils/index.ts +++ b/backend/src/domain/utils/index.ts @@ -1,5 +1,3 @@ -export * from './value-comparison.util'; -export * from './client-validation.util'; -export * from './phone-normalization.util'; - - +export * from "./client-validation.util"; +export * from "./phone-normalization.util"; +export * from "./value-comparison.util"; diff --git a/backend/src/domain/utils/phone-normalization.util.ts b/backend/src/domain/utils/phone-normalization.util.ts index 54820e5..1afddb9 100644 --- a/backend/src/domain/utils/phone-normalization.util.ts +++ b/backend/src/domain/utils/phone-normalization.util.ts @@ -1,17 +1,15 @@ - export function normalizePhone(phone: string): string { - if (!phone) return phone; - - const trimmed = phone.trim(); - - if (trimmed.startsWith('+')) { - const normalized = '+' + trimmed.substring(1).replace(/[\s\-\(\)\.]/g, ''); - - return normalized.length > 15 ? normalized.substring(0, 15) : normalized; - } - - const digitsOnly = trimmed.replace(/\D/g, ''); - - return digitsOnly.length > 15 ? digitsOnly.substring(0, 15) : digitsOnly; -} + if (!phone) return phone; + + const trimmed = phone.trim(); + + if (trimmed.startsWith("+")) { + const normalized = `+${trimmed.substring(1).replace(/[\s\-().]/g, "")}`; + return normalized.length > 15 ? normalized.substring(0, 15) : normalized; + } + + const digitsOnly = trimmed.replace(/\D/g, ""); + + return digitsOnly.length > 15 ? digitsOnly.substring(0, 15) : digitsOnly; +} diff --git a/backend/src/domain/utils/value-comparison.util.ts b/backend/src/domain/utils/value-comparison.util.ts index 7156fae..5d1b65c 100644 --- a/backend/src/domain/utils/value-comparison.util.ts +++ b/backend/src/domain/utils/value-comparison.util.ts @@ -1,47 +1,45 @@ - export function hasValueChanged(newValue: unknown, oldValue: unknown): boolean { - if (newValue === undefined) return false; - - if (newValue === null && (oldValue === null || oldValue === undefined)) return false; - if (oldValue === null && newValue === null) return false; - - const normalizedNew = normalizeValue(newValue); - const normalizedOld = normalizeValue(oldValue); - - return normalizedNew !== normalizedOld; -} + if (newValue === undefined) return false; + + if (newValue === null && (oldValue === null || oldValue === undefined)) + return false; + if (oldValue === null && newValue === null) return false; -function normalizeValue(value: unknown): string | number | boolean | null | undefined { - if (value === null || value === undefined) return value; - - if (typeof value === 'string') { - return value.trim(); - } - - if (typeof value === 'number' || typeof value === 'boolean') { - return value; - } - - return String(value); + const normalizedNew = normalizeValue(newValue); + const normalizedOld = normalizeValue(oldValue); + + return normalizedNew !== normalizedOld; } +function normalizeValue( + value: unknown, +): string | number | boolean | null | undefined { + if (value === null || value === undefined) return value; -export function getChangedFields>( - updateDto: Partial, - existingEntity: T -): Partial { - const changedFields: Partial = {}; - - for (const key in updateDto) { - if (updateDto.hasOwnProperty(key) && updateDto[key] !== undefined) { - if (hasValueChanged(updateDto[key], existingEntity[key])) { - changedFields[key] = updateDto[key]; - } - } - } - - return changedFields; + if (typeof value === "string") { + return value.trim(); + } + + if (typeof value === "number" || typeof value === "boolean") { + return value; + } + + return String(value); } +export function getChangedFields>( + updateDto: Partial, + existingEntity: T, +): Partial { + const changedFields: Partial = {}; + for (const key in updateDto) { + if (Object.hasOwn(updateDto, key) && updateDto[key] !== undefined) { + if (hasValueChanged(updateDto[key], existingEntity[key])) { + changedFields[key] = updateDto[key]; + } + } + } + return changedFields; +} diff --git a/backend/src/presentation/auth/controller.ts b/backend/src/presentation/auth/controller.ts index d26b178..6ba197b 100644 --- a/backend/src/presentation/auth/controller.ts +++ b/backend/src/presentation/auth/controller.ts @@ -1,138 +1,137 @@ -import { Request, Response } from 'express'; -import { CustomError, LoginProfileDto, RegisterProfileDto } from '../../domain'; -import type { JwtPayload } from '../../domain/interfaces/jwt-payload'; -import { AuthServices } from '../services/auth.services'; -import { RevokedTokenModel } from '../../data/postgres/models/revoked-token.model'; -import { jwtAdapter } from '../../config'; +import type { Request, Response } from "express"; + +import { jwtAdapter } from "../../config"; +import { RevokedTokenModel } from "../../data/postgres/models/revoked-token.model"; +import { CustomError, LoginProfileDto, RegisterProfileDto } from "../../domain"; +import type { JwtPayload } from "../../domain/interfaces/jwt-payload"; +import type { AuthServices } from "../services/auth.services"; export class AuthController { + constructor(private readonly authServices: AuthServices) {} + + private handleError = (error: unknown, res: Response) => { + if (error instanceof CustomError) { + return res.status(error.statusCode).json({ + message: error.message, + }); + } + console.log(`error: ${error}`); + return res.status(500).json({ + message: "Internal server error", + }); + }; + + registerUser(req: Request, res: Response) { + const [error, registerDto] = RegisterProfileDto.create(req.body); + + if (error || !registerDto) { + return res.status(400).json({ + message: error || "Invalid data", + }); + } + + this.authServices + .registerProfile(registerDto) + .then((result) => { + res.json(result); + }) + .catch((error) => { + this.handleError(error, res); + }); + } + + loginUser(req: Request, res: Response) { + const [error, loginDto] = LoginProfileDto.create(req.body); + + if (error || !loginDto) { + return res.status(400).json({ + message: error || "Invalid data", + }); + } + + this.authServices + .loginProfile(loginDto) + .then((result) => { + res.json(result); + }) + .catch((error) => { + this.handleError(error, res); + }); + } + + validateEmail(_req: Request, res: Response) { + res.json({ + message: "validate email", + }); + } + + getProfile(req: Request, res: Response) { + const user = req.user; + + if (!user || !user.id) { + return res.status(401).json({ + message: "User not authenticated", + }); + } + + this.authServices + .getProfile(user.id) + .then((result) => { + res.json(result); + }) + .catch((error) => { + this.handleError(error, res); + }); + } + + logout = async (req: Request, res: Response) => { + try { + const user = req.user; + const authHeader = req.headers.authorization; + + if (!user || !user.id) { + return res.status(401).json({ + message: "User not authenticated", + }); + } + + if (!authHeader) { + return res.status(400).json({ + message: "Token is required", + }); + } + + const token = authHeader.split(" ")[1]; + const decodedToken: JwtPayload | null = + await jwtAdapter.validateToken(token); + + if (!decodedToken || !decodedToken.jti) { + return res.status(400).json({ + message: "Invalid token", + }); + } + + const expiresAt = decodedToken.exp + ? new Date(decodedToken.exp * 1000) + : new Date(Date.now() + 24 * 60 * 60 * 1000); + + const ip = req.ip || req.socket.remoteAddress; + const userAgent = req.headers["user-agent"]; + + await RevokedTokenModel.create({ + token_jti: decodedToken.jti, + user_id: parseInt(user.id, 10), + expires_at: expiresAt, + reason: "logout", + ip_address: ip, + user_agent: userAgent, + }); - constructor( - private readonly authServices: AuthServices - ){} - - private handleError = (error: unknown, res: Response) => { - if (error instanceof CustomError) { - return res.status(error.statusCode).json({ - message: error.message - }); - } - console.log(`error: ${error}`); - return res.status(500).json({ - message: 'Internal server error' - }); - } - - - registerUser(req: Request, res: Response){ - const [error, registerDto] = RegisterProfileDto.create(req.body); - - if (error || !registerDto) { - return res.status(400).json({ - message: error || 'Invalid data' - }); - } - - this.authServices.registerProfile(registerDto) - .then( result => { - res.json(result); - }) - .catch( error => { - this.handleError(error, res); - }); - - } - - loginUser(req: Request, res: Response){ - const [error, loginDto] = LoginProfileDto.create(req.body); - - if (error || !loginDto) { - return res.status(400).json({ - message: error || 'Invalid data' - }); - } - - this.authServices.loginProfile(loginDto) - .then( result => { - res.json(result); - }) - .catch( error => { - this.handleError(error, res); - }); - } - - validateEmail(req: Request, res: Response){ - res.json({ - message: 'validate email' - }); - } - - getProfile(req: Request, res: Response){ - const user = req.user; - - if (!user || !user.id) { - return res.status(401).json({ - message: 'User not authenticated' - }); - } - - this.authServices.getProfile(user.id) - .then( result => { - res.json(result); - }) - .catch( error => { - this.handleError(error, res); - }); - } - - logout = async (req: Request, res: Response) => { - try { - const user = req.user; - const authHeader = req.headers.authorization; - - if (!user || !user.id) { - return res.status(401).json({ - message: 'User not authenticated' - }); - } - - if (!authHeader) { - return res.status(400).json({ - message: 'Token is required' - }); - } - - const token = authHeader.split(' ')[1]; - const decodedToken: JwtPayload | null = await jwtAdapter.validateToken(token); - - if (!decodedToken || !decodedToken.jti) { - return res.status(400).json({ - message: 'Invalid token' - }); - } - - const expiresAt = decodedToken.exp - ? new Date(decodedToken.exp * 1000) - : new Date(Date.now() + 24 * 60 * 60 * 1000); - - const ip = req.ip || req.socket.remoteAddress; - const userAgent = req.headers['user-agent']; - - await RevokedTokenModel.create({ - token_jti: decodedToken.jti, - user_id: parseInt(user.id), - expires_at: expiresAt, - reason: 'logout', - ip_address: ip, - user_agent: userAgent - }); - - return res.json({ - message: 'Logged out successfully' - }); - } catch (error) { - this.handleError(error, res); - } - }; - -} \ No newline at end of file + return res.json({ + message: "Logged out successfully", + }); + } catch (error) { + this.handleError(error, res); + } + }; +} diff --git a/backend/src/presentation/auth/routes.ts b/backend/src/presentation/auth/routes.ts index 40a3669..a810849 100644 --- a/backend/src/presentation/auth/routes.ts +++ b/backend/src/presentation/auth/routes.ts @@ -1,197 +1,203 @@ -import { Router } from 'express'; -import { bcryptAdapter, jwtAdapter } from '../../config'; -import { AuthController } from './controller'; -import { AuthServices } from '../services/auth.services'; -import { AuthMiddleware } from '../middlewares/auth.middleware'; +import { Router } from "express"; + +import { bcryptAdapter, jwtAdapter } from "../../config"; +import { AuthMiddleware } from "../middlewares/auth.middleware"; +import { AuthServices } from "../services/auth.services"; +import { AuthController } from "./controller"; export class Authroutes { - static get routes(): Router { - const router = Router(); - const authServices = new AuthServices(bcryptAdapter, jwtAdapter); - const controller = new AuthController(authServices); - const authMiddleware = new AuthMiddleware(jwtAdapter); - - // Rutas públicas - /** - * @swagger - * /api/auth/login: - * post: - * summary: Login a user - * tags: [Auth] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - email - * - password - * properties: - * email: - * type: string - * example: admin@example.com - * password: - * type: string - * example: password123 - * responses: - * 200: - * description: Login successful - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Login successful - * token: - * type: string - * description: JWT token to use in Authorization header (Bearer token) - * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsInJvbGVfaWQiOjEsImlhdCI6MTY5OTk5OTk5OSwiZXhwIjoxNzAwMDg2Mzk5fQ.example_signature - * user: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * email: - * type: string - * example: admin@example.com - * first_name: - * type: string - * example: Admin - * last_name: - * type: string - * example: User - * role_id: - * type: integer - * example: 1 - * 400: - * description: Bad request - * 401: - * description: Invalid credentials - */ - router.post('/login', (req, res) => controller.loginUser(req, res)); + static get routes(): Router { + const router = Router(); + const authServices = new AuthServices(bcryptAdapter, jwtAdapter); + const controller = new AuthController(authServices); + const authMiddleware = new AuthMiddleware(jwtAdapter); - /** - * @swagger - * /api/auth/register: - * post: - * summary: Register a new user (Admin only) - * tags: [Auth] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - first_name - * - last_name - * - email - * - password - * properties: - * first_name: - * type: string - * last_name: - * type: string - * email: - * type: string - * password: - * type: string - * phone: - * type: string - * role_id: - * type: integer - * responses: - * 201: - * description: User created - * 400: - * description: Bad request - * 401: - * description: Unauthorized - * 403: - * description: Admin access required - */ - router.post( - '/register', - authMiddleware.authenticate, - authMiddleware.requireAdmin, - (req, res) => controller.registerUser(req, res) - ); - router.get('/validate-email/:token', (req, res) => controller.validateEmail(req, res)); - - // Rutas protegidas (requieren autenticación) - /** - * @swagger - * /api/auth/me: - * get: - * summary: Get current user profile - * tags: [Auth] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User profile retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * email: - * type: string - * example: admin@example.com - * first_name: - * type: string - * example: Admin - * last_name: - * type: string - * example: User - * phone: - * type: string - * example: +541112345678 - * role_id: - * type: integer - * example: 1 - * 401: - * description: Unauthorized - Invalid or missing token - */ - router.get('/me', authMiddleware.authenticate, (req, res) => controller.getProfile(req, res)); + // Rutas públicas + /** + * @swagger + * /api/auth/login: + * post: + * summary: Login a user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * example: admin@example.com + * password: + * type: string + * example: password123 + * responses: + * 200: + * description: Login successful + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Login successful + * token: + * type: string + * description: JWT token to use in Authorization header (Bearer token) + * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsInJvbGVfaWQiOjEsImlhdCI6MTY5OTk5OTk5OSwiZXhwIjoxNzAwMDg2Mzk5fQ.example_signature + * user: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * email: + * type: string + * example: admin@example.com + * first_name: + * type: string + * example: Admin + * last_name: + * type: string + * example: User + * role_id: + * type: integer + * example: 1 + * 400: + * description: Bad request + * 401: + * description: Invalid credentials + */ + router.post("/login", (req, res) => controller.loginUser(req, res)); - /** - * @swagger - * /api/auth/logout: - * post: - * summary: Logout and revoke current token - * description: Revokes the current JWT token by adding it to the blacklist. The token will no longer be valid for authentication. - * tags: [Auth] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Logged out successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Logged out successfully - * 400: - * description: Bad request - Token is required or invalid - * 401: - * description: Unauthorized - Invalid or missing token - */ - router.post('/logout', authMiddleware.authenticate, (req, res) => controller.logout(req, res)); + /** + * @swagger + * /api/auth/register: + * post: + * summary: Register a new user (Admin only) + * tags: [Auth] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - first_name + * - last_name + * - email + * - password + * properties: + * first_name: + * type: string + * last_name: + * type: string + * email: + * type: string + * password: + * type: string + * phone: + * type: string + * role_id: + * type: integer + * responses: + * 201: + * description: User created + * 400: + * description: Bad request + * 401: + * description: Unauthorized + * 403: + * description: Admin access required + */ + router.post( + "/register", + authMiddleware.authenticate, + authMiddleware.requireAdmin, + (req, res) => controller.registerUser(req, res), + ); + router.get("/validate-email/:token", (req, res) => + controller.validateEmail(req, res), + ); - return router; - } -} + // Rutas protegidas (requieren autenticación) + /** + * @swagger + * /api/auth/me: + * get: + * summary: Get current user profile + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: User profile retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * email: + * type: string + * example: admin@example.com + * first_name: + * type: string + * example: Admin + * last_name: + * type: string + * example: User + * phone: + * type: string + * example: +541112345678 + * role_id: + * type: integer + * example: 1 + * 401: + * description: Unauthorized - Invalid or missing token + */ + router.get("/me", authMiddleware.authenticate, (req, res) => + controller.getProfile(req, res), + ); + /** + * @swagger + * /api/auth/logout: + * post: + * summary: Logout and revoke current token + * description: Revokes the current JWT token by adding it to the blacklist. The token will no longer be valid for authentication. + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Logged out successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Logged out successfully + * 400: + * description: Bad request - Token is required or invalid + * 401: + * description: Unauthorized - Invalid or missing token + */ + router.post("/logout", authMiddleware.authenticate, (req, res) => + controller.logout(req, res), + ); + + return router; + } +} diff --git a/backend/src/presentation/clients/controller.ts b/backend/src/presentation/clients/controller.ts index b46c0f1..74841e1 100644 --- a/backend/src/presentation/clients/controller.ts +++ b/backend/src/presentation/clients/controller.ts @@ -1,271 +1,274 @@ -import { Request, Response } from 'express'; -import { UpdateClientDto } from '../../domain'; -import { CreateTenantDto } from '../../domain/dtos/clients/create-tenant.dto'; -import { CreateOwnerDto } from '../../domain/dtos/clients/create-owner.dto'; -import { CreateLeadDto } from '../../domain/dtos/clients/create-lead.dto'; -import { ClientServices } from '../services/client.services'; -import { TenantServices } from '../services/tenant.services'; -import { OwnerServices } from '../services/owner.services'; -import { LeadServices } from '../services/lead.services'; -import { ErrorHandlerUtil } from '../shared/error-handler.util'; +import type { Request, Response } from "express"; + +import { UpdateClientDto } from "../../domain"; +import { CreateLeadDto } from "../../domain/dtos/clients/create-lead.dto"; +import { CreateOwnerDto } from "../../domain/dtos/clients/create-owner.dto"; +import { CreateTenantDto } from "../../domain/dtos/clients/create-tenant.dto"; +import type { ClientServices } from "../services/client.services"; +import type { LeadServices } from "../services/lead.services"; +import type { OwnerServices } from "../services/owner.services"; +import type { TenantServices } from "../services/tenant.services"; +import { ErrorHandlerUtil } from "../shared/error-handler.util"; export class ClientController { - constructor( - private readonly clientServices: ClientServices, - private readonly tenantServices: TenantServices, - private readonly ownerServices: OwnerServices, - private readonly leadServices: LeadServices - ) {} - - createTenant = async (req: Request, res: Response) => { - try { - const user = req.user; - if (!user || !user.id) { - return res.status(401).json({ - message: 'User not authenticated' - }); - } - - const [error, createTenantDto] = CreateTenantDto.create(req.body); - - if (error || !createTenantDto) { - return res.status(400).json({ - message: error || 'Invalid tenant data' - }); - } - - const result = await this.tenantServices.createTenantWithProperty( - createTenantDto, - Number(user.id) - ); - - return res.status(201).json({ - message: 'Tenant created successfully', - data: result - }); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - createOwner = async (req: Request, res: Response) => { - try { - const [error, createOwnerDto] = CreateOwnerDto.create(req.body); - - if (error || !createOwnerDto) { - return res.status(400).json({ - message: error || 'Invalid owner data' - }); - } - - const result = await this.ownerServices.createOwnerWithProperty(createOwnerDto); - - return res.status(201).json({ - message: 'Owner created successfully', - data: result - }); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - createLead = async (req: Request, res: Response) => { - try { - const [error, createLeadDto] = CreateLeadDto.create(req.body); - - if (error || !createLeadDto) { - return res.status(400).json({ - message: error || 'Invalid lead data' - }); - } - - const result = await this.leadServices.createLeadWithConsultation( - createLeadDto - ); - - return res.status(201).json({ - message: 'Lead created successfully', - data: result - }); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - listClients = async (req: Request, res: Response) => { - try { - const { - contact_category_id, - purchase_interest, - rental_interest, - city_id, - search, - includeDeleted, - limit, - offset, - } = req.query; - - const filters: Record = {}; - - if (contact_category_id) filters.contact_category_id = Number(contact_category_id); - if (purchase_interest !== undefined) filters.purchase_interest = purchase_interest === 'true'; - if (rental_interest !== undefined) filters.rental_interest = rental_interest === 'true'; - if (city_id) filters.city_id = Number(city_id); - if (search) filters.search = String(search); - if (limit) filters.limit = Number(limit); - if (offset) filters.offset = Number(offset); - if (includeDeleted === 'true') filters.includeDeleted = true; - - const result = await this.clientServices.listClients(filters); - return res.json(result); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - getClientById = async (req: Request, res: Response) => { - try { - const { id } = req.params; - - if (!id || isNaN(Number(id))) { - return res.status(400).json({ - message: 'Invalid client ID' - }); - } - - const result = await this.clientServices.getClientById(Number(id)); - return res.json(result); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - updateClient = async (req: Request, res: Response) => { - try { - const { id } = req.params; - - if (!id || isNaN(Number(id))) { - return res.status(400).json({ - message: 'Invalid client ID' - }); - } - - const [error, updateClientDto] = UpdateClientDto.create(req.body); - - if (error || !updateClientDto) { - return res.status(400).json({ - message: error || 'Invalid client data' - }); - } - - const result = await this.clientServices.updateClient(Number(id), updateClientDto); - return res.json({ - message: 'Client updated successfully', - data: result - }); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - deleteClient = async (req: Request, res: Response) => { - try { - const { id } = req.params; - - if (!id || isNaN(Number(id))) { - return res.status(400).json({ - message: 'Invalid client ID' - }); - } - - const result = await this.clientServices.deleteClient(Number(id)); - return res.json(result); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - restoreClient = async (req: Request, res: Response) => { - try { - const { id } = req.params; - - if (!id || isNaN(Number(id))) { - return res.status(400).json({ - message: 'Invalid client ID' - }); - } - - const result = await this.clientServices.restoreClient(Number(id)); - return res.json(result); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - addPropertyOfInterest = async (req: Request, res: Response) => { - try { - const { id } = req.params; - const { property_id, notes } = req.body; - - if (!id || isNaN(Number(id))) { - return res.status(400).json({ - message: 'Invalid client ID' - }); - } - - if (!property_id || isNaN(Number(property_id))) { - return res.status(400).json({ - message: 'property_id is required and must be a valid number' - }); - } - - const result = await this.clientServices.addPropertyOfInterest( - Number(id), - Number(property_id), - notes - ); - - return res.status(201).json({ - message: 'Property of interest added successfully', - data: result - }); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } - - addOwnedProperty = async (req: Request, res: Response) => { - try { - const { id } = req.params; - const { property_id } = req.body; - - if (!id || isNaN(Number(id))) { - return res.status(400).json({ - message: 'Invalid client ID' - }); - } - - if (!property_id || isNaN(Number(property_id))) { - return res.status(400).json({ - message: 'property_id is required and must be a valid number' - }); - } - - const result = await this.clientServices.addOwnedProperty( - Number(id), - Number(property_id) - ); - - return res.status(200).json({ - message: 'Property associated with owner successfully', - data: result - }); - } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Client'); - } - } + constructor( + private readonly clientServices: ClientServices, + private readonly tenantServices: TenantServices, + private readonly ownerServices: OwnerServices, + private readonly leadServices: LeadServices, + ) {} + + createTenant = async (req: Request, res: Response) => { + try { + const user = req.user; + if (!user || !user.id) { + return res.status(401).json({ + message: "User not authenticated", + }); + } + + const [error, createTenantDto] = CreateTenantDto.create(req.body); + + if (error || !createTenantDto) { + return res.status(400).json({ + message: error || "Invalid tenant data", + }); + } + + const result = await this.tenantServices.createTenantWithProperty( + createTenantDto, + Number(user.id), + ); + + return res.status(201).json({ + message: "Tenant created successfully", + data: result, + }); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + createOwner = async (req: Request, res: Response) => { + try { + const [error, createOwnerDto] = CreateOwnerDto.create(req.body); + + if (error || !createOwnerDto) { + return res.status(400).json({ + message: error || "Invalid owner data", + }); + } + + const result = + await this.ownerServices.createOwnerWithProperty(createOwnerDto); + + return res.status(201).json({ + message: "Owner created successfully", + data: result, + }); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + createLead = async (req: Request, res: Response) => { + try { + const [error, createLeadDto] = CreateLeadDto.create(req.body); + + if (error || !createLeadDto) { + return res.status(400).json({ + message: error || "Invalid lead data", + }); + } + + const result = + await this.leadServices.createLeadWithConsultation(createLeadDto); + + return res.status(201).json({ + message: "Lead created successfully", + data: result, + }); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + listClients = async (req: Request, res: Response) => { + try { + const { + contact_category_id, + purchase_interest, + rental_interest, + city_id, + search, + includeDeleted, + limit, + offset, + } = req.query; + + const filters: Record = {}; + + if (contact_category_id) + filters.contact_category_id = Number(contact_category_id); + if (purchase_interest !== undefined) + filters.purchase_interest = purchase_interest === "true"; + if (rental_interest !== undefined) + filters.rental_interest = rental_interest === "true"; + if (city_id) filters.city_id = Number(city_id); + if (search) filters.search = String(search); + if (limit) filters.limit = Number(limit); + if (offset) filters.offset = Number(offset); + if (includeDeleted === "true") filters.includeDeleted = true; + + const result = await this.clientServices.listClients(filters); + return res.json(result); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + getClientById = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + if (!id || Number.isNaN(Number(id))) { + return res.status(400).json({ + message: "Invalid client ID", + }); + } + + const result = await this.clientServices.getClientById(Number(id)); + return res.json(result); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + updateClient = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + if (!id || Number.isNaN(Number(id))) { + return res.status(400).json({ + message: "Invalid client ID", + }); + } + + const [error, updateClientDto] = UpdateClientDto.create(req.body); + + if (error || !updateClientDto) { + return res.status(400).json({ + message: error || "Invalid client data", + }); + } + + const result = await this.clientServices.updateClient( + Number(id), + updateClientDto, + ); + return res.json({ + message: "Client updated successfully", + data: result, + }); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + deleteClient = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + if (!id || Number.isNaN(Number(id))) { + return res.status(400).json({ + message: "Invalid client ID", + }); + } + + const result = await this.clientServices.deleteClient(Number(id)); + return res.json(result); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + restoreClient = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + if (!id || Number.isNaN(Number(id))) { + return res.status(400).json({ + message: "Invalid client ID", + }); + } + + const result = await this.clientServices.restoreClient(Number(id)); + return res.json(result); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + addPropertyOfInterest = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { property_id, notes } = req.body; + + if (!id || Number.isNaN(Number(id))) { + return res.status(400).json({ + message: "Invalid client ID", + }); + } + + if (!property_id || Number.isNaN(Number(property_id))) { + return res.status(400).json({ + message: "property_id is required and must be a valid number", + }); + } + + const result = await this.clientServices.addPropertyOfInterest( + Number(id), + Number(property_id), + notes, + ); + + return res.status(201).json({ + message: "Property of interest added successfully", + data: result, + }); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; + + addOwnedProperty = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { property_id } = req.body; + + if (!id || Number.isNaN(Number(id))) { + return res.status(400).json({ + message: "Invalid client ID", + }); + } + + if (!property_id || Number.isNaN(Number(property_id))) { + return res.status(400).json({ + message: "property_id is required and must be a valid number", + }); + } + + const result = await this.clientServices.addOwnedProperty( + Number(id), + Number(property_id), + ); + + return res.status(200).json({ + message: "Property associated with owner successfully", + data: result, + }); + } catch (error) { + ErrorHandlerUtil.handleError(error, res, "Client"); + } + }; } - - - - diff --git a/backend/src/presentation/clients/routes.ts b/backend/src/presentation/clients/routes.ts index 5fb3425..0816424 100644 --- a/backend/src/presentation/clients/routes.ts +++ b/backend/src/presentation/clients/routes.ts @@ -1,959 +1,940 @@ -import { Router } from 'express'; -import { ClientController } from './controller'; -import { ClientServices } from '../services/client.services'; -import { TenantServices } from '../services/tenant.services'; -import { OwnerServices } from '../services/owner.services'; -import { LeadServices } from '../services/lead.services'; -import { AuthMiddleware } from '../middlewares/auth.middleware'; -import { jwtAdapter } from '../../config'; +import { Router } from "express"; -export class ClientRoutes { - static get routes(): Router { - const router = Router(); - - const clientServices = new ClientServices(); - const tenantServices = new TenantServices(); - const ownerServices = new OwnerServices(); - const leadServices = new LeadServices(); - const controller = new ClientController( - clientServices, - tenantServices, - ownerServices, - leadServices - ); - const authMiddleware = new AuthMiddleware(jwtAdapter); - - /** - * @swagger - * /api/clients/tenants: - * post: - * summary: Create a new tenant with property and rental contract - * tags: [Clients] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - first_name - * - last_name - * - phone - * properties: - * first_name: - * type: string - * example: Juan - * last_name: - * type: string - * example: Pérez - * phone: - * type: string - * example: "+54 11 1234-5678" - * email: - * type: string - * example: juan.perez@example.com - * dni: - * type: string - * example: "12345678" - * address: - * type: string - * example: Av. Corrientes 1234, CABA - * notes: - * type: string - * example: Inquilino responsable, referencias verificadas - * property_id: - * type: integer - * example: 7 - * property_address: - * type: string - * example: Calle Falsa 123 - * contract_start_date: - * type: string - * format: date - * example: "2024-01-01" - * contract_end_date: - * type: string - * format: date - * example: "2025-01-01" - * next_increase_date: - * type: string - * format: date - * example: "2024-07-01" - * monthly_amount: - * type: number - * example: 150000 - * currency_type_id: - * type: integer - * example: 1 - * currency_type: - * type: string - * example: ARS - * remind_increase: - * type: boolean - * example: true - * remind_contract_end: - * type: boolean - * example: true - * external_reference: - * type: string - * example: CONTRATO-2024-001 - * responses: - * 201: - * description: Tenant created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Tenant created successfully - * data: - * type: object - * properties: - * client: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * first_name: - * type: string - * example: Juan - * last_name: - * type: string - * example: Pérez - * email: - * type: string - * example: juan.perez@example.com - * phone: - * type: string - * example: +541112345678 - * dni: - * type: string - * example: "12345678" - * address: - * type: string - * example: Av. Corrientes 1234, CABA - * notes: - * type: string - * example: Inquilino responsable, referencias verificadas - * contact_category_id: - * type: integer - * example: 2 - * rental_interest: - * type: boolean - * example: true - * client_rental: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * property_id: - * type: integer - * example: 7 - * contract_start_date: - * type: string - * format: date - * example: "2024-01-01" - * contract_end_date: - * type: string - * format: date - * example: "2025-01-01" - * next_increase_date: - * type: string - * format: date - * example: "2024-07-01" - * rental: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * monthly_amount: - * type: string - * example: "150000.00" - * currency_type_id: - * type: integer - * example: 1 - * start_date: - * type: string - * format: date - * example: "2024-01-01" - * end_date: - * type: string - * format: date - * example: "2025-01-01" - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Validation error - * 401: - * description: Unauthorized - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Authorization header is required - */ - router.post( - '/tenants', - authMiddleware.authenticate, - (req, res) => controller.createTenant(req, res) - ); - - /** - * @swagger - * /api/clients/owners: - * post: - * summary: Create a new owner with optional property association - * tags: [Clients] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - first_name - * - last_name - * - phone - * properties: - * first_name: - * type: string - * example: Ana - * last_name: - * type: string - * example: Martínez - * phone: - * type: string - * example: "+54 11 1111-2222" - * email: - * type: string - * example: ana.martinez@example.com - * dni: - * type: string - * example: "11223344" - * address: - * type: string - * example: Av. Libertador 7890, CABA - * notes: - * type: string - * example: Propietaria de departamento en Palermo - * property_id: - * type: integer - * example: 1 - * responses: - * 201: - * description: Owner created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Owner created successfully - * data: - * type: object - * properties: - * client: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * first_name: - * type: string - * example: Ana - * last_name: - * type: string - * example: Martínez - * email: - * type: string - * example: ana.martinez@example.com - * phone: - * type: string - * example: +541111112222 - * dni: - * type: string - * example: "11223344" - * address: - * type: string - * example: Av. Libertador 7890, CABA - * notes: - * type: string - * example: Propietaria de departamento en Palermo - * contact_category_id: - * type: integer - * example: 1 - * purchase_interest: - * type: boolean - * example: false - * property: - * type: object - * nullable: true - * description: Property object if property_id was provided and property was associated - * properties: - * id: - * type: integer - * example: 1 - * title: - * type: string - * example: Departamento en Palermo - * owner_id: - * type: integer - * example: 1 - * info: - * type: string - * nullable: true - * example: Property with ID 1 already has an owner assigned. Owner created but not associated. - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Validation error - * 401: - * description: Unauthorized - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Authorization header is required - */ - router.post( - '/owners', - authMiddleware.authenticate, - (req, res) => controller.createOwner(req, res) - ); +import { jwtAdapter } from "../../config"; +import { AuthMiddleware } from "../middlewares/auth.middleware"; +import { ClientServices } from "../services/client.services"; +import { LeadServices } from "../services/lead.services"; +import { OwnerServices } from "../services/owner.services"; +import { TenantServices } from "../services/tenant.services"; +import { ClientController } from "./controller"; - /** - * @swagger - * /api/clients/leads: - * post: - * summary: Create a new lead with consultation and property of interest - * tags: [Clients] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - first_name - * - last_name - * - phone - * - email - * properties: - * first_name: - * type: string - * example: Pedro - * last_name: - * type: string - * example: Gómez - * phone: - * type: string - * example: "+54 11 9999-0000" - * email: - * type: string - * example: pedro.gomez@example.com - * notes: - * type: string - * example: Interesado en departamento de 2 ambientes en Palermo, presupuesto $200.000 - * consultation_type_id: - * type: integer - * example: 1 - * consultation_type: - * type: string - * example: Consulta de Alquiler - * property_id: - * type: integer - * example: 1 - * responses: - * 201: - * description: Lead created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Lead created successfully - * data: - * type: object - * properties: - * client: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * first_name: - * type: string - * example: Pedro - * last_name: - * type: string - * example: Gómez - * email: - * type: string - * example: pedro.gomez@example.com - * phone: - * type: string - * example: "+541199990000" - * notes: - * type: string - * example: Interesado en departamento de 2 ambientes en Palermo, presupuesto $200.000 - * contact_category_id: - * type: integer - * example: 3 - * rental_interest: - * type: boolean - * example: true - * consultation: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * consultation_type_id: - * type: integer - * example: 1 - * consultation_type: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * name: - * type: string - * example: Consulta de Alquiler - * property_id: - * type: integer - * nullable: true - * example: 1 - * message: - * type: string - * nullable: true - * example: Interesado en departamento de 2 ambientes en Palermo, presupuesto $200.000 - * is_read: - * type: boolean - * example: false - * created_at: - * type: string - * format: date-time - * example: "2025-12-04T01:44:56.021Z" - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Validation error - * 401: - * description: Unauthorized - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Authorization header is required - */ - router.post( - '/leads', - authMiddleware.authenticate, - (req, res) => controller.createLead(req, res) - ); +export class ClientRoutes { + static get routes(): Router { + const router = Router(); - /** - * @swagger - * /api/clients: - * get: - * summary: List clients with optional filters - * tags: [Clients] - * security: - * - bearerAuth: [] - * parameters: - * - in: query - * name: contact_category_id - * schema: - * type: integer - * - in: query - * name: search - * schema: - * type: string - * - in: query - * name: limit - * schema: - * type: integer - * - in: query - * name: offset - * schema: - * type: integer - * responses: - * 200: - * description: List of clients - */ - router.get( - '/', - authMiddleware.authenticate, - (req, res) => controller.listClients(req, res) - ); + const clientServices = new ClientServices(); + const tenantServices = new TenantServices(); + const ownerServices = new OwnerServices(); + const leadServices = new LeadServices(); + const controller = new ClientController( + clientServices, + tenantServices, + ownerServices, + leadServices, + ); + const authMiddleware = new AuthMiddleware(jwtAdapter); - /** - * @swagger - * /api/clients/{id}: - * get: - * summary: Get client by ID with enriched property details - * tags: [Clients] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: Client ID - * responses: - * 200: - * description: Client details with enriched property information - * content: - * application/json: - * schema: - * type: object - * properties: - * client: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * first_name: - * type: string - * example: Juan - * last_name: - * type: string - * example: Pérez - * email: - * type: string - * example: juan.perez@example.com - * phone: - * type: string - * example: "+541112345678" - * contact_category: - * type: object - * properties: - * id: - * type: integer - * name: - * type: string - * example: Lead - * city: - * type: object - * nullable: true - * properties_of_interest: - * type: array - * description: Properties of interest (for Leads and Tenants) - * items: - * type: object - * properties: - * id: - * type: integer - * example: 10 - * title: - * type: string - * example: Departamento 2 ambientes Palermo - * description: - * type: string - * example: Hermoso departamento con vista al parque - * surface_area: - * type: number - * example: 65.5 - * bedrooms: - * type: integer - * example: 2 - * bathrooms: - * type: integer - * example: 1 - * garage: - * type: boolean - * example: true - * address: - * type: object - * nullable: true - * properties: - * full_address: - * type: string - * example: Av. Santa Fe 1234, Palermo - * neighborhood: - * type: string - * example: Palermo - * city: - * type: object - * properties: - * id: - * type: integer - * name: - * type: string - * province: - * type: object - * properties: - * id: - * type: integer - * name: - * type: string - * country: - * type: object - * properties: - * id: - * type: integer - * name: - * type: string - * location: - * type: object - * properties: - * latitude: - * type: number - * example: -34.603722 - * longitude: - * type: number - * example: -58.381592 - * prices: - * type: array - * description: All prices for this property (sale, rental, etc.) - * items: - * type: object - * properties: - * amount: - * type: number - * example: 250000 - * currency: - * type: object - * properties: - * id: - * type: integer - * name: - * type: string - * symbol: - * type: string - * example: ARS - * operation_type: - * type: object - * properties: - * id: - * type: integer - * name: - * type: string - * example: Alquiler - * main_image: - * type: object - * nullable: true - * properties: - * id: - * type: integer - * url: - * type: string - * example: https://res.cloudinary.com/xxx/image/upload/v123/property_10.jpg - * is_primary: - * type: boolean - * example: true - * age: - * type: object - * nullable: true - * properties: - * id: - * type: integer - * name: - * type: string - * example: 5 años - * property_type: - * type: object - * properties: - * id: - * type: integer - * name: - * type: string - * property_status: - * type: object - * properties: - * id: - * type: integer - * name: - * type: string - * interest_created_at: - * type: string - * format: date-time - * interest_notes: - * type: string - * owned_properties: - * type: array - * description: Properties owned by the client (for Owners) - * items: - * type: object - * description: Same structure as properties_of_interest, without interest_created_at and interest_notes - * rented_property: - * type: object - * nullable: true - * description: Currently rented property (for Tenants) - * properties: - * id: - * type: integer - * title: - * type: string - * description: - * type: string - * surface_area: - * type: number - * bedrooms: - * type: integer - * bathrooms: - * type: integer - * garage: - * type: boolean - * address: - * type: object - * nullable: true - * prices: - * type: array - * description: All prices for this property - * main_image: - * type: object - * nullable: true - * age: - * type: object - * nullable: true - * property_type: - * type: object - * property_status: - * type: object - * rental: - * type: object - * properties: - * id: - * type: integer - * contract_start_date: - * type: string - * format: date - * contract_end_date: - * type: string - * format: date - * monthly_amount: - * type: string - * currency: - * type: object - * 404: - * description: Client not found - */ - router.get( - '/:id', - authMiddleware.authenticate, - (req, res) => controller.getClientById(req, res) - ); + /** + * @swagger + * /api/clients/tenants: + * post: + * summary: Create a new tenant with property and rental contract + * tags: [Clients] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - first_name + * - last_name + * - phone + * properties: + * first_name: + * type: string + * example: Juan + * last_name: + * type: string + * example: Pérez + * phone: + * type: string + * example: "+54 11 1234-5678" + * email: + * type: string + * example: juan.perez@example.com + * dni: + * type: string + * example: "12345678" + * address: + * type: string + * example: Av. Corrientes 1234, CABA + * notes: + * type: string + * example: Inquilino responsable, referencias verificadas + * property_id: + * type: integer + * example: 7 + * property_address: + * type: string + * example: Calle Falsa 123 + * contract_start_date: + * type: string + * format: date + * example: "2024-01-01" + * contract_end_date: + * type: string + * format: date + * example: "2025-01-01" + * next_increase_date: + * type: string + * format: date + * example: "2024-07-01" + * monthly_amount: + * type: number + * example: 150000 + * currency_type_id: + * type: integer + * example: 1 + * currency_type: + * type: string + * example: ARS + * remind_increase: + * type: boolean + * example: true + * remind_contract_end: + * type: boolean + * example: true + * external_reference: + * type: string + * example: CONTRATO-2024-001 + * responses: + * 201: + * description: Tenant created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Tenant created successfully + * data: + * type: object + * properties: + * client: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * first_name: + * type: string + * example: Juan + * last_name: + * type: string + * example: Pérez + * email: + * type: string + * example: juan.perez@example.com + * phone: + * type: string + * example: +541112345678 + * dni: + * type: string + * example: "12345678" + * address: + * type: string + * example: Av. Corrientes 1234, CABA + * notes: + * type: string + * example: Inquilino responsable, referencias verificadas + * contact_category_id: + * type: integer + * example: 2 + * rental_interest: + * type: boolean + * example: true + * client_rental: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * property_id: + * type: integer + * example: 7 + * contract_start_date: + * type: string + * format: date + * example: "2024-01-01" + * contract_end_date: + * type: string + * format: date + * example: "2025-01-01" + * next_increase_date: + * type: string + * format: date + * example: "2024-07-01" + * rental: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * monthly_amount: + * type: string + * example: "150000.00" + * currency_type_id: + * type: integer + * example: 1 + * start_date: + * type: string + * format: date + * example: "2024-01-01" + * end_date: + * type: string + * format: date + * example: "2025-01-01" + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Validation error + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Authorization header is required + */ + router.post("/tenants", authMiddleware.authenticate, (req, res) => + controller.createTenant(req, res), + ); - /** - * @swagger - * /api/clients/{id}: - * patch: - * summary: Update a client (partial update) - * tags: [Clients] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * description: All fields are optional. At least one field must be provided. - * responses: - * 200: - * description: Client updated - * 400: - * description: Bad request - * 404: - * description: Client not found - */ - router.patch( - '/:id', - authMiddleware.authenticate, - (req, res) => controller.updateClient(req, res) - ); + /** + * @swagger + * /api/clients/owners: + * post: + * summary: Create a new owner with optional property association + * tags: [Clients] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - first_name + * - last_name + * - phone + * properties: + * first_name: + * type: string + * example: Ana + * last_name: + * type: string + * example: Martínez + * phone: + * type: string + * example: "+54 11 1111-2222" + * email: + * type: string + * example: ana.martinez@example.com + * dni: + * type: string + * example: "11223344" + * address: + * type: string + * example: Av. Libertador 7890, CABA + * notes: + * type: string + * example: Propietaria de departamento en Palermo + * property_id: + * type: integer + * example: 1 + * responses: + * 201: + * description: Owner created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Owner created successfully + * data: + * type: object + * properties: + * client: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * first_name: + * type: string + * example: Ana + * last_name: + * type: string + * example: Martínez + * email: + * type: string + * example: ana.martinez@example.com + * phone: + * type: string + * example: +541111112222 + * dni: + * type: string + * example: "11223344" + * address: + * type: string + * example: Av. Libertador 7890, CABA + * notes: + * type: string + * example: Propietaria de departamento en Palermo + * contact_category_id: + * type: integer + * example: 1 + * purchase_interest: + * type: boolean + * example: false + * property: + * type: object + * nullable: true + * description: Property object if property_id was provided and property was associated + * properties: + * id: + * type: integer + * example: 1 + * title: + * type: string + * example: Departamento en Palermo + * owner_id: + * type: integer + * example: 1 + * info: + * type: string + * nullable: true + * example: Property with ID 1 already has an owner assigned. Owner created but not associated. + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Validation error + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Authorization header is required + */ + router.post("/owners", authMiddleware.authenticate, (req, res) => + controller.createOwner(req, res), + ); - /** - * @swagger - * /api/clients/{id}: - * delete: - * summary: Delete a client (soft delete) - * tags: [Clients] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Client deleted - * 404: - * description: Client not found - */ - router.delete( - '/:id', - authMiddleware.authenticate, - (req, res) => controller.deleteClient(req, res) - ); + /** + * @swagger + * /api/clients/leads: + * post: + * summary: Create a new lead with consultation and property of interest + * tags: [Clients] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - first_name + * - last_name + * - phone + * - email + * properties: + * first_name: + * type: string + * example: Pedro + * last_name: + * type: string + * example: Gómez + * phone: + * type: string + * example: "+54 11 9999-0000" + * email: + * type: string + * example: pedro.gomez@example.com + * notes: + * type: string + * example: Interesado en departamento de 2 ambientes en Palermo, presupuesto $200.000 + * consultation_type_id: + * type: integer + * example: 1 + * consultation_type: + * type: string + * example: Consulta de Alquiler + * property_id: + * type: integer + * example: 1 + * responses: + * 201: + * description: Lead created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Lead created successfully + * data: + * type: object + * properties: + * client: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * first_name: + * type: string + * example: Pedro + * last_name: + * type: string + * example: Gómez + * email: + * type: string + * example: pedro.gomez@example.com + * phone: + * type: string + * example: "+541199990000" + * notes: + * type: string + * example: Interesado en departamento de 2 ambientes en Palermo, presupuesto $200.000 + * contact_category_id: + * type: integer + * example: 3 + * rental_interest: + * type: boolean + * example: true + * consultation: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * consultation_type_id: + * type: integer + * example: 1 + * consultation_type: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * name: + * type: string + * example: Consulta de Alquiler + * property_id: + * type: integer + * nullable: true + * example: 1 + * message: + * type: string + * nullable: true + * example: Interesado en departamento de 2 ambientes en Palermo, presupuesto $200.000 + * is_read: + * type: boolean + * example: false + * created_at: + * type: string + * format: date-time + * example: "2025-12-04T01:44:56.021Z" + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Validation error + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Authorization header is required + */ + router.post("/leads", authMiddleware.authenticate, (req, res) => + controller.createLead(req, res), + ); - /** - * @swagger - * /api/clients/{id}/restore: - * post: - * summary: Restore a deleted client - * tags: [Clients] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Client restored - * 404: - * description: Client not found - */ - router.post( - '/:id/restore', - authMiddleware.authenticate, - (req, res) => controller.restoreClient(req, res) - ); + /** + * @swagger + * /api/clients: + * get: + * summary: List clients with optional filters + * tags: [Clients] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: contact_category_id + * schema: + * type: integer + * - in: query + * name: search + * schema: + * type: string + * - in: query + * name: limit + * schema: + * type: integer + * - in: query + * name: offset + * schema: + * type: integer + * responses: + * 200: + * description: List of clients + */ + router.get("/", authMiddleware.authenticate, (req, res) => + controller.listClients(req, res), + ); - /** - * @swagger - * /api/clients/{id}/properties-of-interest: - * post: - * summary: Add a property of interest to a client (Lead or Tenant) - * tags: [Clients] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - property_id - * properties: - * property_id: - * type: integer - * example: 123 - * notes: - * type: string - * example: Cliente interesado en esta propiedad - * responses: - * 201: - * description: Property of interest added successfully - * 400: - * description: Bad request - * 404: - * description: Client or property not found - */ - router.post( - '/:id/properties-of-interest', - authMiddleware.authenticate, - (req, res) => controller.addPropertyOfInterest(req, res) - ); + /** + * @swagger + * /api/clients/{id}: + * get: + * summary: Get client by ID with enriched property details + * tags: [Clients] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Client ID + * responses: + * 200: + * description: Client details with enriched property information + * content: + * application/json: + * schema: + * type: object + * properties: + * client: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * first_name: + * type: string + * example: Juan + * last_name: + * type: string + * example: Pérez + * email: + * type: string + * example: juan.perez@example.com + * phone: + * type: string + * example: "+541112345678" + * contact_category: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * example: Lead + * city: + * type: object + * nullable: true + * properties_of_interest: + * type: array + * description: Properties of interest (for Leads and Tenants) + * items: + * type: object + * properties: + * id: + * type: integer + * example: 10 + * title: + * type: string + * example: Departamento 2 ambientes Palermo + * description: + * type: string + * example: Hermoso departamento con vista al parque + * surface_area: + * type: number + * example: 65.5 + * bedrooms: + * type: integer + * example: 2 + * bathrooms: + * type: integer + * example: 1 + * garage: + * type: boolean + * example: true + * address: + * type: object + * nullable: true + * properties: + * full_address: + * type: string + * example: Av. Santa Fe 1234, Palermo + * neighborhood: + * type: string + * example: Palermo + * city: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * province: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * country: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * location: + * type: object + * properties: + * latitude: + * type: number + * example: -34.603722 + * longitude: + * type: number + * example: -58.381592 + * prices: + * type: array + * description: All prices for this property (sale, rental, etc.) + * items: + * type: object + * properties: + * amount: + * type: number + * example: 250000 + * currency: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * symbol: + * type: string + * example: ARS + * operation_type: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * example: Alquiler + * main_image: + * type: object + * nullable: true + * properties: + * id: + * type: integer + * url: + * type: string + * example: https://res.cloudinary.com/xxx/image/upload/v123/property_10.jpg + * is_primary: + * type: boolean + * example: true + * age: + * type: object + * nullable: true + * properties: + * id: + * type: integer + * name: + * type: string + * example: 5 años + * property_type: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * property_status: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * interest_created_at: + * type: string + * format: date-time + * interest_notes: + * type: string + * owned_properties: + * type: array + * description: Properties owned by the client (for Owners) + * items: + * type: object + * description: Same structure as properties_of_interest, without interest_created_at and interest_notes + * rented_property: + * type: object + * nullable: true + * description: Currently rented property (for Tenants) + * properties: + * id: + * type: integer + * title: + * type: string + * description: + * type: string + * surface_area: + * type: number + * bedrooms: + * type: integer + * bathrooms: + * type: integer + * garage: + * type: boolean + * address: + * type: object + * nullable: true + * prices: + * type: array + * description: All prices for this property + * main_image: + * type: object + * nullable: true + * age: + * type: object + * nullable: true + * property_type: + * type: object + * property_status: + * type: object + * rental: + * type: object + * properties: + * id: + * type: integer + * contract_start_date: + * type: string + * format: date + * contract_end_date: + * type: string + * format: date + * monthly_amount: + * type: string + * currency: + * type: object + * 404: + * description: Client not found + */ + router.get("/:id", authMiddleware.authenticate, (req, res) => + controller.getClientById(req, res), + ); - /** - * @swagger - * /api/clients/{id}/owned-properties: - * post: - * summary: Associate a property to an Owner - * tags: [Clients] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - property_id - * properties: - * property_id: - * type: integer - * example: 123 - * responses: - * 200: - * description: Property associated with owner successfully - * 400: - * description: Bad request - * 404: - * description: Client or property not found - */ - router.post( - '/:id/owned-properties', - authMiddleware.authenticate, - (req, res) => controller.addOwnedProperty(req, res) - ); + /** + * @swagger + * /api/clients/{id}: + * patch: + * summary: Update a client (partial update) + * tags: [Clients] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * description: All fields are optional. At least one field must be provided. + * responses: + * 200: + * description: Client updated + * 400: + * description: Bad request + * 404: + * description: Client not found + */ + router.patch("/:id", authMiddleware.authenticate, (req, res) => + controller.updateClient(req, res), + ); - return router; - } -} + /** + * @swagger + * /api/clients/{id}: + * delete: + * summary: Delete a client (soft delete) + * tags: [Clients] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Client deleted + * 404: + * description: Client not found + */ + router.delete("/:id", authMiddleware.authenticate, (req, res) => + controller.deleteClient(req, res), + ); + /** + * @swagger + * /api/clients/{id}/restore: + * post: + * summary: Restore a deleted client + * tags: [Clients] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Client restored + * 404: + * description: Client not found + */ + router.post("/:id/restore", authMiddleware.authenticate, (req, res) => + controller.restoreClient(req, res), + ); + /** + * @swagger + * /api/clients/{id}/properties-of-interest: + * post: + * summary: Add a property of interest to a client (Lead or Tenant) + * tags: [Clients] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - property_id + * properties: + * property_id: + * type: integer + * example: 123 + * notes: + * type: string + * example: Cliente interesado en esta propiedad + * responses: + * 201: + * description: Property of interest added successfully + * 400: + * description: Bad request + * 404: + * description: Client or property not found + */ + router.post( + "/:id/properties-of-interest", + authMiddleware.authenticate, + (req, res) => controller.addPropertyOfInterest(req, res), + ); + /** + * @swagger + * /api/clients/{id}/owned-properties: + * post: + * summary: Associate a property to an Owner + * tags: [Clients] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - property_id + * properties: + * property_id: + * type: integer + * example: 123 + * responses: + * 200: + * description: Property associated with owner successfully + * 400: + * description: Bad request + * 404: + * description: Client or property not found + */ + router.post( + "/:id/owned-properties", + authMiddleware.authenticate, + (req, res) => controller.addOwnedProperty(req, res), + ); + return router; + } +} diff --git a/backend/src/presentation/company/controller.ts b/backend/src/presentation/company/controller.ts index 9f3ca99..dcc6cda 100644 --- a/backend/src/presentation/company/controller.ts +++ b/backend/src/presentation/company/controller.ts @@ -6,7 +6,7 @@ import type { CompanyServices } from "../services/company.services"; export class CompanyController { constructor(private readonly companyServices: CompanyServices) {} - getSettings = async (req: Request, res: Response) => { + getSettings = async (_req: Request, res: Response) => { try { const settings = await this.companyServices.getCompanySettings(); return res.json(settings); diff --git a/backend/src/presentation/consultations/controller.ts b/backend/src/presentation/consultations/controller.ts index 99cc923..97bfb13 100644 --- a/backend/src/presentation/consultations/controller.ts +++ b/backend/src/presentation/consultations/controller.ts @@ -1,8 +1,8 @@ import type { Request, Response } from "express"; import { CustomError } from "../../domain"; -import { CreatePropertyConsultationDto } from "../../domain/dtos/consultations/create-property-consultation.dto"; import { CreateGeneralConsultationDto } from "../../domain/dtos/consultations/create-general-consultation.dto"; +import { CreatePropertyConsultationDto } from "../../domain/dtos/consultations/create-property-consultation.dto"; import { DeleteMultipleConsultationsDto } from "../../domain/dtos/consultations/delete-multiple-consultations.dto"; import type { PropertyConsultationServices } from "../services/property-consultation.services"; @@ -84,10 +84,10 @@ export class ConsultationController { } = req.query; const filters = { - limit: limit ? parseInt(limit as string) : undefined, - offset: offset ? parseInt(offset as string) : undefined, + limit: limit ? parseInt(limit as string, 10) : undefined, + offset: offset ? parseInt(offset as string, 10) : undefined, consultation_type_id: consultation_type_id - ? parseInt(consultation_type_id as string) + ? parseInt(consultation_type_id as string, 10) : undefined, start_date: start_date as string, end_date: end_date as string, @@ -113,9 +113,9 @@ export class ConsultationController { } const { id } = req.params; - const consultationId = parseInt(id); + const consultationId = parseInt(id, 10); - if (isNaN(consultationId) || consultationId <= 0) { + if (Number.isNaN(consultationId) || consultationId <= 0) { return res.status(400).json({ message: "Invalid consultation ID", }); @@ -170,9 +170,9 @@ export class ConsultationController { } const { id } = req.params; - const consultationId = parseInt(id); + const consultationId = parseInt(id, 10); - if (isNaN(consultationId) || consultationId <= 0) { + if (Number.isNaN(consultationId) || consultationId <= 0) { return res.status(400).json({ message: "Invalid consultation ID", }); @@ -196,15 +196,16 @@ export class ConsultationController { } const { id } = req.params; - const consultationId = parseInt(id); + const consultationId = parseInt(id, 10); - if (isNaN(consultationId) || consultationId <= 0) { + if (Number.isNaN(consultationId) || consultationId <= 0) { return res.status(400).json({ message: "Invalid consultation ID", }); } - const result = await this.consultationServices.markAsUnread(consultationId); + const result = + await this.consultationServices.markAsUnread(consultationId); return res.status(200).json(result); } catch (error) { @@ -222,9 +223,9 @@ export class ConsultationController { } const { id } = req.params; - const consultationId = parseInt(id); + const consultationId = parseInt(id, 10); - if (isNaN(consultationId) || consultationId <= 0) { + if (Number.isNaN(consultationId) || consultationId <= 0) { return res.status(400).json({ message: "Invalid consultation ID", }); diff --git a/backend/src/presentation/dashboard/controller.ts b/backend/src/presentation/dashboard/controller.ts index c5586f0..8253605 100644 --- a/backend/src/presentation/dashboard/controller.ts +++ b/backend/src/presentation/dashboard/controller.ts @@ -1,12 +1,12 @@ -import type { Request, Response } from 'express'; -import { ErrorHandlerUtil } from '../shared/error-handler.util'; -import type { DashboardServices } from '../services/dashboard.services'; +import type { Request, Response } from "express"; + +import type { DashboardServices } from "../services/dashboard.services"; +import { ErrorHandlerUtil } from "../shared/error-handler.util"; export class DashboardController { constructor(private readonly dashboardServices: DashboardServices) {} - - getDashboard = async (req: Request, res: Response) => { + getDashboard = async (_req: Request, res: Response) => { try { const result = await this.dashboardServices.getDashboardData(); @@ -14,8 +14,7 @@ export class DashboardController { data: result, }); } catch (error) { - ErrorHandlerUtil.handleError(error, res, 'Dashboard'); + ErrorHandlerUtil.handleError(error, res, "Dashboard"); } }; } - diff --git a/backend/src/presentation/dashboard/routes.ts b/backend/src/presentation/dashboard/routes.ts index fa03e9a..84e227c 100644 --- a/backend/src/presentation/dashboard/routes.ts +++ b/backend/src/presentation/dashboard/routes.ts @@ -1,8 +1,9 @@ -import { Router } from 'express'; -import { jwtAdapter } from '../../config'; -import { AuthMiddleware } from '../middlewares/auth.middleware'; -import { DashboardServices } from '../services/dashboard.services'; -import { DashboardController } from './controller'; +import { Router } from "express"; + +import { jwtAdapter } from "../../config"; +import { AuthMiddleware } from "../middlewares/auth.middleware"; +import { DashboardServices } from "../services/dashboard.services"; +import { DashboardController } from "./controller"; export class DashboardRoutes { static get routes(): Router { @@ -107,11 +108,10 @@ export class DashboardRoutes { * 500: * description: Internal server error */ - router.get('/', authMiddleware.authenticate, (req, res) => + router.get("/", authMiddleware.authenticate, (req, res) => controller.getDashboard(req, res), ); return router; } } - diff --git a/backend/src/presentation/middleware.ts b/backend/src/presentation/middleware.ts index b28b04f..e69de29 100644 --- a/backend/src/presentation/middleware.ts +++ b/backend/src/presentation/middleware.ts @@ -1,3 +0,0 @@ - - - diff --git a/backend/src/presentation/middlewares/auth.middleware.ts b/backend/src/presentation/middlewares/auth.middleware.ts index 4ca9e35..47ffafc 100644 --- a/backend/src/presentation/middlewares/auth.middleware.ts +++ b/backend/src/presentation/middlewares/auth.middleware.ts @@ -1,197 +1,202 @@ -import { Request, Response, NextFunction } from 'express'; -import { JwtAdapter } from '../../domain'; -import { CustomError } from '../../domain'; -import { ProfileModel, RoleModel } from '../../data/postgres/models'; -import { RevokedTokenModel } from '../../data/postgres/models/revoked-token.model'; +import type { NextFunction, Request, Response } from "express"; + +import { ProfileModel, RoleModel } from "../../data/postgres/models"; +import { RevokedTokenModel } from "../../data/postgres/models/revoked-token.model"; +import { CustomError, type JwtAdapter } from "../../domain"; export class AuthMiddleware { - constructor( - private readonly jwtAdapter: JwtAdapter - ) {} - - authenticate = async (req: Request, res: Response, next: NextFunction) => { - try { - const authHeader = req.headers.authorization; - - if (!authHeader) { - return res.status(401).json({ - message: 'Authorization header is required' - }); - } - - const parts = authHeader.split(' '); - if (parts.length !== 2 || parts[0] !== 'Bearer') { - return res.status(401).json({ - message: 'Invalid authorization format. Expected: Bearer ' - }); - } - - const token = parts[1]; - - const payload = await this.jwtAdapter.validateToken(token); - - if (!payload) { - return res.status(401).json({ - message: 'Invalid or expired token' - }); - } - - if (payload.jti) { - const isRevoked = await RevokedTokenModel.isRevoked(payload.jti); - - if (isRevoked) { - return res.status(401).json({ - message: 'Token has been revoked. Please login again.' - }); - } - } - - req.user = payload; - - next(); - } catch (error) { - if (error instanceof CustomError) { - return res.status(error.statusCode).json({ - message: error.message - }); - } - - return res.status(401).json({ - message: 'Authentication failed' - }); - } - } - - requireAdmin = async (req: Request, res: Response, next: NextFunction) => { - try { - const user = req.user; - - if (!user || !user.id) { - return res.status(401).json({ - message: 'User not authenticated' - }); - } - - const userRole = user.role?.toLowerCase(); - - if (userRole !== 'admin') { - const userId = typeof user.id === 'string' ? parseInt(user.id, 10) : user.id; - const profile = await ProfileModel.findById(userId); - - if (!profile) { - return res.status(401).json({ - message: 'User not found' - }); - } - - const role = await RoleModel.findById(profile.role_id); - - if (!role || role.name.toLowerCase() !== 'admin') { - return res.status(403).json({ - message: 'Admin access required' - }); - } - - req.user = { - ...user, - role: role.name, - isAdmin: true - }; - } else { - req.user = { - ...user, - isAdmin: true - }; - } - - next(); - } catch (error) { - if (error instanceof CustomError) { - return res.status(error.statusCode).json({ - message: error.message - }); - } - - return res.status(500).json({ - message: 'Error verifying admin access' - }); - } - } - - requireAdminOrOwner = async (req: Request, res: Response, next: NextFunction) => { - try { - const user = req.user; - - if (!user || !user.id) { - return res.status(401).json({ - message: 'User not authenticated' - }); - } - - const userRole = user.role?.toLowerCase(); - let isAdmin = userRole === 'admin'; - - if (!userRole) { - const userId = typeof user.id === 'string' ? parseInt(user.id, 10) : user.id; - const profile = await ProfileModel.findById(userId); - - if (!profile) { - return res.status(401).json({ - message: 'User not found' - }); - } - - const role = await RoleModel.findById(profile.role_id); - isAdmin = !!(role && role.name.toLowerCase() === 'admin'); - - req.user = { - ...user, - role: role?.name || null - }; - } - - const resourceId = req.params.id; - if (!resourceId) { - return res.status(400).json({ - message: 'Resource ID is required' - }); - } - - const resourceIdNumber = parseInt(resourceId, 10); - if (isNaN(resourceIdNumber)) { - return res.status(400).json({ - message: 'Invalid resource ID' - }); - } - - const userId = typeof user.id === 'string' ? parseInt(user.id, 10) : user.id; - const isOwner = userId === resourceIdNumber; - - if (!isAdmin && !isOwner) { - return res.status(403).json({ - message: 'Access denied. You can only edit your own profile or must be an admin' - }); - } - - if (req.user) { - req.user = { - ...req.user, - isAdmin - }; - } - - next(); - } catch (error) { - if (error instanceof CustomError) { - return res.status(error.statusCode).json({ - message: error.message - }); - } - - return res.status(500).json({ - message: 'Error verifying access' - }); - } - } -} + constructor(private readonly jwtAdapter: JwtAdapter) {} + + authenticate = async (req: Request, res: Response, next: NextFunction) => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader) { + return res.status(401).json({ + message: "Authorization header is required", + }); + } + + const parts = authHeader.split(" "); + if (parts.length !== 2 || parts[0] !== "Bearer") { + return res.status(401).json({ + message: "Invalid authorization format. Expected: Bearer ", + }); + } + + const token = parts[1]; + + const payload = await this.jwtAdapter.validateToken(token); + + if (!payload) { + return res.status(401).json({ + message: "Invalid or expired token", + }); + } + + if (payload.jti) { + const isRevoked = await RevokedTokenModel.isRevoked(payload.jti); + + if (isRevoked) { + return res.status(401).json({ + message: "Token has been revoked. Please login again.", + }); + } + } + + req.user = payload; + + next(); + } catch (error) { + if (error instanceof CustomError) { + return res.status(error.statusCode).json({ + message: error.message, + }); + } + + return res.status(401).json({ + message: "Authentication failed", + }); + } + }; + + requireAdmin = async (req: Request, res: Response, next: NextFunction) => { + try { + const user = req.user; + + if (!user || !user.id) { + return res.status(401).json({ + message: "User not authenticated", + }); + } + + const userRole = user.role?.toLowerCase(); + + if (userRole !== "admin") { + const userId = + typeof user.id === "string" ? parseInt(user.id, 10) : user.id; + const profile = await ProfileModel.findById(userId); + + if (!profile) { + return res.status(401).json({ + message: "User not found", + }); + } + + const role = await RoleModel.findById(profile.role_id); + if (!role || role.name.toLowerCase() !== "admin") { + return res.status(403).json({ + message: "Admin access required", + }); + } + + req.user = { + ...user, + role: role.name, + isAdmin: true, + }; + } else { + req.user = { + ...user, + isAdmin: true, + }; + } + + next(); + } catch (error) { + if (error instanceof CustomError) { + return res.status(error.statusCode).json({ + message: error.message, + }); + } + + return res.status(500).json({ + message: "Error verifying admin access", + }); + } + }; + + requireAdminOrOwner = async ( + req: Request, + res: Response, + next: NextFunction, + ) => { + try { + const user = req.user; + + if (!user || !user.id) { + return res.status(401).json({ + message: "User not authenticated", + }); + } + + const userRole = user.role?.toLowerCase(); + let isAdmin = userRole === "admin"; + + if (!userRole) { + const userId = + typeof user.id === "string" ? parseInt(user.id, 10) : user.id; + const profile = await ProfileModel.findById(userId); + + if (!profile) { + return res.status(401).json({ + message: "User not found", + }); + } + + const role = await RoleModel.findById(profile.role_id); + isAdmin = !!(role && role.name.toLowerCase() === "admin"); + + req.user = { + ...user, + role: role?.name || null, + }; + } + + const resourceId = req.params.id; + if (!resourceId) { + return res.status(400).json({ + message: "Resource ID is required", + }); + } + + const resourceIdNumber = parseInt(resourceId, 10); + if (Number.isNaN(resourceIdNumber)) { + return res.status(400).json({ + message: "Invalid resource ID", + }); + } + + const userId = + typeof user.id === "string" ? parseInt(user.id, 10) : user.id; + const isOwner = userId === resourceIdNumber; + + if (!isAdmin && !isOwner) { + return res.status(403).json({ + message: + "Access denied. You can only edit your own profile or must be an admin", + }); + } + + if (req.user) { + req.user = { + ...req.user, + isAdmin, + }; + } + + next(); + } catch (error) { + if (error instanceof CustomError) { + return res.status(error.statusCode).json({ + message: error.message, + }); + } + + return res.status(500).json({ + message: "Error verifying access", + }); + } + }; +} diff --git a/backend/src/presentation/middlewares/cors.middleware.ts b/backend/src/presentation/middlewares/cors.middleware.ts index 0f7cd96..fd6c468 100644 --- a/backend/src/presentation/middlewares/cors.middleware.ts +++ b/backend/src/presentation/middlewares/cors.middleware.ts @@ -1,30 +1,32 @@ -import { Request, Response, NextFunction } from 'express'; +import type { NextFunction, Request, Response } from "express"; export class CorsMiddleware { - static configure(allowedOrigins: string[] = ['*']) { - return (req: Request, res: Response, next: NextFunction) => { - const origin = req.headers.origin; - - if (allowedOrigins.includes('*')) { - if (origin) { - res.header('Access-Control-Allow-Origin', origin); - res.header('Access-Control-Allow-Credentials', 'true'); - } else { - res.header('Access-Control-Allow-Origin', '*'); - } - } else if (origin && allowedOrigins.includes(origin)) { - res.header('Access-Control-Allow-Origin', origin); - res.header('Access-Control-Allow-Credentials', 'true'); - } - - res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - if (req.method === 'OPTIONS') { - return res.sendStatus(200); - } - - next(); - }; - } -} + static configure(allowedOrigins: string[] = ["*"]) { + return (req: Request, res: Response, next: NextFunction) => { + const origin = req.headers.origin; + + if (allowedOrigins.includes("*")) { + if (origin) { + res.header("Access-Control-Allow-Origin", origin); + res.header("Access-Control-Allow-Credentials", "true"); + } else { + res.header("Access-Control-Allow-Origin", "*"); + } + } else if (origin && allowedOrigins.includes(origin)) { + res.header("Access-Control-Allow-Origin", origin); + res.header("Access-Control-Allow-Credentials", "true"); + } + + res.header( + "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE, PATCH, OPTIONS", + ); + res.header("Access-Control-Allow-Headers", "Content-Type, Authorization"); + if (req.method === "OPTIONS") { + return res.sendStatus(200); + } + + next(); + }; + } +} diff --git a/backend/src/presentation/middlewares/error-handler.middleware.ts b/backend/src/presentation/middlewares/error-handler.middleware.ts index 5c79acd..93cd1a8 100644 --- a/backend/src/presentation/middlewares/error-handler.middleware.ts +++ b/backend/src/presentation/middlewares/error-handler.middleware.ts @@ -1,34 +1,33 @@ -import { Request, Response, NextFunction } from 'express'; -import { CustomError } from '../../domain'; +import type { NextFunction, Request, Response } from "express"; +import { CustomError } from "../../domain"; export class ErrorHandlerMiddleware { - static handle = ( - error: unknown, - req: Request, - res: Response, - next: NextFunction - ) => { - if (error instanceof CustomError) { - return res.status(error.statusCode).json({ - message: error.message, - statusCode: error.statusCode - }); - } - - if (error instanceof Error) { - console.error('Error:', error); - return res.status(500).json({ - message: error.message || 'Internal server error', - statusCode: 500 - }); - } - - console.error('Unknown error:', error); - return res.status(500).json({ - message: 'Internal server error', - statusCode: 500 - }); - } -} + static handle = ( + error: unknown, + _req: Request, + res: Response, + _next: NextFunction, + ) => { + if (error instanceof CustomError) { + return res.status(error.statusCode).json({ + message: error.message, + statusCode: error.statusCode, + }); + } + + if (error instanceof Error) { + console.error("Error:", error); + return res.status(500).json({ + message: error.message || "Internal server error", + statusCode: 500, + }); + } + console.error("Unknown error:", error); + return res.status(500).json({ + message: "Internal server error", + statusCode: 500, + }); + }; +} diff --git a/backend/src/presentation/middlewares/index.ts b/backend/src/presentation/middlewares/index.ts index e04cd84..1f47225 100644 --- a/backend/src/presentation/middlewares/index.ts +++ b/backend/src/presentation/middlewares/index.ts @@ -1,5 +1,4 @@ -export * from './auth.middleware'; -export * from './error-handler.middleware'; -export * from './cors.middleware'; -export * from './upload.middleware'; - +export * from "./auth.middleware"; +export * from "./cors.middleware"; +export * from "./error-handler.middleware"; +export * from "./upload.middleware"; diff --git a/backend/src/presentation/middlewares/upload.middleware.ts b/backend/src/presentation/middlewares/upload.middleware.ts index 763d12f..d347767 100644 --- a/backend/src/presentation/middlewares/upload.middleware.ts +++ b/backend/src/presentation/middlewares/upload.middleware.ts @@ -1,236 +1,251 @@ -import { Request, Response, NextFunction } from 'express'; -import multer from 'multer'; -import { CustomError } from '../../domain'; +import type { NextFunction, Request, Response } from "express"; +import multer from "multer"; export class UploadMiddleware { - private static storage = multer.memoryStorage(); - - private static imageFileFilter = ( - req: Request, - file: Express.Multer.File, - cb: multer.FileFilterCallback - ) => { - const allowedMimes = [ - 'image/jpeg', - 'image/jpg', - 'image/png', - 'image/gif', - 'image/webp' - ]; - - if (allowedMimes.includes(file.mimetype)) { - cb(null, true); - } else { - cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WEBP images are allowed.')); - } - }; - - private static documentFileFilter = ( - req: Request, - file: Express.Multer.File, - cb: multer.FileFilterCallback - ) => { - if (file.mimetype === 'application/pdf') { - cb(null, true); - } else { - cb(new Error('Invalid file type. Only PDF documents are allowed.')); - } - }; - - static single(fieldName: string = 'file', maxSize: number = 5 * 1024 * 1024) { - const upload = multer({ - storage: UploadMiddleware.storage, - fileFilter: UploadMiddleware.imageFileFilter, - limits: { - fileSize: maxSize - } - }); - - return (req: Request, res: Response, next: NextFunction) => { - upload.single(fieldName)(req, res, (err) => { - if (err) { - if (err instanceof multer.MulterError) { - if (err.code === 'LIMIT_FILE_SIZE') { - return res.status(400).json({ - message: `File size exceeds the maximum limit of ${maxSize / (1024 * 1024)}MB` - }); - } - return res.status(400).json({ - message: `Upload error: ${err.message}` - }); - } - return res.status(400).json({ - message: err.message || 'Error uploading file' - }); - } - next(); - }); - }; - } - - static multiple(fieldName: string = 'files', maxCount: number = 10, maxSize: number = 5 * 1024 * 1024) { - const upload = multer({ - storage: UploadMiddleware.storage, - fileFilter: UploadMiddleware.imageFileFilter, - limits: { - fileSize: maxSize, - files: maxCount - } - }); - - return (req: Request, res: Response, next: NextFunction) => { - upload.array(fieldName, maxCount)(req, res, (err) => { - if (err) { - if (err instanceof multer.MulterError) { - if (err.code === 'LIMIT_FILE_SIZE') { - return res.status(400).json({ - message: `File size exceeds the maximum limit of ${maxSize / (1024 * 1024)}MB` - }); - } - if (err.code === 'LIMIT_FILE_COUNT') { - return res.status(400).json({ - message: `Number of files exceeds the maximum limit of ${maxCount}` - }); - } - return res.status(400).json({ - message: `Upload error: ${err.message}` - }); - } - return res.status(400).json({ - message: err.message || 'Error uploading files' - }); - } - next(); - }); - }; - } - - static fields(fields: Array<{ name: string; maxCount?: number }>, maxSize: number = 5 * 1024 * 1024) { - const upload = multer({ - storage: UploadMiddleware.storage, - fileFilter: UploadMiddleware.imageFileFilter, - limits: { - fileSize: maxSize - } - }); - - return (req: Request, res: Response, next: NextFunction) => { - upload.fields(fields)(req, res, (err) => { - if (err) { - if (err instanceof multer.MulterError) { - if (err.code === 'LIMIT_FILE_SIZE') { - return res.status(400).json({ - message: `File size exceeds the maximum limit of ${maxSize / (1024 * 1024)}MB` - }); - } - return res.status(400).json({ - message: `Upload error: ${err.message}` - }); - } - return res.status(400).json({ - message: err.message || 'Error uploading files' - }); - } - next(); - }); - }; - } - - static documents(fieldName: string = 'documents', maxCount: number = 10, maxSize: number = 10 * 1024 * 1024) { - const upload = multer({ - storage: UploadMiddleware.storage, - fileFilter: UploadMiddleware.documentFileFilter, - limits: { - fileSize: maxSize, - files: maxCount - } - }); - - return (req: Request, res: Response, next: NextFunction) => { - upload.array(fieldName, maxCount)(req, res, (err) => { - if (err) { - if (err instanceof multer.MulterError) { - if (err.code === 'LIMIT_FILE_SIZE') { - return res.status(400).json({ - message: `File size exceeds the maximum limit of ${maxSize / (1024 * 1024)}MB` - }); - } - if (err.code === 'LIMIT_FILE_COUNT') { - return res.status(400).json({ - message: `Number of files exceeds the maximum limit of ${maxCount}` - }); - } - return res.status(400).json({ - message: `Upload error: ${err.message}` - }); - } - return res.status(400).json({ - message: err.message || 'Error uploading documents' - }); - } - next(); - }); - }; - } - - static imagesAndDocuments(options: { - imageField?: string; - documentField?: string; - maxImages?: number; - maxDocuments?: number; - maxImageSize?: number; - maxDocumentSize?: number; - } = {}) { - const { - imageField = 'images', - documentField = 'documents', - maxImages = 10, - maxDocuments = 10, - maxImageSize = 5 * 1024 * 1024, - maxDocumentSize = 10 * 1024 * 1024 - } = options; - - const upload = multer({ - storage: UploadMiddleware.storage, - fileFilter: (req, file, cb) => { - if (file.fieldname === imageField) { - UploadMiddleware.imageFileFilter(req, file, cb); - } else if (file.fieldname === documentField) { - UploadMiddleware.documentFileFilter(req, file, cb); - } else { - cb(new Error(`Unknown field: ${file.fieldname}`)); - } - }, - limits: { - fileSize: Math.max(maxImageSize, maxDocumentSize) - } - }); - - return (req: Request, res: Response, next: NextFunction) => { - const fields = [ - { name: imageField, maxCount: maxImages }, - { name: documentField, maxCount: maxDocuments } - ]; - - upload.fields(fields)(req, res, (err) => { - if (err) { - if (err instanceof multer.MulterError) { - if (err.code === 'LIMIT_FILE_SIZE') { - return res.status(400).json({ - message: `File size exceeds the maximum limit` - }); - } - return res.status(400).json({ - message: `Upload error: ${err.message}` - }); - } - return res.status(400).json({ - message: err.message || 'Error uploading files' - }); - } - next(); - }); - }; - } + private static storage = multer.memoryStorage(); + + private static imageFileFilter = ( + _req: Request, + file: Express.Multer.File, + cb: multer.FileFilterCallback, + ) => { + const allowedMimes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp", + ]; + + if (allowedMimes.includes(file.mimetype)) { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type. Only JPEG, PNG, GIF, and WEBP images are allowed.", + ), + ); + } + }; + + private static documentFileFilter = ( + _req: Request, + file: Express.Multer.File, + cb: multer.FileFilterCallback, + ) => { + if (file.mimetype === "application/pdf") { + cb(null, true); + } else { + cb(new Error("Invalid file type. Only PDF documents are allowed.")); + } + }; + + static single(fieldName: string = "file", maxSize: number = 5 * 1024 * 1024) { + const upload = multer({ + storage: UploadMiddleware.storage, + fileFilter: UploadMiddleware.imageFileFilter, + limits: { + fileSize: maxSize, + }, + }); + + return (req: Request, res: Response, next: NextFunction) => { + upload.single(fieldName)(req, res, (err) => { + if (err) { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + return res.status(400).json({ + message: `File size exceeds the maximum limit of ${maxSize / (1024 * 1024)}MB`, + }); + } + return res.status(400).json({ + message: `Upload error: ${err.message}`, + }); + } + return res.status(400).json({ + message: err.message || "Error uploading file", + }); + } + next(); + }); + }; + } + + static multiple( + fieldName: string = "files", + maxCount: number = 10, + maxSize: number = 5 * 1024 * 1024, + ) { + const upload = multer({ + storage: UploadMiddleware.storage, + fileFilter: UploadMiddleware.imageFileFilter, + limits: { + fileSize: maxSize, + files: maxCount, + }, + }); + + return (req: Request, res: Response, next: NextFunction) => { + upload.array(fieldName, maxCount)(req, res, (err) => { + if (err) { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + return res.status(400).json({ + message: `File size exceeds the maximum limit of ${maxSize / (1024 * 1024)}MB`, + }); + } + if (err.code === "LIMIT_FILE_COUNT") { + return res.status(400).json({ + message: `Number of files exceeds the maximum limit of ${maxCount}`, + }); + } + return res.status(400).json({ + message: `Upload error: ${err.message}`, + }); + } + return res.status(400).json({ + message: err.message || "Error uploading files", + }); + } + next(); + }); + }; + } + + static fields( + fields: Array<{ name: string; maxCount?: number }>, + maxSize: number = 5 * 1024 * 1024, + ) { + const upload = multer({ + storage: UploadMiddleware.storage, + fileFilter: UploadMiddleware.imageFileFilter, + limits: { + fileSize: maxSize, + }, + }); + + return (req: Request, res: Response, next: NextFunction) => { + upload.fields(fields)(req, res, (err) => { + if (err) { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + return res.status(400).json({ + message: `File size exceeds the maximum limit of ${maxSize / (1024 * 1024)}MB`, + }); + } + return res.status(400).json({ + message: `Upload error: ${err.message}`, + }); + } + return res.status(400).json({ + message: err.message || "Error uploading files", + }); + } + next(); + }); + }; + } + + static documents( + fieldName: string = "documents", + maxCount: number = 10, + maxSize: number = 10 * 1024 * 1024, + ) { + const upload = multer({ + storage: UploadMiddleware.storage, + fileFilter: UploadMiddleware.documentFileFilter, + limits: { + fileSize: maxSize, + files: maxCount, + }, + }); + + return (req: Request, res: Response, next: NextFunction) => { + upload.array(fieldName, maxCount)(req, res, (err) => { + if (err) { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + return res.status(400).json({ + message: `File size exceeds the maximum limit of ${maxSize / (1024 * 1024)}MB`, + }); + } + if (err.code === "LIMIT_FILE_COUNT") { + return res.status(400).json({ + message: `Number of files exceeds the maximum limit of ${maxCount}`, + }); + } + return res.status(400).json({ + message: `Upload error: ${err.message}`, + }); + } + return res.status(400).json({ + message: err.message || "Error uploading documents", + }); + } + next(); + }); + }; + } + + static imagesAndDocuments( + options: { + imageField?: string; + documentField?: string; + maxImages?: number; + maxDocuments?: number; + maxImageSize?: number; + maxDocumentSize?: number; + } = {}, + ) { + const { + imageField = "images", + documentField = "documents", + maxImages = 10, + maxDocuments = 10, + maxImageSize = 5 * 1024 * 1024, + maxDocumentSize = 10 * 1024 * 1024, + } = options; + + const upload = multer({ + storage: UploadMiddleware.storage, + fileFilter: (req, file, cb) => { + if (file.fieldname === imageField) { + UploadMiddleware.imageFileFilter(req, file, cb); + } else if (file.fieldname === documentField) { + UploadMiddleware.documentFileFilter(req, file, cb); + } else { + cb(new Error(`Unknown field: ${file.fieldname}`)); + } + }, + limits: { + fileSize: Math.max(maxImageSize, maxDocumentSize), + }, + }); + + return (req: Request, res: Response, next: NextFunction) => { + const fields = [ + { name: imageField, maxCount: maxImages }, + { name: documentField, maxCount: maxDocuments }, + ]; + + upload.fields(fields)(req, res, (err) => { + if (err) { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + return res.status(400).json({ + message: `File size exceeds the maximum limit`, + }); + } + return res.status(400).json({ + message: `Upload error: ${err.message}`, + }); + } + return res.status(400).json({ + message: err.message || "Error uploading files", + }); + } + next(); + }); + }; + } } - diff --git a/backend/src/presentation/properties/controller.ts b/backend/src/presentation/properties/controller.ts index f398462..79faeb6 100644 --- a/backend/src/presentation/properties/controller.ts +++ b/backend/src/presentation/properties/controller.ts @@ -3,8 +3,8 @@ import type { Request, Response } from "express"; import { CreatePropertyDto, CreatePropertyGroupedDto, - UpdatePropertyGroupedDto, CustomError, + UpdatePropertyGroupedDto, } from "../../domain"; import type { PropertyServices } from "../services/property.services"; @@ -33,7 +33,7 @@ export class PropertyController { } const capturedByUserId = parseInt(user.id, 10); - if (isNaN(capturedByUserId)) { + if (Number.isNaN(capturedByUserId)) { return res.status(400).json({ message: "Invalid user ID", }); @@ -86,7 +86,7 @@ export class PropertyController { const { id } = req.params; const includeArchived = req.query.includeArchived === "true"; - if (!id || isNaN(Number(id))) { + if (!id || Number.isNaN(Number(id))) { return res.status(400).json({ message: "Invalid property ID", }); @@ -156,7 +156,7 @@ export class PropertyController { try { const { id } = req.params; - if (!id || isNaN(Number(id))) { + if (!id || Number.isNaN(Number(id))) { return res.status(400).json({ message: "Invalid property ID", }); @@ -166,7 +166,7 @@ export class PropertyController { if (typeof req.body.propertyDetails === "string") { try { updateData = JSON.parse(req.body.propertyDetails); - } catch (error) { + } catch { return res.status(400).json({ message: "Invalid propertyDetails JSON format", }); @@ -190,7 +190,7 @@ export class PropertyController { try { const { id } = req.params; - if (!id || isNaN(Number(id))) { + if (!id || Number.isNaN(Number(id))) { return res.status(400).json({ message: "Invalid property ID", }); @@ -207,7 +207,7 @@ export class PropertyController { try { const { id } = req.params; - if (!id || isNaN(Number(id))) { + if (!id || Number.isNaN(Number(id))) { return res.status(400).json({ message: "Invalid property ID", }); @@ -252,45 +252,54 @@ export class PropertyController { } else if (Array.isArray(req.body.documentNames)) { documentNames = req.body.documentNames; } - } catch (error) { + } catch { console.warn("Could not parse documentNames, using defaults"); } } const bodyWithArchive: Record = { ...req.body }; - let basicData: Record = { visibility_status: "Archivada" }; + let basicData: Record = { + visibility_status: "Archivada", + }; if (bodyWithArchive.basic) { try { - const parsedBasic = typeof bodyWithArchive.basic === 'string' - ? JSON.parse(bodyWithArchive.basic) - : bodyWithArchive.basic; + const parsedBasic = + typeof bodyWithArchive.basic === "string" + ? JSON.parse(bodyWithArchive.basic) + : bodyWithArchive.basic; basicData = { ...parsedBasic, visibility_status: "Archivada" }; - } catch (error) { + } catch { basicData = { visibility_status: "Archivada" }; } } bodyWithArchive.basic = JSON.stringify(basicData); - const [error, initialDto] = UpdatePropertyGroupedDto.create( - bodyWithArchive, - ); - + const [error, initialDto] = + UpdatePropertyGroupedDto.create(bodyWithArchive); + let updatePropertyGroupedDto = initialDto; if (error || !initialDto) { - if (!req.body.basic && !req.body.geography && !req.body.address && - !req.body.values && !req.body.characteristics && !req.body.surface && - !req.body.services && !req.body.internal) { + if ( + !req.body.basic && + !req.body.geography && + !req.body.address && + !req.body.values && + !req.body.characteristics && + !req.body.surface && + !req.body.services && + !req.body.internal + ) { const [archiveError, archiveDto] = UpdatePropertyGroupedDto.create({ - basic: JSON.stringify({ visibility_status: "Archivada" }) + basic: JSON.stringify({ visibility_status: "Archivada" }), }); - + if (archiveError || !archiveDto) { return res.status(400).json({ message: "Failed to create archive DTO", }); } - + const result = await this.propertyServices.updatePropertyGrouped( Number(id), archiveDto, @@ -304,7 +313,7 @@ export class PropertyController { data: result, }); } - + return res.status(400).json({ message: error || "Invalid property update data", }); @@ -316,7 +325,10 @@ export class PropertyController { }); } - if (!updatePropertyGroupedDto.basic || Object.keys(updatePropertyGroupedDto.basic).length === 0) { + if ( + !updatePropertyGroupedDto.basic || + Object.keys(updatePropertyGroupedDto.basic).length === 0 + ) { const newDto = new UpdatePropertyGroupedDto( { visibility_status: "Archivada" }, updatePropertyGroupedDto.geography, @@ -329,9 +341,9 @@ export class PropertyController { ); updatePropertyGroupedDto = newDto; } else { - const mergedBasic = { - ...updatePropertyGroupedDto.basic, - visibility_status: "Archivada" + const mergedBasic = { + ...updatePropertyGroupedDto.basic, + visibility_status: "Archivada", }; const newDto = new UpdatePropertyGroupedDto( mergedBasic, @@ -368,7 +380,7 @@ export class PropertyController { const { id } = req.params; const { visibility_status_id } = req.body; - if (!id || isNaN(Number(id))) { + if (!id || Number.isNaN(Number(id))) { return res.status(400).json({ message: "Invalid property ID", }); @@ -389,7 +401,7 @@ export class PropertyController { const { id } = req.params; const { featured_web } = req.body; - if (!id || isNaN(Number(id))) { + if (!id || Number.isNaN(Number(id))) { return res.status(400).json({ message: "Invalid property ID", }); @@ -419,7 +431,7 @@ export class PropertyController { try { const { id } = req.params; - if (!id || isNaN(Number(id))) { + if (!id || Number.isNaN(Number(id))) { return res.status(400).json({ message: "Invalid property ID", }); @@ -432,7 +444,6 @@ export class PropertyController { } }; - createPropertyGrouped = async (req: Request, res: Response) => { try { const user = req.user; @@ -443,7 +454,7 @@ export class PropertyController { } const capturedByUserId = parseInt(user.id, 10); - if (isNaN(capturedByUserId)) { + if (Number.isNaN(capturedByUserId)) { return res.status(400).json({ message: "Invalid user ID", }); @@ -499,20 +510,25 @@ export class PropertyController { } else if (Array.isArray(req.body.documentNames)) { documentNames = req.body.documentNames; } - } catch (error) { + } catch { console.warn("Could not parse documentNames, using defaults"); } } - console.log('[PropertyController] req.body.basic (raw):', req.body.basic); - console.log('[PropertyController] req.body.basic (raw):', req.body.basic); - if (req.body.basic && typeof req.body.basic === 'string') { + console.log("[PropertyController] req.body.basic (raw):", req.body.basic); + console.log("[PropertyController] req.body.basic (raw):", req.body.basic); + if (req.body.basic && typeof req.body.basic === "string") { try { const parsed = JSON.parse(req.body.basic); - console.log('[PropertyController] req.body.basic (parsed):', parsed); - console.log('[PropertyController] parsed.owner_id:', parsed.owner_id, 'type:', typeof parsed.owner_id); + console.log("[PropertyController] req.body.basic (parsed):", parsed); + console.log( + "[PropertyController] parsed.owner_id:", + parsed.owner_id, + "type:", + typeof parsed.owner_id, + ); } catch (e) { - console.error('[PropertyController] Error parsing basic JSON:', e); + console.error("[PropertyController] Error parsing basic JSON:", e); } } @@ -547,7 +563,7 @@ export class PropertyController { try { const { id } = req.params; - if (!id || isNaN(Number(id))) { + if (!id || Number.isNaN(Number(id))) { return res.status(400).json({ message: "Invalid property ID", }); @@ -592,7 +608,7 @@ export class PropertyController { } else if (Array.isArray(req.body.documentNames)) { documentNames = req.body.documentNames; } - } catch (error) { + } catch { console.warn("Could not parse documentNames, using defaults"); } } @@ -607,17 +623,17 @@ export class PropertyController { }); } - const user = (req as any).user; + const user = (req as unknown as { user?: { id: string } }).user; const userId = user?.id ? parseInt(user.id, 10) : undefined; const result = await this.propertyServices.updatePropertyGrouped( - Number(id), - updatePropertyGroupedDto, - images.length > 0 ? images : undefined, - documents.length > 0 ? documents : undefined, - documentNames.length > 0 ? documentNames : undefined, - userId, - ); + Number(id), + updatePropertyGroupedDto, + images.length > 0 ? images : undefined, + documents.length > 0 ? documents : undefined, + documentNames.length > 0 ? documentNames : undefined, + userId, + ); return res.status(200).json({ message: "Property updated successfully", @@ -631,7 +647,7 @@ export class PropertyController { getDocumentDownloadUrl = async (req: Request, res: Response) => { try { const { documentId } = req.params; - if (!documentId || isNaN(Number(documentId))) { + if (!documentId || Number.isNaN(Number(documentId))) { return res.status(400).json({ message: "Invalid document ID", }); diff --git a/backend/src/presentation/properties/routes.ts b/backend/src/presentation/properties/routes.ts index 909222a..714f2b1 100644 --- a/backend/src/presentation/properties/routes.ts +++ b/backend/src/presentation/properties/routes.ts @@ -179,7 +179,7 @@ export class PropertyRoutes { router.post( "/", authMiddleware.authenticate, - UploadMiddleware.multiple("images", 10, 5 * 1024 * 1024), + UploadMiddleware.multiple("images", 10, 5 * 1024 * 1024), (req, res) => controller.createProperty(req, res), ); @@ -380,8 +380,8 @@ export class PropertyRoutes { documentField: "documents", maxImages: 10, maxDocuments: 10, - maxImageSize: 5 * 1024 * 1024, - maxDocumentSize: 10 * 1024 * 1024, + maxImageSize: 5 * 1024 * 1024, + maxDocumentSize: 10 * 1024 * 1024, }), (req, res) => controller.createPropertyGrouped(req, res), ); @@ -515,8 +515,8 @@ export class PropertyRoutes { documentField: "documents", maxImages: 10, maxDocuments: 10, - maxImageSize: 5 * 1024 * 1024, - maxDocumentSize: 10 * 1024 * 1024, + maxImageSize: 5 * 1024 * 1024, + maxDocumentSize: 10 * 1024 * 1024, }), (req, res) => controller.archivePropertyGrouped(req, res), ); @@ -673,8 +673,8 @@ export class PropertyRoutes { documentField: "documents", maxImages: 10, maxDocuments: 10, - maxImageSize: 5 * 1024 * 1024, - maxDocumentSize: 10 * 1024 * 1024, + maxImageSize: 5 * 1024 * 1024, + maxDocumentSize: 10 * 1024 * 1024, }), (req, res) => controller.updatePropertyGrouped(req, res), ); @@ -705,81 +705,81 @@ export class PropertyRoutes { controller.deleteProperty(req, res), ); - /** - * @swagger - * /api/properties/documents/{documentId}/download-url: - * get: - * summary: Get document download URL - * description: | - * Returns document information with a Cloudinary URL that forces download. - * Uses the fl_attachment transformation to add Content-Disposition header. - * **Requires authentication.** - * tags: [Properties] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: documentId - * required: true - * schema: - * type: integer - * description: Document ID - * example: 1 - * responses: - * 200: - * description: Document information with download URL - * content: - * application/json: - * schema: - * type: object - * properties: - * document: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * document_name: - * type: string - * example: Escritura - * file_path: - * type: string - * description: Original Cloudinary URL (for preview) - * example: https://res.cloudinary.com/demo/raw/upload/v1/sample.pdf - * download_url: - * type: string - * description: Cloudinary URL that forces download - * example: https://res.cloudinary.com/demo/raw/upload/fl_attachment/v1/sample.pdf - * uploaded_at: - * type: string - * format: date-time - * example: 2024-12-10T20:00:00.000Z - * 400: - * description: Invalid document ID - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Invalid document ID - * 404: - * description: Document not found - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Document not found - */ - router.get( - "/documents/:documentId/download-url", - authMiddleware.authenticate, - (req, res) => controller.getDocumentDownloadUrl(req, res), - ); + /** + * @swagger + * /api/properties/documents/{documentId}/download-url: + * get: + * summary: Get document download URL + * description: | + * Returns document information with a Cloudinary URL that forces download. + * Uses the fl_attachment transformation to add Content-Disposition header. + * **Requires authentication.** + * tags: [Properties] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: documentId + * required: true + * schema: + * type: integer + * description: Document ID + * example: 1 + * responses: + * 200: + * description: Document information with download URL + * content: + * application/json: + * schema: + * type: object + * properties: + * document: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * document_name: + * type: string + * example: Escritura + * file_path: + * type: string + * description: Original Cloudinary URL (for preview) + * example: https://res.cloudinary.com/demo/raw/upload/v1/sample.pdf + * download_url: + * type: string + * description: Cloudinary URL that forces download + * example: https://res.cloudinary.com/demo/raw/upload/fl_attachment/v1/sample.pdf + * uploaded_at: + * type: string + * format: date-time + * example: 2024-12-10T20:00:00.000Z + * 400: + * description: Invalid document ID + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Invalid document ID + * 404: + * description: Document not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Document not found + */ + router.get( + "/documents/:documentId/download-url", + authMiddleware.authenticate, + (req, res) => controller.getDocumentDownloadUrl(req, res), + ); return router; } diff --git a/backend/src/presentation/routes.ts b/backend/src/presentation/routes.ts index 2a987b0..69c04ef 100644 --- a/backend/src/presentation/routes.ts +++ b/backend/src/presentation/routes.ts @@ -27,4 +27,3 @@ export class AppRoutes { return router; } } - diff --git a/backend/src/presentation/server.ts b/backend/src/presentation/server.ts index 6220eea..d8f5d43 100644 --- a/backend/src/presentation/server.ts +++ b/backend/src/presentation/server.ts @@ -1,8 +1,8 @@ import express, { type Router } from "express"; -import type { Server as HttpServer } from "http"; -import path from "path"; import { PostgresDatabase } from "../data/postgres/database"; +import type { Server as HttpServer } from "node:http"; +import path from "node:path"; interface Options { port: number; @@ -25,18 +25,16 @@ export class Server { } async start() { - this.app.use(express.json()); + this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); - this.app.disable("x-powered-by"); + this.app.disable("x-powered-by"); const { CorsMiddleware } = await import("./middlewares/cors.middleware"); - this.app.use(CorsMiddleware.configure(["*"])); + this.app.use(CorsMiddleware.configure(["*"])); this.app.use(express.static(this.publicPath)); - const { swaggerSpec, generateSwaggerSpec } = await import( - "../config/swagger" - ); + const { generateSwaggerSpec } = await import("../config/swagger"); const swaggerUi = await import("swagger-ui-express"); const swaggerUiOptions = { @@ -45,9 +43,9 @@ export class Server { swaggerOptions: { persistAuthorization: true, displayRequestDuration: true, - filter: true, + filter: true, tryItOutEnabled: true, - docExpansion: "list", + docExpansion: "list", }, }; @@ -63,7 +61,7 @@ export class Server { this.app.use("/docs", swaggerUi.serve, swaggerSetup); this.app.use("/api-docs", swaggerUi.serve, swaggerSetup); - this.app.get("/health", (req, res) => { + this.app.get("/health", (_req, res) => { res.status(200).json({ status: "ok", timestamp: new Date().toISOString(), @@ -88,7 +86,7 @@ export class Server { return next(); } const indexPath = path.join( - __dirname + `../../../${this.publicPath}/index.html`, + `${__dirname}../../../${this.publicPath}/index.html`, ); res.sendFile(indexPath); }); diff --git a/backend/src/presentation/services/auth.services.ts b/backend/src/presentation/services/auth.services.ts index 75deef9..edf8eb2 100644 --- a/backend/src/presentation/services/auth.services.ts +++ b/backend/src/presentation/services/auth.services.ts @@ -1,128 +1,145 @@ import { ProfileModel, RoleModel } from "../../data/postgres/models"; -import { CustomError, JwtAdapter, LoginProfileDto, ProfileEntity, RegisterProfileDto } from "../../domain"; -import { HashAdapter } from "../../domain/interfaces/hash.adapter"; +import { + CustomError, + type JwtAdapter, + type LoginProfileDto, + ProfileEntity, + type RegisterProfileDto, +} from "../../domain"; +import type { HashAdapter } from "../../domain/interfaces/hash.adapter"; +export class AuthServices { + constructor( + private readonly hashAdapter: HashAdapter, + private readonly jwtAdapter: JwtAdapter, + ) {} + public async registerProfile(registerProfileDto: RegisterProfileDto) { + const existProfile = await ProfileModel.findByEmail( + registerProfileDto.email, + ); + if (existProfile) { + throw CustomError.badRequest("Profile already exists"); + } -export class AuthServices { + try { + const hashedPassword = await this.hashAdapter.hash( + registerProfileDto.password, + ); + + let roleId: number = registerProfileDto.role_id || 0; + if (!roleId) { + const defaultRole = await RoleModel.findByName("agent"); + if (!defaultRole || !defaultRole.id) { + const adminRole = await RoleModel.findByName("admin"); + if (!adminRole || !adminRole.id) { + throw CustomError.internalServerError( + 'Default role "agent" or "admin" not found. Run: npm run db:seed-roles', + ); + } + roleId = adminRole.id as number; + } else { + roleId = defaultRole.id as number; + } + } + + const profile = await ProfileModel.create({ + first_name: registerProfileDto.first_name, + last_name: registerProfileDto.last_name, + email: registerProfileDto.email, + password_hash: hashedPassword, + phone: registerProfileDto.phone, + role_id: roleId, + }); + + const profileEntity = ProfileEntity.fromObject(profile); + const role = await RoleModel.findById(profileEntity.role_id); + + const token = await this.jwtAdapter.generateToken({ + id: profileEntity.id.toString(), + email: profileEntity.email, + role: role?.name || "agent", + }); + + const publicUser = profileEntity.toPublicObject(); + return { + user: { + ...publicUser, + role: role?.name || null, + role_id: undefined, + }, + token, + }; + } catch (error) { + throw CustomError.internalServerError( + `Error registering profile: ${error}`, + ); + } + } + + public async loginProfile(loginProfileDto: LoginProfileDto) { + const profile = await ProfileModel.findByEmailWithPassword( + loginProfileDto.email, + ); + if (!profile) { + throw CustomError.badRequest("Invalid credentials"); + } + + const isMatch = await this.hashAdapter.compare( + loginProfileDto.password, + profile.password_hash, + ); + if (!isMatch) { + throw CustomError.badRequest("Invalid credentials"); + } + + if (!profile.active) { + throw CustomError.forbidden( + "User account is disabled. Please contact the administrator.", + ); + } + + const profileEntity = ProfileEntity.fromObject(profile); + const role = await RoleModel.findById(profileEntity.role_id); + + const token = await this.jwtAdapter.generateToken({ + id: profileEntity.id.toString(), + email: profileEntity.email, + role: role?.name || "agent", + }); + + const publicUser = profileEntity.toPublicObject(); + return { + user: { + ...publicUser, + role: role?.name || null, + role_id: undefined, + }, + token, + }; + } + + public async getProfile(userId: string | number) { + try { + const profile = await ProfileModel.findById(userId); + if (!profile) { + throw CustomError.notFound("Profile not found"); + } + + const profileEntity = ProfileEntity.fromObject(profile); + const role = await RoleModel.findById(profileEntity.role_id); - constructor( - private readonly hashAdapter: HashAdapter, - private readonly jwtAdapter: JwtAdapter - ){} - - public async registerProfile(registerProfileDto: RegisterProfileDto) { - const existProfile = await ProfileModel.findByEmail(registerProfileDto.email); - if (existProfile) { - throw CustomError.badRequest('Profile already exists'); - } - - try { - const hashedPassword = await this.hashAdapter.hash(registerProfileDto.password); - - let roleId: number = registerProfileDto.role_id || 0; - if (!roleId) { - const defaultRole = await RoleModel.findByName('agent'); - if (!defaultRole || !defaultRole.id) { - const adminRole = await RoleModel.findByName('admin'); - if (!adminRole || !adminRole.id) { - throw CustomError.internalServerError('Default role "agent" or "admin" not found. Run: npm run db:seed-roles'); - } - roleId = adminRole.id as number; - } else { - roleId = defaultRole.id as number; - } - } - - const profile = await ProfileModel.create({ - first_name: registerProfileDto.first_name, - last_name: registerProfileDto.last_name, - email: registerProfileDto.email, - password_hash: hashedPassword, - phone: registerProfileDto.phone, - role_id: roleId - }); - - const profileEntity = ProfileEntity.fromObject(profile); - const role = await RoleModel.findById(profileEntity.role_id); - - const token = await this.jwtAdapter.generateToken({ - id: profileEntity.id.toString(), - email: profileEntity.email, - role: role?.name || 'agent' - }); - - const publicUser = profileEntity.toPublicObject(); - return { - user: { - ...publicUser, - role: role?.name || null, - role_id: undefined - }, - token - }; - } catch (error) { - throw CustomError.internalServerError(`Error registering profile: ${error}`); - } - - } - - public async loginProfile(loginProfileDto: LoginProfileDto) { - const profile = await ProfileModel.findByEmailWithPassword(loginProfileDto.email); - if (!profile) { - throw CustomError.badRequest('Invalid credentials'); - } - - const isMatch = await this.hashAdapter.compare(loginProfileDto.password, profile.password_hash); - if (!isMatch) { - throw CustomError.badRequest('Invalid credentials'); - } - - if (!profile.active) { - throw CustomError.forbidden('User account is disabled. Please contact the administrator.'); - } - - const profileEntity = ProfileEntity.fromObject(profile); - const role = await RoleModel.findById(profileEntity.role_id); - - const token = await this.jwtAdapter.generateToken({ - id: profileEntity.id.toString(), - email: profileEntity.email, - role: role?.name || 'agent' - }); - - const publicUser = profileEntity.toPublicObject(); - return { - user: { - ...publicUser, - role: role?.name || null, - role_id: undefined - }, - token - }; - } - - public async getProfile(userId: string | number) { - try { - const profile = await ProfileModel.findById(userId); - if (!profile) { - throw CustomError.notFound('Profile not found'); - } - - const profileEntity = ProfileEntity.fromObject(profile); - const role = await RoleModel.findById(profileEntity.role_id); - - const publicUser = profileEntity.toPublicObject(); - return { - ...publicUser, - role: role?.name || null, - role_id: undefined - }; - } catch (error) { - if (error instanceof CustomError) { - throw error; - } - throw CustomError.internalServerError(`Error getting profile: ${error}`); - } - } -} \ No newline at end of file + const publicUser = profileEntity.toPublicObject(); + return { + ...publicUser, + role: role?.name || null, + role_id: undefined, + }; + } catch (error) { + if (error instanceof CustomError) { + throw error; + } + throw CustomError.internalServerError(`Error getting profile: ${error}`); + } + } +} diff --git a/backend/src/presentation/services/client.services.ts b/backend/src/presentation/services/client.services.ts index 00209de..8307ddd 100644 --- a/backend/src/presentation/services/client.services.ts +++ b/backend/src/presentation/services/client.services.ts @@ -1,735 +1,926 @@ -import { - ClientModel, - ClientPropertyInterestModel, - ContactCategoryModel, - PropertyModel, - CityModel, - ProvinceModel, - CountryModel -} from '../../data/postgres/models'; -import { UpdateClientDto } from '../../domain/dtos/clients'; -import { CustomError, ClientEntity, hasValueChanged, normalizePhone } from '../../domain'; -import type { PropertyRow, RentalInfoRow, ConsultationRow, PropertyPriceRow } from '../../domain/interfaces/database-rows'; +import { + CityModel, + ClientModel, + ClientPropertyInterestModel, + ContactCategoryModel, + CountryModel, + PropertyModel, + ProvinceModel, +} from "../../data/postgres/models"; +import { + ClientEntity, + CustomError, + hasValueChanged, + normalizePhone, +} from "../../domain"; +import type { UpdateClientDto } from "../../domain/dtos/clients"; +import type { PropertyRow } from "../../domain/interfaces/database-rows"; import type { - EnrichedConsultation, - PropertyDetails, - CurrencyInfo, - OperationTypeInfo, - PropertyOfInterest, - OwnedPropertyWithDetails, - RentedPropertyWithDetails -} from '../../domain/interfaces/enriched-data'; - + CurrencyInfo, + EnrichedConsultation, + OperationTypeInfo, + OwnedPropertyWithDetails, + PropertyDetails, + PropertyOfInterest, + RentedPropertyWithDetails, +} from "../../domain/interfaces/enriched-data"; export class ClientServices { - - - async listClients(filters?: { - contact_category_id?: number; - purchase_interest?: boolean; - rental_interest?: boolean; - city_id?: number; - limit?: number; - offset?: number; - search?: string; - includeDeleted?: boolean; - }) { - let clients = await ClientModel.findAll(filters); - - if (filters?.search) { - const searchLower = filters.search.toLowerCase(); - clients = clients.filter(client => - client.first_name?.toLowerCase().includes(searchLower) || - client.last_name?.toLowerCase().includes(searchLower) || - client.email?.toLowerCase().includes(searchLower) || - client.phone?.includes(searchLower) || - client.dni?.includes(searchLower) - ); - } - - const { ContactCategoryModel } = await import('../../data/postgres/models/clients/contact-category.model'); - const enrichedClients = await Promise.all( - clients.map(async (client) => { - const clientEntity = ClientEntity.fromDatabaseObject(client); - const category = await ContactCategoryModel.findById(clientEntity.contact_category_id); - - const categoryName = category?.name || null; - const properties = await this.enrichClientWithProperties(clientEntity.id, categoryName); - - return { - ...clientEntity.toPublicObject(), - contact_category: category ? { - id: category.id, - name: category.name - } : null, - ...properties - }; - }) - ); - - return { clients: enrichedClients, count: enrichedClients.length }; - } - - async getClientById(id: number) { - const client = await ClientModel.findById(id); - if (!client) { - throw CustomError.notFound(`Client with ID ${id} not found`); - } - - const clientEntity = ClientEntity.fromDatabaseObject(client); - - const { ContactCategoryModel } = await import('../../data/postgres/models/clients/contact-category.model'); - const category = await ContactCategoryModel.findById(clientEntity.contact_category_id); - - let propertySearchType = null; - if (clientEntity.property_search_type_id) { - const { PropertySearchTypeModel } = await import('../../data/postgres/models/clients/property-search-type.model'); - propertySearchType = await PropertySearchTypeModel.findById(clientEntity.property_search_type_id); - } - - let city = null; - if (clientEntity.city_id) { - city = await CityModel.findById(clientEntity.city_id); - if (city) { - const province = await ProvinceModel.findById(city.province_id); - if (province) { - const country = await CountryModel.findById(province.country_id); - - interface CityWithProvince { - id: number | undefined; - name: string; - province_id: number; - province?: { - id: number | undefined; - name: string; - country: { id: number | undefined; name: string } | null; - } | null; - } - const cityWithProvince = city as CityWithProvince; - cityWithProvince.province = province ? { - id: province.id, - name: province.name, - country: country ? { - id: country.id, - name: country.name - } : null - } : null; - } - } - } - - const categoryName = category?.name; - const properties = await this.enrichClientWithProperties(clientEntity.id, categoryName || null); - - let consultations: EnrichedConsultation[] = []; - if (categoryName === 'Lead') { - consultations = await this.getClientConsultations(clientEntity.id); - } - - return { - client: { - ...clientEntity.toPublicObject(), - contact_category: category ? { - id: category.id, - name: category.name - } : null, - property_search_type: propertySearchType ? { - id: propertySearchType.id, - name: propertySearchType.name - } : null, - city: city - }, - ...properties, - ...(categoryName === 'Lead' && { consultations }) - }; - } - - private async enrichPropertyDetails(property: PropertyRow): Promise { - const { PropertyPriceModel } = await import('../../data/postgres/models/properties/property-price.model'); - const { PropertyMultimediaModel } = await import('../../data/postgres/models/properties/property-multimedia.model'); - const { PropertyAddressModel } = await import('../../data/postgres/models/properties/property-address.model'); - const { AddressModel } = await import('../../data/postgres/models/properties/address.model'); - const { PropertyAgeModel } = await import('../../data/postgres/models/properties/property-age.model'); - const { PropertyOperationTypeModel } = await import('../../data/postgres/models/properties/property-operation-type.model'); - const { CurrencyTypeModel } = await import('../../data/postgres/models/payments/currency-type.model'); - - const [prices, mainImage, propertyAddresses, age] = await Promise.all([ - PropertyPriceModel.findByPropertyId(property.id), - PropertyMultimediaModel.findPrimaryByPropertyId(property.id), - PropertyAddressModel.findByPropertyId(property.id), - property.age_id ? PropertyAgeModel.findById(property.age_id) : Promise.resolve(null) - ]); - - let pricesData: Array<{ - amount: number; - currency: CurrencyInfo | null; - operation_type: OperationTypeInfo | null; - }> = []; - if (prices.length > 0) { - pricesData = await Promise.all( - prices.map(async (price) => { - const [currency, operationType] = await Promise.all([ - CurrencyTypeModel.findById(price.currency_type_id), - PropertyOperationTypeModel.findById(price.operation_type_id) - ]); - - return { - amount: price.price, - currency: currency ? { - id: currency.id, - name: currency.name, - symbol: currency.symbol - } : null, - operation_type: operationType ? { - id: operationType.id, - name: operationType.name - } : null - }; - }) - ); - } - - let addressData = null; - if (propertyAddresses.length > 0) { - const address = await AddressModel.findById(propertyAddresses[0].address_id); - if (address) { - const city = await CityModel.findById(address.city_id); - if (city) { - const province = await ProvinceModel.findById(city.province_id); - if (province) { - const country = await CountryModel.findById(province.country_id); - - addressData = { - street: address.street || null, - number: address.number || null, - full_address: address.full_address, - neighborhood: address.neighborhood, - city: { - id: city.id, - name: city.name, - province: { - id: province.id, - name: province.name, - country: country ? { - id: country.id, - name: country.name - } : null - } - }, - location: { - latitude: address.latitude ?? null, - longitude: address.longitude ?? null - } - }; - } - } - } - } - - let imageData = null; - if (mainImage) { - imageData = { - id: mainImage.id, - url: mainImage.file_path, - is_primary: true - }; - } - - let ageData = null; - if (age) { - ageData = { - id: age.id, - name: age.name - }; - } - - return { - description: property.description, - surface_area: property.total_area, - bedrooms: property.bedrooms_count, - bathrooms: property.bathrooms_count, - garage: (property.parking_spaces_count || 0) > 0, - address: addressData, - prices: pricesData, - main_image: imageData, - age: ageData - }; - } - - private async enrichClientWithProperties( - clientId: number, - categoryName: string | null - ): Promise<{ - properties_of_interest: PropertyOfInterest[]; - owned_properties: OwnedPropertyWithDetails[]; - rented_property: RentedPropertyWithDetails | null; - }> { - const { PropertyModel } = await import('../../data/postgres/models/properties/property.model'); - const { PropertyTypeModel } = await import('../../data/postgres/models/properties/property-type.model'); - const { PropertyStatusModel } = await import('../../data/postgres/models/properties/property-status.model'); - const { ClientRentalModel } = await import('../../data/postgres/models/rentals/client-rental.model'); - const { RentalModel } = await import('../../data/postgres/models/rentals/rental.model'); - const { CurrencyTypeModel } = await import('../../data/postgres/models/payments/currency-type.model'); - - let propertiesOfInterest: PropertyOfInterest[] = []; - let ownedProperties: OwnedPropertyWithDetails[] = []; - let rentedProperty: RentedPropertyWithDetails | null = null; - - if (categoryName === 'Lead') { - const interests = await ClientPropertyInterestModel.findByClientId(clientId); - - if (interests.length > 0) { - const interestResults = await Promise.all( - interests.map(async (interest) => { - const property = await PropertyModel.findById(interest.property_id); - if (!property) return null; - - const [propertyType, propertyStatus, enrichedDetails] = await Promise.all([ - PropertyTypeModel.findById(property.property_type_id), - PropertyStatusModel.findById(property.property_status_id), - this.enrichPropertyDetails(property as PropertyRow) - ]); - - return { - id: property.id, - title: property.title, - ...enrichedDetails, - property_type: propertyType ? { id: propertyType.id, name: propertyType.name } : null, - property_status: propertyStatus ? { id: propertyStatus.id, name: propertyStatus.name } : null, - interest_created_at: interest.created_at, - interest_notes: interest.notes - } as PropertyOfInterest; - }) - ); - propertiesOfInterest = interestResults.filter((p): p is PropertyOfInterest => p !== null); - } - } - - if (categoryName === 'Propietario') { - const properties = await PropertyModel.findAll({ owner_id: clientId }); - - ownedProperties = await Promise.all( - properties.map(async (property: PropertyRow) => { - const [propertyType, propertyStatus, enrichedDetails] = await Promise.all([ - PropertyTypeModel.findById(property.property_type_id), - PropertyStatusModel.findById(property.property_status_id), - this.enrichPropertyDetails(property as PropertyRow) - ]); - - return { - id: property.id, - title: property.title, - ...enrichedDetails, - property_type: propertyType ? { id: propertyType.id, name: propertyType.name } : null, - property_status: propertyStatus ? { id: propertyStatus.id, name: propertyStatus.name } : null, - publication_date: property.publication_date - }; - }) - ); - } - - if (categoryName === 'Inquilino') { - const clientRentals = await ClientRentalModel.findByClientId(clientId); - - if (clientRentals.length > 0) { - const sortedRentals = clientRentals.sort((a, b) => { - const dateA = a.contract_start_date ? new Date(a.contract_start_date).getTime() : 0; - const dateB = b.contract_start_date ? new Date(b.contract_start_date).getTime() : 0; - return dateB - dateA; - }); - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const activeRental = sortedRentals.find((rental) => { - if (!rental.contract_end_date) return true; - const endDate = new Date(rental.contract_end_date); - endDate.setHours(0, 0, 0, 0); - return endDate >= today; - }); - - if (activeRental && activeRental.property_id) { - const property = await PropertyModel.findById(activeRental.property_id); - if (property) { - const [propertyType, propertyStatus, enrichedDetails] = await Promise.all([ - PropertyTypeModel.findById(property.property_type_id), - PropertyStatusModel.findById(property.property_status_id), - this.enrichPropertyDetails(property as PropertyRow) - ]); - - const rentals = await RentalModel.findAll({ property_id: activeRental.property_id }); - const rentalInfo = rentals.find(r => r.client_rental_id === activeRental.id); - - let currency = null; - if (rentalInfo?.currency_type_id) { - currency = await CurrencyTypeModel.findById(rentalInfo.currency_type_id); - } - - rentedProperty = { - id: property.id, - title: property.title, - ...enrichedDetails, - property_type: propertyType ? { id: propertyType.id, name: propertyType.name } : null, - property_status: propertyStatus ? { id: propertyStatus.id, name: propertyStatus.name } : null, - rental: { - id: activeRental.id, - contract_start_date: activeRental.contract_start_date ?? null, - contract_end_date: activeRental.contract_end_date ?? null, - next_increase_date: activeRental.next_increase_date ?? null, - monthly_amount: rentalInfo?.monthly_amount ?? 0, - currency: currency ? { id: currency.id, name: currency.name, symbol: currency.symbol } : null, - external_reference: activeRental.external_reference || null - } - }; - } - } - } - - const interests = await ClientPropertyInterestModel.findByClientId(clientId); - - if (interests.length > 0) { - const interestResults = await Promise.all( - interests.map(async (interest) => { - const property = await PropertyModel.findById(interest.property_id); - if (!property) return null; - - const [propertyType, propertyStatus, enrichedDetails] = await Promise.all([ - PropertyTypeModel.findById(property.property_type_id), - PropertyStatusModel.findById(property.property_status_id), - this.enrichPropertyDetails(property as PropertyRow) - ]); - - return { - id: property.id, - title: property.title, - ...enrichedDetails, - property_type: propertyType ? { id: propertyType.id, name: propertyType.name } : null, - property_status: propertyStatus ? { id: propertyStatus.id, name: propertyStatus.name } : null, - interest_created_at: interest.created_at, - interest_notes: interest.notes - } as PropertyOfInterest; - }) - ); - propertiesOfInterest = interestResults.filter((p): p is PropertyOfInterest => p !== null); - } - } - - return { - properties_of_interest: propertiesOfInterest, - owned_properties: ownedProperties, - rented_property: rentedProperty - }; - } - - private async getClientConsultations(clientId: number): Promise { - const { ClientConsultationModel } = await import('../../data/postgres/models/crm/client-consultation.model'); - const { ConsultationTypeModel } = await import('../../data/postgres/models/crm/consultation-type.model'); - - const consultations = await ClientConsultationModel.findByClientId(clientId); - - if (consultations.length === 0) { - return []; - } - - const enrichedConsultations = await Promise.all( - consultations.map(async (consultation) => { - const consultationType = await ConsultationTypeModel.findById(consultation.consultation_type_id); - - let property = null; - if (consultation.property_id) { - const propertyData = await PropertyModel.findById(consultation.property_id); - if (propertyData) { - property = { - id: propertyData.id, - title: propertyData.title - }; - } - } - - return { - id: consultation.id, - consultation_date: consultation.consultation_date, - message: consultation.message, - response: consultation.response || null, - response_date: consultation.response_date || null, - is_read: consultation.is_read || false, - consultation_type: consultationType ? { - id: consultationType.id, - name: consultationType.name - } : null, - property: property - }; - }) - ); - - return enrichedConsultations; - } - - async updateClient(id: number, updateClientDto: UpdateClientDto) { - const existingClient = await ClientModel.findById(id); - if (!existingClient) { - throw CustomError.notFound(`Client with ID ${id} not found`); - } - - const clientEntity = ClientEntity.fromDatabaseObject(existingClient); - - if (!clientEntity.canBeUpdated()) { - throw CustomError.badRequest('Cannot update deleted client'); - } - - let contactCategoryId: number | undefined = updateClientDto.contact_category_id; - if (!contactCategoryId && updateClientDto.contact_category) { - const category = await ContactCategoryModel.findByName(updateClientDto.contact_category); - if (!category || !category.id) { - throw CustomError.badRequest( - `Contact category "${updateClientDto.contact_category}" not found. Valid categories: Lead, Inquilino, Propietario` - ); - } - contactCategoryId = category.id; - } - - let propertySearchTypeId: number | undefined = updateClientDto.property_search_type_id; - if (!propertySearchTypeId && updateClientDto.property_search_type) { - const { PropertySearchTypeModel } = await import('../../data/postgres/models/clients/property-search-type.model'); - const searchType = await PropertySearchTypeModel.findByName(updateClientDto.property_search_type); - if (!searchType || !searchType.id) { - throw CustomError.badRequest( - `Property search type "${updateClientDto.property_search_type}" not found` - ); - } - propertySearchTypeId = searchType.id; - } - - const updateData: Record = {}; - - if (updateClientDto.first_name !== undefined && hasValueChanged(updateClientDto.first_name, existingClient.first_name)) { - updateData.first_name = updateClientDto.first_name; - } - if (updateClientDto.last_name !== undefined && hasValueChanged(updateClientDto.last_name, existingClient.last_name)) { - updateData.last_name = updateClientDto.last_name; - } - if (updateClientDto.phone !== undefined && hasValueChanged(updateClientDto.phone, existingClient.phone)) { - updateData.phone = normalizePhone(updateClientDto.phone); - } - if (updateClientDto.email !== undefined && hasValueChanged(updateClientDto.email, existingClient.email)) { - updateData.email = updateClientDto.email; - } - if (updateClientDto.dni !== undefined && hasValueChanged(updateClientDto.dni, existingClient.dni)) { - updateData.dni = updateClientDto.dni; - } - if (updateClientDto.property_interest_phone !== undefined && hasValueChanged(updateClientDto.property_interest_phone, existingClient.property_interest_phone)) { - updateData.property_interest_phone = normalizePhone(updateClientDto.property_interest_phone); - } - if (updateClientDto.address !== undefined && hasValueChanged(updateClientDto.address, existingClient.address)) { - updateData.address = updateClientDto.address; - } - if (updateClientDto.notes !== undefined && hasValueChanged(updateClientDto.notes, existingClient.notes)) { - updateData.notes = updateClientDto.notes; - } - if (contactCategoryId !== undefined && hasValueChanged(contactCategoryId, existingClient.contact_category_id)) { - updateData.contact_category_id = contactCategoryId; - } - if (updateClientDto.interest_zone !== undefined && hasValueChanged(updateClientDto.interest_zone, existingClient.interest_zone)) { - updateData.interest_zone = updateClientDto.interest_zone; - } - if (updateClientDto.purchase_interest !== undefined && hasValueChanged(updateClientDto.purchase_interest, existingClient.purchase_interest)) { - updateData.purchase_interest = updateClientDto.purchase_interest; - } - if (updateClientDto.rental_interest !== undefined && hasValueChanged(updateClientDto.rental_interest, existingClient.rental_interest)) { - updateData.rental_interest = updateClientDto.rental_interest; - } - if (propertySearchTypeId !== undefined && hasValueChanged(propertySearchTypeId, existingClient.property_search_type_id)) { - updateData.property_search_type_id = propertySearchTypeId; - } - if (updateClientDto.city_id !== undefined && hasValueChanged(updateClientDto.city_id, existingClient.city_id)) { - updateData.city_id = updateClientDto.city_id; - } - - if (Object.keys(updateData).length === 0) { - return { client: clientEntity.toPublicObject() }; - } - const updatedClient = await ClientModel.update(id, updateData); - if (!updatedClient) { - throw CustomError.internalServerError('Failed to update client'); - } - - const updatedClientEntity = ClientEntity.fromDatabaseObject(updatedClient); - - return { client: updatedClientEntity.toPublicObject() }; - } - - async deleteClient(id: number) { - const client = await ClientModel.findById(id); - if (!client) { - throw CustomError.notFound(`Client with ID ${id} not found`); - } - - if (client.deleted === true) { - throw CustomError.badRequest('Client is already deleted'); - } - - const ownedProperties = await PropertyModel.findAll({ owner_id: id }); - if (ownedProperties.length > 0) { - throw CustomError.badRequest( - `Cannot delete client: has ${ownedProperties.length} active ${ownedProperties.length === 1 ? 'property' : 'properties'} as owner` - ); - } - - const { ClientRentalModel } = await import('../../data/postgres/models/rentals/client-rental.model'); - const clientRentals = await ClientRentalModel.findByClientId(id); - - if (clientRentals.length > 0) { - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const activeRentals = clientRentals.filter((rental) => { - if (!rental.contract_end_date) return true; - const endDate = new Date(rental.contract_end_date); - endDate.setHours(0, 0, 0, 0); - return endDate >= today; - }); - - if (activeRentals.length > 0) { - throw CustomError.badRequest( - `Cannot delete client: has ${activeRentals.length} active ${activeRentals.length === 1 ? 'rental' : 'rentals'}` - ); - } - } - - const deleted = await ClientModel.delete(id); - if (!deleted) { - throw CustomError.internalServerError('Failed to delete client'); - } - - return { message: 'Client deleted successfully' }; - } - - - async restoreClient(id: number) { - const restored = await ClientModel.restore(id); - if (!restored) { - throw CustomError.notFound(`Deleted client with ID ${id} not found or already restored`); - } - - return { message: 'Client restored successfully' }; - } - - private async resolveGeography(geography: { - country?: string; - province?: string; - city?: string; - }): Promise<{ cityId: number }> { - if (!geography.country || !geography.province || !geography.city) { - throw CustomError.badRequest('Country, province, and city are required for geography'); - } - - let country = await CountryModel.findByName(geography.country); - if (!country || !country.id) { - country = await CountryModel.create({ name: geography.country }); - } - let province = await ProvinceModel.findByName(geography.province); - if (!province || !province.id) { - province = await ProvinceModel.create({ - name: geography.province, - country_id: country.id! - }); - } - - let city = await CityModel.findByNameAndProvince(geography.city, province.id!); - if (!city || !city.id) { - city = await CityModel.create({ - name: geography.city, - province_id: province.id! - }); - } - - return { cityId: city.id! }; - } - - async addPropertyOfInterest(clientId: number, propertyId: number, notes?: string) { - const client = await ClientModel.findById(clientId); - if (!client) { - throw CustomError.notFound(`Client with ID ${clientId} not found`); - } - - const property = await PropertyModel.findById(propertyId); - if (!property) { - throw CustomError.notFound(`Property with ID ${propertyId} not found`); - } - const propertyInterest = await ClientPropertyInterestModel.create({ - client_id: clientId, - property_id: propertyId, - notes: notes || undefined, - }); - - const { PropertyTypeModel } = await import('../../data/postgres/models/properties/property-type.model'); - const { PropertyStatusModel } = await import('../../data/postgres/models/properties/property-status.model'); - - const [propertyType, propertyStatus] = await Promise.all([ - PropertyTypeModel.findById(property.property_type_id), - PropertyStatusModel.findById(property.property_status_id) - ]); - - return { - property_interest: { - id: propertyInterest.id, - client_id: propertyInterest.client_id, - property_id: propertyInterest.property_id, - created_at: propertyInterest.created_at, - notes: propertyInterest.notes, - }, - property: { - id: property.id, - title: property.title, - property_type: propertyType ? { id: propertyType.id, name: propertyType.name } : null, - property_status: propertyStatus ? { id: propertyStatus.id, name: propertyStatus.name } : null, - } - }; - } - - async addOwnedProperty(clientId: number, propertyId: number) { - const client = await ClientModel.findById(clientId); - if (!client) { - throw CustomError.notFound(`Client with ID ${clientId} not found`); - } - - const { ContactCategoryModel } = await import('../../data/postgres/models/clients/contact-category.model'); - const category = await ContactCategoryModel.findById(client.contact_category_id); - - if (!category || category.name !== 'Propietario') { - throw CustomError.badRequest('Client must be an Owner to associate properties'); - } - - const property = await PropertyModel.findById(propertyId); - if (!property) { - throw CustomError.notFound(`Property with ID ${propertyId} not found`); - } - const updatedProperty = await PropertyModel.update(propertyId, { - owner_id: clientId - }); - - if (!updatedProperty) { - throw CustomError.internalServerError('Failed to associate property with owner'); - } - - const { PropertyTypeModel } = await import('../../data/postgres/models/properties/property-type.model'); - const { PropertyStatusModel } = await import('../../data/postgres/models/properties/property-status.model'); - - const [propertyType, propertyStatus] = await Promise.all([ - PropertyTypeModel.findById(updatedProperty.property_type_id), - PropertyStatusModel.findById(updatedProperty.property_status_id) - ]); - - return { - property: { - id: updatedProperty.id, - title: updatedProperty.title, - owner_id: updatedProperty.owner_id, - property_type: propertyType ? { id: propertyType.id, name: propertyType.name } : null, - property_status: propertyStatus ? { id: propertyStatus.id, name: propertyStatus.name } : null, - publication_date: updatedProperty.publication_date - } - }; - } + async listClients(filters?: { + contact_category_id?: number; + purchase_interest?: boolean; + rental_interest?: boolean; + city_id?: number; + limit?: number; + offset?: number; + search?: string; + includeDeleted?: boolean; + }) { + let clients = await ClientModel.findAll(filters); + + if (filters?.search) { + const searchLower = filters.search.toLowerCase(); + clients = clients.filter( + (client) => + client.first_name?.toLowerCase().includes(searchLower) || + client.last_name?.toLowerCase().includes(searchLower) || + client.email?.toLowerCase().includes(searchLower) || + client.phone?.includes(searchLower) || + client.dni?.includes(searchLower), + ); + } + + const { ContactCategoryModel } = await import( + "../../data/postgres/models/clients/contact-category.model" + ); + const enrichedClients = await Promise.all( + clients.map(async (client) => { + const clientEntity = ClientEntity.fromDatabaseObject(client); + const category = await ContactCategoryModel.findById( + clientEntity.contact_category_id, + ); + + const categoryName = category?.name || null; + const properties = await this.enrichClientWithProperties( + clientEntity.id, + categoryName, + ); + + return { + ...clientEntity.toPublicObject(), + contact_category: category + ? { + id: category.id, + name: category.name, + } + : null, + ...properties, + }; + }), + ); + + return { clients: enrichedClients, count: enrichedClients.length }; + } + + async getClientById(id: number) { + const client = await ClientModel.findById(id); + if (!client) { + throw CustomError.notFound(`Client with ID ${id} not found`); + } + + const clientEntity = ClientEntity.fromDatabaseObject(client); + + const { ContactCategoryModel } = await import( + "../../data/postgres/models/clients/contact-category.model" + ); + const category = await ContactCategoryModel.findById( + clientEntity.contact_category_id, + ); + + let propertySearchType = null; + if (clientEntity.property_search_type_id) { + const { PropertySearchTypeModel } = await import( + "../../data/postgres/models/clients/property-search-type.model" + ); + propertySearchType = await PropertySearchTypeModel.findById( + clientEntity.property_search_type_id, + ); + } + + let city = null; + if (clientEntity.city_id) { + city = await CityModel.findById(clientEntity.city_id); + if (city) { + const province = await ProvinceModel.findById(city.province_id); + if (province) { + const country = await CountryModel.findById(province.country_id); + + interface CityWithProvince { + id: number | undefined; + name: string; + province_id: number; + province?: { + id: number | undefined; + name: string; + country: { id: number | undefined; name: string } | null; + } | null; + } + const cityWithProvince = city as CityWithProvince; + cityWithProvince.province = province + ? { + id: province.id, + name: province.name, + country: country + ? { + id: country.id, + name: country.name, + } + : null, + } + : null; + } + } + } + + const categoryName = category?.name; + const properties = await this.enrichClientWithProperties( + clientEntity.id, + categoryName || null, + ); + + let consultations: EnrichedConsultation[] = []; + if (categoryName === "Lead") { + consultations = await this.getClientConsultations(clientEntity.id); + } + + return { + client: { + ...clientEntity.toPublicObject(), + contact_category: category + ? { + id: category.id, + name: category.name, + } + : null, + property_search_type: propertySearchType + ? { + id: propertySearchType.id, + name: propertySearchType.name, + } + : null, + city: city, + }, + ...properties, + ...(categoryName === "Lead" && { consultations }), + }; + } + + private async enrichPropertyDetails( + property: PropertyRow, + ): Promise { + const { PropertyPriceModel } = await import( + "../../data/postgres/models/properties/property-price.model" + ); + const { PropertyMultimediaModel } = await import( + "../../data/postgres/models/properties/property-multimedia.model" + ); + const { PropertyAddressModel } = await import( + "../../data/postgres/models/properties/property-address.model" + ); + const { AddressModel } = await import( + "../../data/postgres/models/properties/address.model" + ); + const { PropertyAgeModel } = await import( + "../../data/postgres/models/properties/property-age.model" + ); + const { PropertyOperationTypeModel } = await import( + "../../data/postgres/models/properties/property-operation-type.model" + ); + const { CurrencyTypeModel } = await import( + "../../data/postgres/models/payments/currency-type.model" + ); + + const [prices, mainImage, propertyAddresses, age] = await Promise.all([ + PropertyPriceModel.findByPropertyId(property.id), + PropertyMultimediaModel.findPrimaryByPropertyId(property.id), + PropertyAddressModel.findByPropertyId(property.id), + property.age_id + ? PropertyAgeModel.findById(property.age_id) + : Promise.resolve(null), + ]); + + let pricesData: Array<{ + amount: number; + currency: CurrencyInfo | null; + operation_type: OperationTypeInfo | null; + }> = []; + if (prices.length > 0) { + pricesData = await Promise.all( + prices.map(async (price) => { + const [currency, operationType] = await Promise.all([ + CurrencyTypeModel.findById(price.currency_type_id), + PropertyOperationTypeModel.findById(price.operation_type_id), + ]); + + return { + amount: price.price, + currency: currency + ? { + id: currency.id, + name: currency.name, + symbol: currency.symbol, + } + : null, + operation_type: operationType + ? { + id: operationType.id, + name: operationType.name, + } + : null, + }; + }), + ); + } + + let addressData = null; + if (propertyAddresses.length > 0) { + const address = await AddressModel.findById( + propertyAddresses[0].address_id, + ); + if (address) { + const city = await CityModel.findById(address.city_id); + if (city) { + const province = await ProvinceModel.findById(city.province_id); + if (province) { + const country = await CountryModel.findById(province.country_id); + + addressData = { + street: address.street || null, + number: address.number || null, + full_address: address.full_address, + neighborhood: address.neighborhood, + city: { + id: city.id, + name: city.name, + province: { + id: province.id, + name: province.name, + country: country + ? { + id: country.id, + name: country.name, + } + : null, + }, + }, + location: { + latitude: address.latitude ?? null, + longitude: address.longitude ?? null, + }, + }; + } + } + } + } + + let imageData = null; + if (mainImage) { + imageData = { + id: mainImage.id, + url: mainImage.file_path, + is_primary: true, + }; + } + + let ageData = null; + if (age) { + ageData = { + id: age.id, + name: age.name, + }; + } + + return { + description: property.description, + surface_area: property.total_area, + bedrooms: property.bedrooms_count, + bathrooms: property.bathrooms_count, + garage: (property.parking_spaces_count || 0) > 0, + address: addressData, + prices: pricesData, + main_image: imageData, + age: ageData, + }; + } + + private async enrichClientWithProperties( + clientId: number, + categoryName: string | null, + ): Promise<{ + properties_of_interest: PropertyOfInterest[]; + owned_properties: OwnedPropertyWithDetails[]; + rented_property: RentedPropertyWithDetails | null; + }> { + const { PropertyModel } = await import( + "../../data/postgres/models/properties/property.model" + ); + const { PropertyTypeModel } = await import( + "../../data/postgres/models/properties/property-type.model" + ); + const { PropertyStatusModel } = await import( + "../../data/postgres/models/properties/property-status.model" + ); + const { ClientRentalModel } = await import( + "../../data/postgres/models/rentals/client-rental.model" + ); + const { RentalModel } = await import( + "../../data/postgres/models/rentals/rental.model" + ); + const { CurrencyTypeModel } = await import( + "../../data/postgres/models/payments/currency-type.model" + ); + + let propertiesOfInterest: PropertyOfInterest[] = []; + let ownedProperties: OwnedPropertyWithDetails[] = []; + let rentedProperty: RentedPropertyWithDetails | null = null; + + if (categoryName === "Lead") { + const interests = + await ClientPropertyInterestModel.findByClientId(clientId); + + if (interests.length > 0) { + const interestResults = await Promise.all( + interests.map(async (interest) => { + const property = await PropertyModel.findById(interest.property_id); + if (!property) return null; + + const [propertyType, propertyStatus, enrichedDetails] = + await Promise.all([ + PropertyTypeModel.findById(property.property_type_id), + PropertyStatusModel.findById(property.property_status_id), + this.enrichPropertyDetails(property as PropertyRow), + ]); + + return { + id: property.id, + title: property.title, + ...enrichedDetails, + property_type: propertyType + ? { id: propertyType.id, name: propertyType.name } + : null, + property_status: propertyStatus + ? { id: propertyStatus.id, name: propertyStatus.name } + : null, + interest_created_at: interest.created_at, + interest_notes: interest.notes, + } as PropertyOfInterest; + }), + ); + propertiesOfInterest = interestResults.filter( + (p): p is PropertyOfInterest => p !== null, + ); + } + } + + if (categoryName === "Propietario") { + const properties = await PropertyModel.findAll({ owner_id: clientId }); + + ownedProperties = await Promise.all( + properties.map(async (property: PropertyRow) => { + const [propertyType, propertyStatus, enrichedDetails] = + await Promise.all([ + PropertyTypeModel.findById(property.property_type_id), + PropertyStatusModel.findById(property.property_status_id), + this.enrichPropertyDetails(property as PropertyRow), + ]); + + return { + id: property.id, + title: property.title, + ...enrichedDetails, + property_type: propertyType + ? { id: propertyType.id, name: propertyType.name } + : null, + property_status: propertyStatus + ? { id: propertyStatus.id, name: propertyStatus.name } + : null, + publication_date: property.publication_date, + }; + }), + ); + } + + if (categoryName === "Inquilino") { + const clientRentals = await ClientRentalModel.findByClientId(clientId); + + if (clientRentals.length > 0) { + const sortedRentals = clientRentals.sort((a, b) => { + const dateA = a.contract_start_date + ? new Date(a.contract_start_date).getTime() + : 0; + const dateB = b.contract_start_date + ? new Date(b.contract_start_date).getTime() + : 0; + return dateB - dateA; + }); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const activeRental = sortedRentals.find((rental) => { + if (!rental.contract_end_date) return true; + const endDate = new Date(rental.contract_end_date); + endDate.setHours(0, 0, 0, 0); + return endDate >= today; + }); + + if (activeRental?.property_id) { + const property = await PropertyModel.findById( + activeRental.property_id, + ); + if (property) { + const [propertyType, propertyStatus, enrichedDetails] = + await Promise.all([ + PropertyTypeModel.findById(property.property_type_id), + PropertyStatusModel.findById(property.property_status_id), + this.enrichPropertyDetails(property as PropertyRow), + ]); + + const rentals = await RentalModel.findAll({ + property_id: activeRental.property_id, + }); + const rentalInfo = rentals.find( + (r) => r.client_rental_id === activeRental.id, + ); + + let currency = null; + if (rentalInfo?.currency_type_id) { + currency = await CurrencyTypeModel.findById( + rentalInfo.currency_type_id, + ); + } + + rentedProperty = { + id: property.id, + title: property.title, + ...enrichedDetails, + property_type: propertyType + ? { id: propertyType.id, name: propertyType.name } + : null, + property_status: propertyStatus + ? { id: propertyStatus.id, name: propertyStatus.name } + : null, + rental: { + id: activeRental.id, + contract_start_date: activeRental.contract_start_date ?? null, + contract_end_date: activeRental.contract_end_date ?? null, + next_increase_date: activeRental.next_increase_date ?? null, + monthly_amount: rentalInfo?.monthly_amount ?? 0, + currency: currency + ? { + id: currency.id, + name: currency.name, + symbol: currency.symbol, + } + : null, + external_reference: activeRental.external_reference || null, + }, + }; + } + } + } + + const interests = + await ClientPropertyInterestModel.findByClientId(clientId); + + if (interests.length > 0) { + const interestResults = await Promise.all( + interests.map(async (interest) => { + const property = await PropertyModel.findById(interest.property_id); + if (!property) return null; + + const [propertyType, propertyStatus, enrichedDetails] = + await Promise.all([ + PropertyTypeModel.findById(property.property_type_id), + PropertyStatusModel.findById(property.property_status_id), + this.enrichPropertyDetails(property as PropertyRow), + ]); + + return { + id: property.id, + title: property.title, + ...enrichedDetails, + property_type: propertyType + ? { id: propertyType.id, name: propertyType.name } + : null, + property_status: propertyStatus + ? { id: propertyStatus.id, name: propertyStatus.name } + : null, + interest_created_at: interest.created_at, + interest_notes: interest.notes, + } as PropertyOfInterest; + }), + ); + propertiesOfInterest = interestResults.filter( + (p): p is PropertyOfInterest => p !== null, + ); + } + } + + return { + properties_of_interest: propertiesOfInterest, + owned_properties: ownedProperties, + rented_property: rentedProperty, + }; + } + + private async getClientConsultations( + clientId: number, + ): Promise[]> { + const { ClientConsultationModel } = await import( + "../../data/postgres/models/crm/client-consultation.model" + ); + const { ConsultationTypeModel } = await import( + "../../data/postgres/models/crm/consultation-type.model" + ); + + const consultations = + await ClientConsultationModel.findByClientId(clientId); + + if (consultations.length === 0) { + return []; + } + + const enrichedConsultations = await Promise.all( + consultations.map(async (consultation) => { + const consultationType = await ConsultationTypeModel.findById( + consultation.consultation_type_id, + ); + + let property = null; + if (consultation.property_id) { + const propertyData = await PropertyModel.findById( + consultation.property_id, + ); + if (propertyData) { + property = { + id: propertyData.id, + title: propertyData.title, + }; + } + } + + return { + id: consultation.id, + consultation_date: consultation.consultation_date, + message: consultation.message, + response: consultation.response || null, + response_date: consultation.response_date || null, + is_read: consultation.is_read || false, + consultation_type: consultationType + ? { + id: consultationType.id, + name: consultationType.name, + } + : null, + property: property, + }; + }), + ); + + return enrichedConsultations; + } + + async updateClient(id: number, updateClientDto: UpdateClientDto) { + const existingClient = await ClientModel.findById(id); + if (!existingClient) { + throw CustomError.notFound(`Client with ID ${id} not found`); + } + + const clientEntity = ClientEntity.fromDatabaseObject(existingClient); + + if (!clientEntity.canBeUpdated()) { + throw CustomError.badRequest("Cannot update deleted client"); + } + + let contactCategoryId: number | undefined = + updateClientDto.contact_category_id; + if (!contactCategoryId && updateClientDto.contact_category) { + const category = await ContactCategoryModel.findByName( + updateClientDto.contact_category, + ); + if (!category || !category.id) { + throw CustomError.badRequest( + `Contact category "${updateClientDto.contact_category}" not found. Valid categories: Lead, Inquilino, Propietario`, + ); + } + contactCategoryId = category.id; + } + + let propertySearchTypeId: number | undefined = + updateClientDto.property_search_type_id; + if (!propertySearchTypeId && updateClientDto.property_search_type) { + const { PropertySearchTypeModel } = await import( + "../../data/postgres/models/clients/property-search-type.model" + ); + const searchType = await PropertySearchTypeModel.findByName( + updateClientDto.property_search_type, + ); + if (!searchType || !searchType.id) { + throw CustomError.badRequest( + `Property search type "${updateClientDto.property_search_type}" not found`, + ); + } + propertySearchTypeId = searchType.id; + } + + const updateData: Record = {}; + + if ( + updateClientDto.first_name !== undefined && + hasValueChanged(updateClientDto.first_name, existingClient.first_name) + ) { + updateData.first_name = updateClientDto.first_name; + } + if ( + updateClientDto.last_name !== undefined && + hasValueChanged(updateClientDto.last_name, existingClient.last_name) + ) { + updateData.last_name = updateClientDto.last_name; + } + if ( + updateClientDto.phone !== undefined && + hasValueChanged(updateClientDto.phone, existingClient.phone) + ) { + updateData.phone = normalizePhone(updateClientDto.phone); + } + if ( + updateClientDto.email !== undefined && + hasValueChanged(updateClientDto.email, existingClient.email) + ) { + updateData.email = updateClientDto.email; + } + if ( + updateClientDto.dni !== undefined && + hasValueChanged(updateClientDto.dni, existingClient.dni) + ) { + updateData.dni = updateClientDto.dni; + } + if ( + updateClientDto.property_interest_phone !== undefined && + hasValueChanged( + updateClientDto.property_interest_phone, + existingClient.property_interest_phone, + ) + ) { + updateData.property_interest_phone = normalizePhone( + updateClientDto.property_interest_phone, + ); + } + if ( + updateClientDto.address !== undefined && + hasValueChanged(updateClientDto.address, existingClient.address) + ) { + updateData.address = updateClientDto.address; + } + if ( + updateClientDto.notes !== undefined && + hasValueChanged(updateClientDto.notes, existingClient.notes) + ) { + updateData.notes = updateClientDto.notes; + } + if ( + contactCategoryId !== undefined && + hasValueChanged(contactCategoryId, existingClient.contact_category_id) + ) { + updateData.contact_category_id = contactCategoryId; + } + if ( + updateClientDto.interest_zone !== undefined && + hasValueChanged( + updateClientDto.interest_zone, + existingClient.interest_zone, + ) + ) { + updateData.interest_zone = updateClientDto.interest_zone; + } + if ( + updateClientDto.purchase_interest !== undefined && + hasValueChanged( + updateClientDto.purchase_interest, + existingClient.purchase_interest, + ) + ) { + updateData.purchase_interest = updateClientDto.purchase_interest; + } + if ( + updateClientDto.rental_interest !== undefined && + hasValueChanged( + updateClientDto.rental_interest, + existingClient.rental_interest, + ) + ) { + updateData.rental_interest = updateClientDto.rental_interest; + } + if ( + propertySearchTypeId !== undefined && + hasValueChanged( + propertySearchTypeId, + existingClient.property_search_type_id, + ) + ) { + updateData.property_search_type_id = propertySearchTypeId; + } + if ( + updateClientDto.city_id !== undefined && + hasValueChanged(updateClientDto.city_id, existingClient.city_id) + ) { + updateData.city_id = updateClientDto.city_id; + } + + if (Object.keys(updateData).length === 0) { + return { client: clientEntity.toPublicObject() }; + } + const updatedClient = await ClientModel.update(id, updateData); + if (!updatedClient) { + throw CustomError.internalServerError("Failed to update client"); + } + + const updatedClientEntity = ClientEntity.fromDatabaseObject(updatedClient); + + return { client: updatedClientEntity.toPublicObject() }; + } + + async deleteClient(id: number) { + const client = await ClientModel.findById(id); + if (!client) { + throw CustomError.notFound(`Client with ID ${id} not found`); + } + + if (client.deleted === true) { + throw CustomError.badRequest("Client is already deleted"); + } + + const ownedProperties = await PropertyModel.findAll({ owner_id: id }); + if (ownedProperties.length > 0) { + throw CustomError.badRequest( + `Cannot delete client: has ${ownedProperties.length} active ${ownedProperties.length === 1 ? "property" : "properties"} as owner`, + ); + } + + const { ClientRentalModel } = await import( + "../../data/postgres/models/rentals/client-rental.model" + ); + const clientRentals = await ClientRentalModel.findByClientId(id); + + if (clientRentals.length > 0) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const activeRentals = clientRentals.filter((rental) => { + if (!rental.contract_end_date) return true; + const endDate = new Date(rental.contract_end_date); + endDate.setHours(0, 0, 0, 0); + return endDate >= today; + }); + + if (activeRentals.length > 0) { + throw CustomError.badRequest( + `Cannot delete client: has ${activeRentals.length} active ${activeRentals.length === 1 ? "rental" : "rentals"}`, + ); + } + } + + const deleted = await ClientModel.delete(id); + if (!deleted) { + throw CustomError.internalServerError("Failed to delete client"); + } + + return { message: "Client deleted successfully" }; + } + + async restoreClient(id: number) { + const restored = await ClientModel.restore(id); + if (!restored) { + throw CustomError.notFound( + `Deleted client with ID ${id} not found or already restored`, + ); + } + + return { message: "Client restored successfully" }; + } + + async addPropertyOfInterest( + clientId: number, + propertyId: number, + notes?: string, + ) { + const client = await ClientModel.findById(clientId); + if (!client) { + throw CustomError.notFound(`Client with ID ${clientId} not found`); + } + + const property = await PropertyModel.findById(propertyId); + if (!property) { + throw CustomError.notFound(`Property with ID ${propertyId} not found`); + } + const propertyInterest = await ClientPropertyInterestModel.create({ + client_id: clientId, + property_id: propertyId, + notes: notes || undefined, + }); + + const { PropertyTypeModel } = await import( + "../../data/postgres/models/properties/property-type.model" + ); + const { PropertyStatusModel } = await import( + "../../data/postgres/models/properties/property-status.model" + ); + + const [propertyType, propertyStatus] = await Promise.all([ + PropertyTypeModel.findById(property.property_type_id), + PropertyStatusModel.findById(property.property_status_id), + ]); + + return { + property_interest: { + id: propertyInterest.id, + client_id: propertyInterest.client_id, + property_id: propertyInterest.property_id, + created_at: propertyInterest.created_at, + notes: propertyInterest.notes, + }, + property: { + id: property.id, + title: property.title, + property_type: propertyType + ? { id: propertyType.id, name: propertyType.name } + : null, + property_status: propertyStatus + ? { id: propertyStatus.id, name: propertyStatus.name } + : null, + }, + }; + } + + async addOwnedProperty(clientId: number, propertyId: number) { + const client = await ClientModel.findById(clientId); + if (!client) { + throw CustomError.notFound(`Client with ID ${clientId} not found`); + } + + const { ContactCategoryModel } = await import( + "../../data/postgres/models/clients/contact-category.model" + ); + const category = await ContactCategoryModel.findById( + client.contact_category_id, + ); + + if (!category || category.name !== "Propietario") { + throw CustomError.badRequest( + "Client must be an Owner to associate properties", + ); + } + + const property = await PropertyModel.findById(propertyId); + if (!property) { + throw CustomError.notFound(`Property with ID ${propertyId} not found`); + } + const updatedProperty = await PropertyModel.update(propertyId, { + owner_id: clientId, + }); + + if (!updatedProperty) { + throw CustomError.internalServerError( + "Failed to associate property with owner", + ); + } + + const { PropertyTypeModel } = await import( + "../../data/postgres/models/properties/property-type.model" + ); + const { PropertyStatusModel } = await import( + "../../data/postgres/models/properties/property-status.model" + ); + + const [propertyType, propertyStatus] = await Promise.all([ + PropertyTypeModel.findById(updatedProperty.property_type_id), + PropertyStatusModel.findById(updatedProperty.property_status_id), + ]); + + return { + property: { + id: updatedProperty.id, + title: updatedProperty.title, + owner_id: updatedProperty.owner_id, + property_type: propertyType + ? { id: propertyType.id, name: propertyType.name } + : null, + property_status: propertyStatus + ? { id: propertyStatus.id, name: propertyStatus.name } + : null, + publication_date: updatedProperty.publication_date, + }, + }; + } } - diff --git a/backend/src/presentation/services/company.services.ts b/backend/src/presentation/services/company.services.ts index 7ae7e99..9e59949 100644 --- a/backend/src/presentation/services/company.services.ts +++ b/backend/src/presentation/services/company.services.ts @@ -5,7 +5,6 @@ import type { FileUploadAdapter } from "../../domain/interfaces/file-upload.adap export class CompanyServices { constructor(private readonly fileUploadAdapter: FileUploadAdapter) {} - async getCompanySettings() { try { const settings = await CompanySettingsModel.findSettings(); diff --git a/backend/src/presentation/services/dashboard.services.ts b/backend/src/presentation/services/dashboard.services.ts index 0c06ebd..d04be7e 100644 --- a/backend/src/presentation/services/dashboard.services.ts +++ b/backend/src/presentation/services/dashboard.services.ts @@ -1,17 +1,22 @@ -import { PostgresDatabase } from '../../data/postgres/database'; -import { CustomError } from '../../domain'; -import { SqlParams } from '../../data/types/sql.types'; +import { PostgresDatabase } from "../../data/postgres/database"; import { ClientConsultationModel, + ClientModel, PropertyModel, VisibilityStatusModel, - ClientModel, -} from '../../data/postgres/models'; +} from "../../data/postgres/models"; +import type { SqlParams } from "../../data/types/sql.types"; export class DashboardServices { async getDashboardData() { const consultations = await this.getLatestConsultations(5); - const [activeProperties, inactiveProperties, unansweredConsultations, unreadConsultations, newLeadsToday] = await Promise.all([ + const [ + activeProperties, + inactiveProperties, + unansweredConsultations, + unreadConsultations, + newLeadsToday, + ] = await Promise.all([ this.countActiveProperties(), this.countInactiveProperties(), this.countUnansweredConsultations(), @@ -65,7 +70,7 @@ export class DashboardServices { consultation.consultant_last_name, ] .filter(Boolean) - .join(' '); + .join(" "); consultantEmail = consultation.consultant_email || null; consultantPhone = consultation.consultant_phone || null; } @@ -74,7 +79,7 @@ export class DashboardServices { id: consultation.id, consultation_date: consultation.consultation_date, is_read: consultation.is_read || false, - message: consultation.message || '', + message: consultation.message || "", consultant_name: consultantName, consultant_email: consultantEmail, consultant_phone: consultantPhone, @@ -92,50 +97,50 @@ export class DashboardServices { } private async countActiveProperties(): Promise { - const archivedStatus = await VisibilityStatusModel.findByName('Archivada'); + const archivedStatus = await VisibilityStatusModel.findByName("Archivada"); const archivedStatusId = archivedStatus?.id; let query = `SELECT COUNT(*) as count FROM properties`; const values: SqlParams = []; - + if (archivedStatusId) { query += ` WHERE visibility_status_id != $1`; values.push(archivedStatusId); } const result = await PostgresDatabase.query(query, values); - return parseInt(result.rows[0]?.count || '0', 10); + return parseInt(result.rows[0]?.count || "0", 10); } private async countInactiveProperties(): Promise { - const archivedStatus = await VisibilityStatusModel.findByName('Archivada'); - + const archivedStatus = await VisibilityStatusModel.findByName("Archivada"); + if (!archivedStatus?.id) { return 0; } const query = `SELECT COUNT(*) as count FROM properties WHERE visibility_status_id = $1`; const result = await PostgresDatabase.query(query, [archivedStatus.id]); - return parseInt(result.rows[0]?.count || '0', 10); + return parseInt(result.rows[0]?.count || "0", 10); } private async countUnansweredConsultations(): Promise { const query = `SELECT COUNT(*) as count FROM client_consultations WHERE responded_by_user_id IS NULL`; const result = await PostgresDatabase.query(query); - return parseInt(result.rows[0]?.count || '0', 10); + return parseInt(result.rows[0]?.count || "0", 10); } private async countUnreadConsultations(): Promise { const query = `SELECT COUNT(*) as count FROM client_consultations WHERE is_read = false`; const result = await PostgresDatabase.query(query); - return parseInt(result.rows[0]?.count || '0', 10); + return parseInt(result.rows[0]?.count || "0", 10); } private async countNewLeadsToday(): Promise { - const { ContactCategoryModel } = await import('../../data/postgres/models'); - - const leadCategory = await ContactCategoryModel.findByName('Lead'); - + const { ContactCategoryModel } = await import("../../data/postgres/models"); + + const leadCategory = await ContactCategoryModel.findByName("Lead"); + if (!leadCategory?.id) { return 0; } @@ -148,6 +153,6 @@ export class DashboardServices { AND deleted = false `; const result = await PostgresDatabase.query(query, [leadCategory.id]); - return parseInt(result.rows[0]?.count || '0', 10); + return parseInt(result.rows[0]?.count || "0", 10); } } diff --git a/backend/src/presentation/services/helpers/client-creation.helper.ts b/backend/src/presentation/services/helpers/client-creation.helper.ts index fdc8dd7..bec4031 100644 --- a/backend/src/presentation/services/helpers/client-creation.helper.ts +++ b/backend/src/presentation/services/helpers/client-creation.helper.ts @@ -1,102 +1,100 @@ -import { - ClientModel, - ContactCategoryModel, - PropertyModel -} from '../../../data/postgres/models'; -import { CustomError, ClientEntity } from '../../../domain'; -import { normalizePhone } from '../../../domain/utils/phone-normalization.util'; +import { + ClientModel, + ContactCategoryModel, + PropertyModel, +} from "../../../data/postgres/models"; +import { ClientEntity, CustomError } from "../../../domain"; +import { normalizePhone } from "../../../domain/utils/phone-normalization.util"; export class ClientCreationHelper { - + static async resolveContactCategory(categoryName: string): Promise { + const category = await ContactCategoryModel.findByName(categoryName); + if (!category || !category.id) { + throw CustomError.badRequest( + `Contact category "${categoryName}" not found. Please ensure the category exists in the database.`, + ); + } + return category.id; + } - static async resolveContactCategory(categoryName: string): Promise { - const category = await ContactCategoryModel.findByName(categoryName); - if (!category || !category.id) { - throw CustomError.badRequest( - `Contact category "${categoryName}" not found. Please ensure the category exists in the database.` - ); - } - return category.id; - } + static async validatePropertyExists(propertyId: number) { + const property = await PropertyModel.findById(propertyId); + if (!property) { + throw CustomError.badRequest(`Property with ID ${propertyId} not found`); + } + return property; + } - static async validatePropertyExists(propertyId: number) { - const property = await PropertyModel.findById(propertyId); - if (!property) { - throw CustomError.badRequest( - `Property with ID ${propertyId} not found` - ); - } - return property; - } + static async createBaseClient( + clientData: { + first_name: string; + last_name: string; + phone: string; + email?: string; + dni?: string; + address?: string; + notes?: string; + rental_interest?: boolean; + purchase_interest?: boolean; + }, + categoryId: number, + ): Promise<{ client: ClientEntity; wasCreated: boolean }> { + if (clientData.email) { + const existingByEmail = await ClientModel.findByEmail(clientData.email); + if (existingByEmail) { + throw CustomError.conflict( + `El cliente con email ${clientData.email} ya existe. No se permiten duplicados.`, + ); + } + } - - static async createBaseClient( - clientData: { - first_name: string; - last_name: string; - phone: string; - email?: string; - dni?: string; - address?: string; - notes?: string; - rental_interest?: boolean; - purchase_interest?: boolean; - }, - categoryId: number - ): Promise<{ client: ClientEntity; wasCreated: boolean }> { - if (clientData.email) { - const existingByEmail = await ClientModel.findByEmail(clientData.email); - if (existingByEmail) { - throw CustomError.conflict( - `El cliente con email ${clientData.email} ya existe. No se permiten duplicados.` - ); - } - } + if (clientData.dni) { + const existingByDni = await ClientModel.findByDni(clientData.dni); + if (existingByDni) { + throw CustomError.conflict( + `El cliente con DNI ${clientData.dni} ya existe. No se permiten duplicados.`, + ); + } + } - if (clientData.dni) { - const existingByDni = await ClientModel.findByDni(clientData.dni); - if (existingByDni) { - throw CustomError.conflict( - `El cliente con DNI ${clientData.dni} ya existe. No se permiten duplicados.` - ); - } - } + const normalizedPhone = normalizePhone(clientData.phone); + const clientsByPhone = await ClientModel.findByPhone(normalizedPhone); - const normalizedPhone = normalizePhone(clientData.phone); - const clientsByPhone = await ClientModel.findByPhone(normalizedPhone); - - if (clientsByPhone && clientsByPhone.length > 0) { - const exactMatch = clientsByPhone.find(c => - c.first_name === clientData.first_name && - c.last_name === clientData.last_name - ); - - if (exactMatch) { - throw CustomError.conflict( - `El cliente ${clientData.first_name} ${clientData.last_name} con teléfono ${normalizedPhone} ya existe. No se permiten duplicados.` - ); - } - } + if (clientsByPhone && clientsByPhone.length > 0) { + const exactMatch = clientsByPhone.find( + (c) => + c.first_name === clientData.first_name && + c.last_name === clientData.last_name, + ); - const newClient = await ClientModel.create({ - first_name: clientData.first_name, - last_name: clientData.last_name, - phone: normalizedPhone, - email: clientData.email, - dni: clientData.dni, - address: clientData.address, - notes: clientData.notes, - contact_category_id: categoryId, - rental_interest: clientData.rental_interest, - purchase_interest: clientData.purchase_interest, - }); + if (exactMatch) { + throw CustomError.conflict( + `El cliente ${clientData.first_name} ${clientData.last_name} con teléfono ${normalizedPhone} ya existe. No se permiten duplicados.`, + ); + } + } - if (!newClient.id) { - throw CustomError.internalServerError('Failed to create client'); - } + const newClient = await ClientModel.create({ + first_name: clientData.first_name, + last_name: clientData.last_name, + phone: normalizedPhone, + email: clientData.email, + dni: clientData.dni, + address: clientData.address, + notes: clientData.notes, + contact_category_id: categoryId, + rental_interest: clientData.rental_interest, + purchase_interest: clientData.purchase_interest, + }); - console.log(`New client created with ID: ${newClient.id}`); - return { client: ClientEntity.fromDatabaseObject(newClient), wasCreated: true }; - } -} + if (!newClient.id) { + throw CustomError.internalServerError("Failed to create client"); + } + console.log(`New client created with ID: ${newClient.id}`); + return { + client: ClientEntity.fromDatabaseObject(newClient), + wasCreated: true, + }; + } +} diff --git a/backend/src/presentation/services/lead.services.ts b/backend/src/presentation/services/lead.services.ts index 1fa8076..53fe508 100644 --- a/backend/src/presentation/services/lead.services.ts +++ b/backend/src/presentation/services/lead.services.ts @@ -1,59 +1,56 @@ -import { TransactionHelper } from '../../data/postgres/transaction.helper'; -import { - ClientPropertyInterestModel -} from '../../data/postgres/models'; -import { CreateLeadDto } from '../../domain/dtos/clients/create-lead.dto'; -import { ClientEntity } from '../../domain'; -import { ClientCreationHelper } from './helpers/client-creation.helper'; - +import { ClientPropertyInterestModel } from "../../data/postgres/models"; +import { TransactionHelper } from "../../data/postgres/transaction.helper"; +import type { CreateLeadDto } from "../../domain/dtos/clients/create-lead.dto"; +import { ClientCreationHelper } from "./helpers/client-creation.helper"; export class LeadServices { - - - async createLeadWithConsultation( - createLeadDto: CreateLeadDto - ) { - return await TransactionHelper.executeInTransaction(async () => { - const categoryId = await ClientCreationHelper.resolveContactCategory('Lead'); - - if (createLeadDto.property_id) { - await ClientCreationHelper.validatePropertyExists(createLeadDto.property_id); - } - - const { client: newClient } = await ClientCreationHelper.createBaseClient({ - first_name: createLeadDto.first_name, - last_name: createLeadDto.last_name, - phone: createLeadDto.phone, - email: createLeadDto.email, - notes: createLeadDto.notes, - }, categoryId); - - let propertyInterest = null; - if (createLeadDto.property_id && newClient.id) { - try { - propertyInterest = await ClientPropertyInterestModel.create({ - client_id: newClient.id, - property_id: createLeadDto.property_id, - notes: createLeadDto.notes || 'Propiedad de interés al crear Lead', - }); - } catch (error) { - console.log( - `Property interest already exists for client ${newClient.id} and property ${createLeadDto.property_id}` - ); - } - } - - const clientEntity = ClientEntity.fromDatabaseObject(newClient); - - return { - client: clientEntity.toPublicObject(), - property_interest: propertyInterest ? { - id: propertyInterest.id, - property_id: propertyInterest.property_id, - notes: propertyInterest.notes, - } : null, - }; - }); - } + async createLeadWithConsultation(createLeadDto: CreateLeadDto) { + return await TransactionHelper.executeInTransaction(async () => { + const categoryId = + await ClientCreationHelper.resolveContactCategory("Lead"); + + if (createLeadDto.property_id) { + await ClientCreationHelper.validatePropertyExists( + createLeadDto.property_id, + ); + } + + const { client: newClient } = await ClientCreationHelper.createBaseClient( + { + first_name: createLeadDto.first_name, + last_name: createLeadDto.last_name, + phone: createLeadDto.phone, + email: createLeadDto.email, + notes: createLeadDto.notes, + }, + categoryId, + ); + + let propertyInterest = null; + if (createLeadDto.property_id && newClient.id) { + try { + propertyInterest = await ClientPropertyInterestModel.create({ + client_id: newClient.id, + property_id: createLeadDto.property_id, + notes: createLeadDto.notes || "Propiedad de interés al crear Lead", + }); + } catch { + console.log( + `Property interest already exists for client ${newClient.id} and property ${createLeadDto.property_id}`, + ); + } + } + + return { + client: newClient.toPublicObject(), + property_interest: propertyInterest + ? { + id: propertyInterest.id, + property_id: propertyInterest.property_id, + notes: propertyInterest.notes, + } + : null, + }; + }); + } } - diff --git a/backend/src/presentation/services/owner.services.ts b/backend/src/presentation/services/owner.services.ts index f3749e3..313570e 100644 --- a/backend/src/presentation/services/owner.services.ts +++ b/backend/src/presentation/services/owner.services.ts @@ -1,66 +1,74 @@ -import { TransactionHelper } from '../../data/postgres/transaction.helper'; -import { - PropertyModel -} from '../../data/postgres/models'; -import { CreateOwnerDto } from '../../domain/dtos/clients/create-owner.dto'; -import { CustomError, ClientEntity } from '../../domain'; -import { ClientCreationHelper } from './helpers/client-creation.helper'; - +import { PropertyModel } from "../../data/postgres/models"; +import { TransactionHelper } from "../../data/postgres/transaction.helper"; +import { CustomError } from "../../domain"; +import type { CreateOwnerDto } from "../../domain/dtos/clients/create-owner.dto"; +import { ClientCreationHelper } from "./helpers/client-creation.helper"; export class OwnerServices { - - - async createOwnerWithProperty( - createOwnerDto: CreateOwnerDto - ) { - return await TransactionHelper.executeInTransaction(async () => { - const categoryId = await ClientCreationHelper.resolveContactCategory('Propietario'); + async createOwnerWithProperty(createOwnerDto: CreateOwnerDto) { + return await TransactionHelper.executeInTransaction(async () => { + const categoryId = + await ClientCreationHelper.resolveContactCategory("Propietario"); - let property = null; - let propertyAssociated = false; - if (createOwnerDto.property_id) { - property = await ClientCreationHelper.validatePropertyExists(createOwnerDto.property_id); - - if (property.owner_id) { - propertyAssociated = false; - } else { - propertyAssociated = true; - } - } - const { client: newClient, wasCreated } = await ClientCreationHelper.createBaseClient({ - first_name: createOwnerDto.first_name, - last_name: createOwnerDto.last_name, - phone: createOwnerDto.phone, - email: createOwnerDto.email, - dni: createOwnerDto.dni, - address: createOwnerDto.address, - notes: createOwnerDto.notes, - }, categoryId); + let property = null; + let propertyAssociated = false; + if (createOwnerDto.property_id) { + property = await ClientCreationHelper.validatePropertyExists( + createOwnerDto.property_id, + ); - if (createOwnerDto.property_id && property && propertyAssociated && newClient.id) { - const updatedProperty = await PropertyModel.update(createOwnerDto.property_id, { - owner_id: newClient.id - }); + if (property.owner_id) { + propertyAssociated = false; + } else { + propertyAssociated = true; + } + } + const { client: newClient } = await ClientCreationHelper.createBaseClient( + { + first_name: createOwnerDto.first_name, + last_name: createOwnerDto.last_name, + phone: createOwnerDto.phone, + email: createOwnerDto.email, + dni: createOwnerDto.dni, + address: createOwnerDto.address, + notes: createOwnerDto.notes, + }, + categoryId, + ); - if (!updatedProperty) { - throw CustomError.internalServerError('Failed to associate owner with property'); - } - } + if ( + createOwnerDto.property_id && + property && + propertyAssociated && + newClient.id + ) { + const updatedProperty = await PropertyModel.update( + createOwnerDto.property_id, + { + owner_id: newClient.id, + }, + ); - const clientEntity = ClientEntity.fromDatabaseObject(newClient); + if (!updatedProperty) { + throw CustomError.internalServerError( + "Failed to associate owner with property", + ); + } + } - return { - client: clientEntity.toPublicObject(), - property: property ? { - id: property.id, - title: property.title, - owner_id: propertyAssociated ? newClient.id : property.owner_id, - message: propertyAssociated - ? 'Owner successfully associated with property' - : 'Property already has an owner. Owner created but not associated. Edit the property to change the owner.' - } : null, - }; - }); - } + return { + client: newClient.toPublicObject(), + property: property + ? { + id: property.id, + title: property.title, + owner_id: propertyAssociated ? newClient.id : property.owner_id, + message: propertyAssociated + ? "Owner successfully associated with property" + : "Property already has an owner. Owner created but not associated. Edit the property to change the owner.", + } + : null, + }; + }); + } } - diff --git a/backend/src/presentation/services/property-consultation.services.ts b/backend/src/presentation/services/property-consultation.services.ts index b8cf9e6..9f310d4 100644 --- a/backend/src/presentation/services/property-consultation.services.ts +++ b/backend/src/presentation/services/property-consultation.services.ts @@ -5,15 +5,12 @@ import { ClientConsultationModel } from "../../data/postgres/models/crm/client-c import { ConsultationTypeModel } from "../../data/postgres/models/crm/consultation-type.model"; import { PropertyModel } from "../../data/postgres/models/properties/property.model"; import { CustomError } from "../../domain"; -import type { CreatePropertyConsultationDto } from "../../domain/dtos/consultations/create-property-consultation.dto"; import type { CreateGeneralConsultationDto } from "../../domain/dtos/consultations/create-general-consultation.dto"; +import type { CreatePropertyConsultationDto } from "../../domain/dtos/consultations/create-property-consultation.dto"; +import type { ConsultationQueryRow } from "../../domain/interfaces/enriched-data"; import { ClientCreationHelper } from "./helpers/client-creation.helper"; -import type { ConsultationRow } from '../../domain/interfaces/database-rows'; -import type { ConsultationQueryRow } from '../../domain/interfaces/enriched-data'; export class PropertyConsultationServices { - constructor() {} - async createPropertyConsultation(dto: CreatePropertyConsultationDto) { try { const property = await PropertyModel.findById(dto.property_id); @@ -110,7 +107,7 @@ export class PropertyConsultationServices { const dbClient = PostgresDatabase.getClient(); let query = ` - SELECT + SELECT cc.id, cc.consultation_date, cc.message, @@ -222,7 +219,7 @@ export class PropertyConsultationServices { countQuery, values.slice(0, values.length - 2), ); - const total = parseInt(countResult.rows[0].total); + const total = parseInt(countResult.rows[0].total, 10); return { consultations, @@ -418,10 +415,14 @@ export class PropertyConsultationServices { console.log(`Created new lead with ID: ${client.id}`); } + if (!client.id) { + throw CustomError.internalServerError("Client creation failed: No ID"); + } + const updatedConsultation = await ClientConsultationModel.update( consultationId, { - client_id: client.id!, + client_id: client.id, assigned_user_id: assignedUserId, }, ); @@ -430,12 +431,12 @@ export class PropertyConsultationServices { if (consultation.property_id) { try { await ClientPropertyInterestModel.create({ - client_id: client.id!, + client_id: client.id, property_id: consultation.property_id, notes: `Converted from consultation #${consultationId}`, }); propertyInterestCreated = true; - } catch (error) { + } catch { console.log( `Property interest already exists for client ${client.id} and property ${consultation.property_id}`, ); @@ -466,7 +467,6 @@ export class PropertyConsultationServices { } } - async createGeneralConsultation(dto: CreateGeneralConsultationDto) { try { const consultationType = diff --git a/backend/src/presentation/services/property.services.ts b/backend/src/presentation/services/property.services.ts index bee9eb2..d3849bb 100644 --- a/backend/src/presentation/services/property.services.ts +++ b/backend/src/presentation/services/property.services.ts @@ -25,9 +25,9 @@ import { RentalModel, VisibilityStatusModel, } from "../../data/postgres/models"; +import type { PropertyPrice } from "../../data/postgres/models/properties/property-price.model"; import type { PropertyService } from "../../data/postgres/models/properties/property-service.model"; import type { Expense } from "../../data/postgres/models/shared/expense.model"; -import type { PropertyPrice } from "../../data/postgres/models/properties/property-price.model"; import { TransactionHelper } from "../../data/postgres/transaction.helper"; import { CustomError } from "../../domain"; import type { @@ -35,21 +35,16 @@ import type { CreatePropertyGroupedDto, UpdatePropertyGroupedDto, } from "../../domain/dtos/properties"; -import type { FileUploadAdapter } from "../../domain/interfaces/file-upload.adapter"; -import type { - PropertyPriceRow, - PropertyAddressRow, - PropertyServiceRow, - ExpenseRow, - PropertyRow -} from '../../domain/interfaces/database-rows'; import type { - EnrichedPropertyPrice, + PropertyAddressRow, + PropertyPriceRow, +} from "../../domain/interfaces/database-rows"; +import type { EnrichedPropertyAddress, - EnrichedExpense, - PropertyListItem -} from '../../domain/interfaces/enriched-data'; - + EnrichedPropertyPrice, + PropertyListItem, +} from "../../domain/interfaces/enriched-data"; +import type { FileUploadAdapter } from "../../domain/interfaces/file-upload.adapter"; export class PropertyServices { constructor(private readonly fileUploadAdapter: FileUploadAdapter) {} @@ -67,13 +62,13 @@ export class PropertyServices { createPropertyDto.owner_id !== null ) { const ownerIdNumber = Number(createPropertyDto.owner_id); - - if (isNaN(ownerIdNumber) || ownerIdNumber <= 0) { + + if (Number.isNaN(ownerIdNumber) || ownerIdNumber <= 0) { throw CustomError.badRequest( `Invalid owner_id: must be a valid positive number`, ); } - + const ownerClient = await ClientModel.findById(ownerIdNumber); if (!ownerClient) { throw CustomError.badRequest( @@ -91,7 +86,7 @@ export class PropertyServices { await this.checkDuplicateAddress( geography.cityId, createPropertyDto.address.street, - createPropertyDto.address.number + createPropertyDto.address.number, ); const fullAddress = this.buildFullAddress(createPropertyDto.address); @@ -220,9 +215,13 @@ export class PropertyServices { throw CustomError.internalServerError("Failed to create property"); } + if (!address.id) { + throw CustomError.internalServerError("Failed to create address"); + } + await PropertyAddressModel.create({ property_id: property.id, - address_id: address.id!, + address_id: address.id, }); const prices = []; @@ -293,7 +292,8 @@ export class PropertyServices { multimediaRecords.push(multimedia); } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; for (const url of uploadedImages) { try { await this.fileUploadAdapter.deleteFile(url); @@ -310,17 +310,13 @@ export class PropertyServices { } } - const [ - propertyType, - propertyStatus, - visibilityStatus, - addressCity, - ] = await Promise.all([ - PropertyTypeModel.findById(propertyTypeId), - PropertyStatusModel.findById(propertyStatusId), - VisibilityStatusModel.findById(visibilityStatusId), - CityModel.findById(geography.cityId), - ]); + const [propertyType, propertyStatus, visibilityStatus, addressCity] = + await Promise.all([ + PropertyTypeModel.findById(propertyTypeId), + PropertyStatusModel.findById(propertyStatusId), + VisibilityStatusModel.findById(visibilityStatusId), + CityModel.findById(geography.cityId), + ]); const addressProvince = addressCity ? await ProvinceModel.findById(addressCity.province_id) @@ -632,9 +628,13 @@ export class PropertyServices { if (property.owner_id) { const ownerClient = await ClientModel.findById(property.owner_id); if (ownerClient) { - const { ContactCategoryModel } = await import('../../data/postgres/models/clients/contact-category.model'); - const category = await ContactCategoryModel.findById(ownerClient.contact_category_id); - + const { ContactCategoryModel } = await import( + "../../data/postgres/models/clients/contact-category.model" + ); + const category = await ContactCategoryModel.findById( + ownerClient.contact_category_id, + ); + owner = { id: ownerClient.id, first_name: ownerClient.first_name, @@ -644,10 +644,12 @@ export class PropertyServices { phone: ownerClient.phone || null, dni: ownerClient.dni || null, address: ownerClient.address || null, - contact_category: category ? { - id: category.id, - name: category.name - } : null + contact_category: category + ? { + id: category.id, + name: category.name, + } + : null, }; } } @@ -689,7 +691,7 @@ export class PropertyServices { const clientRental = await ClientRentalModel.findById( rental.client_rental_id, ); - if (clientRental && clientRental.client_id) { + if (clientRental?.client_id) { const tenant = await ClientModel.findById(clientRental.client_id); if (tenant) { const rentalCurrency = await CurrencyTypeModel.findById( @@ -1029,17 +1031,24 @@ export class PropertyServices { } return { message: "Property deleted successfully" }; } catch (error: unknown) { - if (error && typeof error === 'object' && 'code' in error && error.code === "23503") { + if ( + error && + typeof error === "object" && + "code" in error && + error.code === "23503" + ) { throw CustomError.badRequest( "Cannot delete property: it has active rentals or sales. Archive it instead.", ); } - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw CustomError.internalServerError(`Error updating property: ${errorMessage}`); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + throw CustomError.internalServerError( + `Error updating property: ${errorMessage}`, + ); } } - async createPropertyGrouped( createPropertyGroupedDto: CreatePropertyGroupedDto, capturedByUserId: number, @@ -1060,17 +1069,27 @@ export class PropertyServices { } = createPropertyGroupedDto; let finalOwnerId: number | undefined; - console.log('[PropertyServices] basic.owner_id:', basic.owner_id, 'type:', typeof basic.owner_id); + console.log( + "[PropertyServices] basic.owner_id:", + basic.owner_id, + "type:", + typeof basic.owner_id, + ); if (basic.owner_id !== undefined && basic.owner_id !== null) { const ownerIdNumber = Number(basic.owner_id); - console.log('[PropertyServices] ownerIdNumber after conversion:', ownerIdNumber, 'isNaN:', isNaN(ownerIdNumber)); - - if (isNaN(ownerIdNumber) || ownerIdNumber <= 0) { + console.log( + "[PropertyServices] ownerIdNumber after conversion:", + ownerIdNumber, + "isNaN:", + Number.isNaN(ownerIdNumber), + ); + + if (Number.isNaN(ownerIdNumber) || ownerIdNumber <= 0) { throw CustomError.badRequest( `Invalid owner_id: must be a valid positive number`, ); } - + const ownerClient = await ClientModel.findById(ownerIdNumber); if (!ownerClient) { throw CustomError.badRequest( @@ -1078,9 +1097,9 @@ export class PropertyServices { ); } finalOwnerId = ownerIdNumber; - console.log('[PropertyServices] finalOwnerId set to:', finalOwnerId); + console.log("[PropertyServices] finalOwnerId set to:", finalOwnerId); } else { - console.log('[PropertyServices] owner_id is undefined or null'); + console.log("[PropertyServices] owner_id is undefined or null"); } const geography = await this.resolveGeography(geoData); @@ -1088,7 +1107,7 @@ export class PropertyServices { await this.checkDuplicateAddress( geography.cityId, addrData.street, - addrData.number + addrData.number, ); const fullAddress = this.buildFullAddressFromGrouped(addrData); @@ -1173,7 +1192,12 @@ export class PropertyServices { } } - console.log('[PropertyServices] Creating property with finalOwnerId:', finalOwnerId, 'type:', typeof finalOwnerId); + console.log( + "[PropertyServices] Creating property with finalOwnerId:", + finalOwnerId, + "type:", + typeof finalOwnerId, + ); const propertyDataToInsert = { title: basic.title, description: basic.description, @@ -1213,16 +1237,25 @@ export class PropertyServices { producer_commission_percentage: internal?.producer_commission_percentage, }; - console.log('[PropertyServices] propertyDataToInsert.owner_id:', propertyDataToInsert.owner_id, 'type:', typeof propertyDataToInsert.owner_id); + console.log( + "[PropertyServices] propertyDataToInsert.owner_id:", + propertyDataToInsert.owner_id, + "type:", + typeof propertyDataToInsert.owner_id, + ); const property = await PropertyModel.create(propertyDataToInsert); if (!property.id) { throw CustomError.internalServerError("Failed to create property"); } + if (!address.id) { + throw CustomError.internalServerError("Failed to create address"); + } + await PropertyAddressModel.create({ property_id: property.id, - address_id: address.id!, + address_id: address.id, }); const prices = []; @@ -1298,11 +1331,7 @@ export class PropertyServices { } } - if ( - servicesData && - servicesData.services && - servicesData.services.length > 0 - ) { + if (servicesData?.services && servicesData.services.length > 0) { for (const serviceName of servicesData.services) { if (!serviceName || !serviceName.trim()) continue; @@ -1378,7 +1407,8 @@ export class PropertyServices { console.error("Error deleting uploaded image:", deleteError); } } - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; throw CustomError.internalServerError( `Error uploading images: ${errorMessage}`, ); @@ -1403,14 +1433,13 @@ export class PropertyServices { ); } - const documentName = - documentNames && documentNames[i] - ? documentNames[i].trim() - : document.originalname?.replace(/\.[^/.]+$/, "") || - `Document ${i + 1}`; + const documentName = documentNames?.[i] + ? documentNames[i].trim() + : document.originalname?.replace(/\.[^/.]+$/, "") || + `Document ${i + 1}`; const publicId = `${documentName}.pdf`; - + const documentUrl = await this.fileUploadAdapter.uploadFile( document.buffer, { @@ -1438,24 +1467,21 @@ export class PropertyServices { console.error("Error deleting uploaded document:", deleteError); } } - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; throw CustomError.internalServerError( `Error uploading documents: ${errorMessage}`, ); } } - const [ - propertyType, - propertyStatus, - visibilityStatus, - addressCity, - ] = await Promise.all([ - PropertyTypeModel.findById(propertyTypeId), - PropertyStatusModel.findById(propertyStatusId), - VisibilityStatusModel.findById(visibilityStatusId), - CityModel.findById(geography.cityId), - ]); + const [propertyType, propertyStatus, visibilityStatus, addressCity] = + await Promise.all([ + PropertyTypeModel.findById(propertyTypeId), + PropertyStatusModel.findById(propertyStatusId), + VisibilityStatusModel.findById(visibilityStatusId), + CityModel.findById(geography.cityId), + ]); const addressProvince = addressCity ? await ProvinceModel.findById(addressCity.province_id) @@ -1617,7 +1643,7 @@ export class PropertyServices { prices: enrichedPrices, expenses: enrichedExpenses, services: enrichedServices.filter((s) => s !== null), - + images: multimediaRecords, documents: documentRecords, }, @@ -1654,13 +1680,13 @@ export class PropertyServices { let finalOwnerId: number | undefined = existingProperty.owner_id; if (basic?.owner_id !== undefined && basic.owner_id !== null) { const ownerIdNumber = Number(basic.owner_id); - - if (isNaN(ownerIdNumber) || ownerIdNumber <= 0) { + + if (Number.isNaN(ownerIdNumber) || ownerIdNumber <= 0) { throw CustomError.badRequest( `Invalid owner_id: must be a valid positive number`, ); } - + const ownerClient = await ClientModel.findById(ownerIdNumber); if (!ownerClient) { throw CustomError.badRequest( @@ -1673,35 +1699,48 @@ export class PropertyServices { let address = null; if (geoData && addrData) { const geography = await this.resolveGeography(geoData); - + await this.checkDuplicateAddress( geography.cityId, addrData.street, addrData.number, - id + id, ); - + const fullAddress = this.buildFullAddressFromGrouped( - addrData as CreatePropertyGroupedDto["address"] + addrData as CreatePropertyGroupedDto["address"], ); - - const { PropertyAddressModel } = await import('../../data/postgres/models/properties/property-address.model'); - const existingPropertyAddresses = await PropertyAddressModel.findByPropertyId(id); - - if (existingPropertyAddresses.length > 0 && existingPropertyAddresses[0].address_id) { - const { AddressModel } = await import('../../data/postgres/models/properties/address.model'); - address = await AddressModel.update(existingPropertyAddresses[0].address_id, { - street: addrData.street, - number: addrData.number || undefined, - full_address: fullAddress, - neighborhood: addrData.neighborhood, - postal_code: addrData.postal_code, - latitude: addrData.latitude, - longitude: addrData.longitude, - city_id: geography.cityId, - }); + + const { PropertyAddressModel } = await import( + "../../data/postgres/models/properties/property-address.model" + ); + const existingPropertyAddresses = + await PropertyAddressModel.findByPropertyId(id); + + if ( + existingPropertyAddresses.length > 0 && + existingPropertyAddresses[0].address_id + ) { + const { AddressModel } = await import( + "../../data/postgres/models/properties/address.model" + ); + address = await AddressModel.update( + existingPropertyAddresses[0].address_id, + { + street: addrData.street, + number: addrData.number || undefined, + full_address: fullAddress, + neighborhood: addrData.neighborhood, + postal_code: addrData.postal_code, + latitude: addrData.latitude, + longitude: addrData.longitude, + city_id: geography.cityId, + }, + ); } else { - const { AddressModel } = await import('../../data/postgres/models/properties/address.model'); + const { AddressModel } = await import( + "../../data/postgres/models/properties/address.model" + ); address = await AddressModel.create({ street: addrData.street, number: addrData.number || undefined, @@ -1712,10 +1751,14 @@ export class PropertyServices { longitude: addrData.longitude, city_id: geography.cityId, }); - + + if (!address.id) { + throw CustomError.internalServerError("Failed to create address"); + } + await PropertyAddressModel.create({ property_id: id, - address_id: address.id!, + address_id: address.id, }); } } @@ -1796,54 +1839,85 @@ export class PropertyServices { const updateData: Record = {}; if (basic?.title) updateData.title = basic.title; - if (basic?.description !== undefined) updateData.description = basic.description; - if (basic?.featured_web !== undefined) updateData.featured_web = basic.featured_web; + if (basic?.description !== undefined) + updateData.description = basic.description; + if (basic?.featured_web !== undefined) + updateData.featured_web = basic.featured_web; if (basic?.publication_date !== undefined) { - updateData.publication_date = typeof basic.publication_date === 'string' - ? new Date(basic.publication_date) - : basic.publication_date; + updateData.publication_date = + typeof basic.publication_date === "string" + ? new Date(basic.publication_date) + : basic.publication_date; } - if (propertyTypeId !== existingProperty.property_type_id) updateData.property_type_id = propertyTypeId; - if (propertyStatusId !== existingProperty.property_status_id) updateData.property_status_id = propertyStatusId; - if (visibilityStatusId !== existingProperty.visibility_status_id) updateData.visibility_status_id = visibilityStatusId; - if (finalOwnerId !== existingProperty.owner_id) updateData.owner_id = finalOwnerId; - - if (characteristics?.bedrooms_count !== undefined) updateData.bedrooms_count = characteristics.bedrooms_count; - if (characteristics?.bathrooms_count !== undefined) updateData.bathrooms_count = characteristics.bathrooms_count; - if (characteristics?.rooms_count !== undefined) updateData.rooms_count = characteristics.rooms_count; - if (characteristics?.toilets_count !== undefined) updateData.toilets_count = characteristics.toilets_count; - if (characteristics?.parking_spaces_count !== undefined) updateData.parking_spaces_count = characteristics.parking_spaces_count; - if (characteristics?.floors_count !== undefined) updateData.floors_count = characteristics.floors_count; - if (situationId !== existingProperty.situation_id) updateData.situation_id = situationId; + if (propertyTypeId !== existingProperty.property_type_id) + updateData.property_type_id = propertyTypeId; + if (propertyStatusId !== existingProperty.property_status_id) + updateData.property_status_id = propertyStatusId; + if (visibilityStatusId !== existingProperty.visibility_status_id) + updateData.visibility_status_id = visibilityStatusId; + if (finalOwnerId !== existingProperty.owner_id) + updateData.owner_id = finalOwnerId; + + if (characteristics?.bedrooms_count !== undefined) + updateData.bedrooms_count = characteristics.bedrooms_count; + if (characteristics?.bathrooms_count !== undefined) + updateData.bathrooms_count = characteristics.bathrooms_count; + if (characteristics?.rooms_count !== undefined) + updateData.rooms_count = characteristics.rooms_count; + if (characteristics?.toilets_count !== undefined) + updateData.toilets_count = characteristics.toilets_count; + if (characteristics?.parking_spaces_count !== undefined) + updateData.parking_spaces_count = characteristics.parking_spaces_count; + if (characteristics?.floors_count !== undefined) + updateData.floors_count = characteristics.floors_count; + if (situationId !== existingProperty.situation_id) + updateData.situation_id = situationId; if (ageId !== existingProperty.age_id) updateData.age_id = ageId; - if (orientationId !== existingProperty.orientation_id) updateData.orientation_id = orientationId; - if (dispositionId !== existingProperty.disposition_id) updateData.disposition_id = dispositionId; - - if (surface?.land_area !== undefined) updateData.land_area = surface.land_area; - if (surface?.semi_covered_area !== undefined) updateData.semi_covered_area = surface.semi_covered_area; - if (surface?.covered_area !== undefined) updateData.covered_area = surface.covered_area; - if (surface?.total_built_area !== undefined) updateData.total_built_area = surface.total_built_area; - if (surface?.uncovered_area !== undefined) updateData.uncovered_area = surface.uncovered_area; - if (surface?.total_area !== undefined) updateData.total_area = surface.total_area; + if (orientationId !== existingProperty.orientation_id) + updateData.orientation_id = orientationId; + if (dispositionId !== existingProperty.disposition_id) + updateData.disposition_id = dispositionId; + + if (surface?.land_area !== undefined) + updateData.land_area = surface.land_area; + if (surface?.semi_covered_area !== undefined) + updateData.semi_covered_area = surface.semi_covered_area; + if (surface?.covered_area !== undefined) + updateData.covered_area = surface.covered_area; + if (surface?.total_built_area !== undefined) + updateData.total_built_area = surface.total_built_area; + if (surface?.uncovered_area !== undefined) + updateData.uncovered_area = surface.uncovered_area; + if (surface?.total_area !== undefined) + updateData.total_area = surface.total_area; if (surface?.zoning !== undefined) updateData.zoning = surface.zoning; - if (internal?.branch_name !== undefined) updateData.branch_name = internal.branch_name; - if (internal?.appraiser !== undefined) updateData.appraiser = internal.appraiser; - if (internal?.producer !== undefined) updateData.producer = internal.producer; - if (internal?.maintenance_user !== undefined) updateData.maintenance_user = internal.maintenance_user; - if (internal?.keys_location !== undefined) updateData.keys_location = internal.keys_location; - if (internal?.internal_comments !== undefined) updateData.internal_comments = internal.internal_comments; - if (internal?.social_media_info !== undefined) updateData.social_media_info = internal.social_media_info; - if (internal?.operation_commission_percentage !== undefined) updateData.operation_commission_percentage = internal.operation_commission_percentage; - if (internal?.producer_commission_percentage !== undefined) updateData.producer_commission_percentage = internal.producer_commission_percentage; - - let property = existingProperty; + if (internal?.branch_name !== undefined) + updateData.branch_name = internal.branch_name; + if (internal?.appraiser !== undefined) + updateData.appraiser = internal.appraiser; + if (internal?.producer !== undefined) + updateData.producer = internal.producer; + if (internal?.maintenance_user !== undefined) + updateData.maintenance_user = internal.maintenance_user; + if (internal?.keys_location !== undefined) + updateData.keys_location = internal.keys_location; + if (internal?.internal_comments !== undefined) + updateData.internal_comments = internal.internal_comments; + if (internal?.social_media_info !== undefined) + updateData.social_media_info = internal.social_media_info; + if (internal?.operation_commission_percentage !== undefined) + updateData.operation_commission_percentage = + internal.operation_commission_percentage; + if (internal?.producer_commission_percentage !== undefined) + updateData.producer_commission_percentage = + internal.producer_commission_percentage; + if (Object.keys(updateData).length > 0) { const updated = await PropertyModel.update(id, updateData); if (!updated) { throw CustomError.internalServerError("Failed to update property"); } - property = updated; } const newPrices = []; @@ -1851,79 +1925,97 @@ export class PropertyServices { for (const priceDto of values.prices) { let currencyTypeId = priceDto.currency_type_id; if (!currencyTypeId && priceDto.currency_symbol) { - const currencyType = await CurrencyTypeModel.findBySymbol(priceDto.currency_symbol); - if (!currencyType || !currencyType.id) { - throw CustomError.badRequest(`Currency symbol "${priceDto.currency_symbol}" not found`); + const currencyType = await CurrencyTypeModel.findBySymbol( + priceDto.currency_symbol, + ); + if (!currencyType || !currencyType.id) { + throw CustomError.badRequest( + `Currency symbol "${priceDto.currency_symbol}" not found`, + ); + } + currencyTypeId = currencyType.id; } - currencyTypeId = currencyType.id; - } - let operationTypeId = priceDto.operation_type_id; - if (!operationTypeId && priceDto.operation_type) { - const operationType = await PropertyOperationTypeModel.findByName(priceDto.operation_type); - if (!operationType || !operationType.id) { - throw CustomError.badRequest(`Operation type "${priceDto.operation_type}" not found`); + let operationTypeId = priceDto.operation_type_id; + if (!operationTypeId && priceDto.operation_type) { + const operationType = await PropertyOperationTypeModel.findByName( + priceDto.operation_type, + ); + if (!operationType || !operationType.id) { + throw CustomError.badRequest( + `Operation type "${priceDto.operation_type}" not found`, + ); + } + operationTypeId = operationType.id; } - operationTypeId = operationType.id; - } - - if (!currencyTypeId || !operationTypeId) { - throw CustomError.badRequest("Failed to resolve currency or operation type"); - } - const existingPrice = await PropertyPriceModel.findCurrentByPropertyAndOperation( - id, - operationTypeId - ); - - if (existingPrice && existingPrice.id) { - if (userId && existingPrice.price !== priceDto.price) { - await PriceHistoryModel.create({ - property_id: id, - previous_price: existingPrice.price, - new_price: priceDto.price, - currency_type_id: currencyTypeId, - operation_type_id: operationTypeId, - responsible_user_id: userId, - }); + if (!currencyTypeId || !operationTypeId) { + throw CustomError.badRequest( + "Failed to resolve currency or operation type", + ); } - const updatedPrice = await PropertyPriceModel.update(existingPrice.id, { - price: priceDto.price, - currency_type_id: currencyTypeId, - }); - newPrices.push(updatedPrice); - } else { - const price = await PropertyPriceModel.create({ - property_id: id, - price: priceDto.price, - currency_type_id: currencyTypeId, - operation_type_id: operationTypeId, - }); - newPrices.push(price); + const existingPrice = + await PropertyPriceModel.findCurrentByPropertyAndOperation( + id, + operationTypeId, + ); + + if (existingPrice?.id) { + if (userId && existingPrice.price !== priceDto.price) { + await PriceHistoryModel.create({ + property_id: id, + previous_price: existingPrice.price, + new_price: priceDto.price, + currency_type_id: currencyTypeId, + operation_type_id: operationTypeId, + responsible_user_id: userId, + }); + } - if (userId) { - await PriceHistoryModel.create({ + const updatedPrice = await PropertyPriceModel.update( + existingPrice.id, + { + price: priceDto.price, + currency_type_id: currencyTypeId, + }, + ); + newPrices.push(updatedPrice); + } else { + const price = await PropertyPriceModel.create({ property_id: id, - previous_price: undefined, - new_price: priceDto.price, + price: priceDto.price, currency_type_id: currencyTypeId, operation_type_id: operationTypeId, - responsible_user_id: userId, }); + newPrices.push(price); + + if (userId) { + await PriceHistoryModel.create({ + property_id: id, + previous_price: undefined, + new_price: priceDto.price, + currency_type_id: currencyTypeId, + operation_type_id: operationTypeId, + responsible_user_id: userId, + }); + } } } } - } const newExpenses = []; if (values?.expenses && values.expenses.length > 0) { for (const expenseDto of values.expenses) { let currencyTypeId = expenseDto.currency_type_id; if (!currencyTypeId && expenseDto.currency_symbol) { - const currencyType = await CurrencyTypeModel.findBySymbol(expenseDto.currency_symbol); + const currencyType = await CurrencyTypeModel.findBySymbol( + expenseDto.currency_symbol, + ); if (!currencyType || !currencyType.id) { - throw CustomError.badRequest(`Currency symbol "${expenseDto.currency_symbol}" not found for expense`); + throw CustomError.badRequest( + `Currency symbol "${expenseDto.currency_symbol}" not found for expense`, + ); } currencyTypeId = currencyType.id; } @@ -1942,11 +2034,13 @@ export class PropertyServices { } } - if (servicesData && servicesData.services && servicesData.services.length > 0) { + if (servicesData?.services && servicesData.services.length > 0) { for (const serviceName of servicesData.services) { if (!serviceName || !serviceName.trim()) continue; - let service = await CatalogServiceModel.findByName(serviceName.trim()); + let service = await CatalogServiceModel.findByName( + serviceName.trim(), + ); if (!service) { service = await CatalogServiceModel.create({ name: serviceName.trim(), @@ -1954,7 +2048,9 @@ export class PropertyServices { } if (!service || !service.id) { - throw CustomError.internalServerError(`Failed to resolve service "${serviceName}"`); + throw CustomError.internalServerError( + `Failed to resolve service "${serviceName}"`, + ); } await PropertyServiceModel.create({ @@ -1970,10 +2066,13 @@ export class PropertyServices { try { for (let i = 0; i < images.length; i++) { const image = images[i]; - const imageUrl = await this.fileUploadAdapter.uploadFile(image.buffer, { - folder: `properties/${id}`, - resourceType: "image", - }); + const imageUrl = await this.fileUploadAdapter.uploadFile( + image.buffer, + { + folder: `properties/${id}`, + resourceType: "image", + }, + ); uploadedImages.push(imageUrl); const multimedia = await PropertyMultimediaModel.create({ @@ -1985,7 +2084,8 @@ export class PropertyServices { multimediaRecords.push(multimedia); } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; for (const url of uploadedImages) { try { await this.fileUploadAdapter.deleteFile(url); @@ -1993,7 +2093,9 @@ export class PropertyServices { console.error("Error deleting uploaded image:", deleteError); } } - throw CustomError.internalServerError(`Error uploading images: ${errorMessage}`); + throw CustomError.internalServerError( + `Error uploading images: ${errorMessage}`, + ); } } @@ -2005,33 +2107,40 @@ export class PropertyServices { const document = documents[i]; if (document.mimetype !== "application/pdf") { - throw CustomError.badRequest(`Document ${i + 1} must be a PDF file`); + throw CustomError.badRequest( + `Document ${i + 1} must be a PDF file`, + ); } - const documentName = documentNames && documentNames[i] + const documentName = documentNames?.[i] ? documentNames[i].trim() - : document.originalname?.replace(/\.[^/.]+$/, "") || `Document ${i + 1}`; + : document.originalname?.replace(/\.[^/.]+$/, "") || + `Document ${i + 1}`; const publicId = `${documentName}.pdf`; - - const documentUrl = await this.fileUploadAdapter.uploadFile(document.buffer, { - folder: `properties/${id}/documents`, - resourceType: "raw", - mimeType: "application/pdf", - publicId: publicId, - }); + + const documentUrl = await this.fileUploadAdapter.uploadFile( + document.buffer, + { + folder: `properties/${id}/documents`, + resourceType: "raw", + mimeType: "application/pdf", + publicId: publicId, + }, + ); uploadedDocuments.push(documentUrl); const docRecord = await PropertyDocumentModel.create({ property_id: id, client_id: finalOwnerId || undefined, - document_name: documentName, + document_name: documentName, file_path: documentUrl, }); documentRecords.push(docRecord); } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; for (const url of uploadedDocuments) { try { await this.fileUploadAdapter.deleteFile(url); @@ -2039,25 +2148,30 @@ export class PropertyServices { console.error("Error deleting uploaded document:", deleteError); } } - throw CustomError.internalServerError(`Error uploading documents: ${errorMessage}`); + throw CustomError.internalServerError( + `Error uploading documents: ${errorMessage}`, + ); } } if (imageOrder && imageOrder.length > 0) { - const existingImages = await PropertyMultimediaModel.findByPropertyId(id); - const existingImageIds = existingImages.map(img => img.id).filter(id => id !== undefined) as number[]; - + const existingImages = + await PropertyMultimediaModel.findByPropertyId(id); + const existingImageIds = existingImages + .map((img) => img.id) + .filter((id) => id !== undefined) as number[]; + for (const orderItem of imageOrder) { if (!existingImageIds.includes(orderItem.id)) { throw CustomError.badRequest( - `Image with ID ${orderItem.id} does not belong to this property` + `Image with ID ${orderItem.id} does not belong to this property`, ); } } await PropertyMultimediaModel.clearPrimaryForProperty(id); - let primaryImageId: number | undefined = undefined; + let primaryImageId: number | undefined; for (let i = 0; i < imageOrder.length; i++) { const orderItem = imageOrder[i]; if (i === 0 || orderItem.is_primary === true) { @@ -2075,7 +2189,6 @@ export class PropertyServices { }); } - private buildFullAddressFromGrouped( address: CreatePropertyGroupedDto["address"], ): string { @@ -2156,7 +2269,7 @@ export class PropertyServices { cityId: number, street: string, number?: string, - excludePropertyId?: number + excludePropertyId?: number, ): Promise { if (!street || !street.trim()) { return; @@ -2164,46 +2277,51 @@ export class PropertyServices { const normalizedStreet = this.normalizeStreet(street); const normalizedNumber = number ? number.trim() : null; - + const existingAddresses = await AddressModel.findByCityId(cityId); - + for (const existingAddr of existingAddresses) { - let existingStreet = existingAddr.street || ''; + let existingStreet = existingAddr.street || ""; let existingNumber = existingAddr.number || null; - + if (!existingStreet && existingAddr.full_address) { const parsed = this.parseFullAddress(existingAddr.full_address); existingStreet = parsed.street; existingNumber = parsed.number; } - + if (!existingStreet) continue; - + const normalizedExistingStreet = this.normalizeStreet(existingStreet); - + const streetsMatch = normalizedExistingStreet === normalizedStreet; - const numbersMatch = this.compareNumbers(normalizedNumber, existingNumber); - + const numbersMatch = this.compareNumbers( + normalizedNumber, + existingNumber, + ); + if (streetsMatch && numbersMatch) { if (!existingAddr.id) continue; - - const propertyAddresses = await PropertyAddressModel.findByAddressId(existingAddr.id); - + + const propertyAddresses = await PropertyAddressModel.findByAddressId( + existingAddr.id, + ); + for (const propAddr of propertyAddresses) { if (excludePropertyId && propAddr.property_id === excludePropertyId) { continue; } - + const property = await PropertyModel.findById( propAddr.property_id, - false + false, ); - + if (property) { throw CustomError.badRequest( `Ya existe una propiedad activa en esta dirección: ${existingAddr.full_address}. ` + - `Si esta es una propiedad diferente (por ejemplo, un departamento en el mismo edificio), ` + - `asegúrate de que el número incluya el piso/departamento (ej: "4A", "4B").` + `Si esta es una propiedad diferente (por ejemplo, un departamento en el mismo edificio), ` + + `asegúrate de que el número incluya el piso/departamento (ej: "4A", "4B").`, ); } } @@ -2212,47 +2330,56 @@ export class PropertyServices { } private normalizeStreet(street: string): string { - if (!street) return ''; - + if (!street) return ""; + return street .toLowerCase() .trim() - .replace(/\s+/g, ' ') - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .replace(/\bav\.?\s*/g, 'avenida ') - .replace(/\bcalle\s+/g, 'calle ') - .replace(/\bbv\.?\s*/g, 'boulevard ') - .replace(/\bpje\.?\s*/g, 'pasaje '); + .replace(/\s+/g, " ") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/\bav\.?\s*/g, "avenida ") + .replace(/\bcalle\s+/g, "calle ") + .replace(/\bbv\.?\s*/g, "boulevard ") + .replace(/\bpje\.?\s*/g, "pasaje "); } - private parseFullAddress(fullAddress: string): { street: string; number: string | null } { - if (!fullAddress) return { street: '', number: null }; - + private parseFullAddress(fullAddress: string): { + street: string; + number: string | null; + } { + if (!fullAddress) return { street: "", number: null }; + const trimmed = fullAddress.trim(); - + const parts = trimmed.split(/\s+/); const numberParts: string[] = []; let foundNumber = false; - let streetParts: string[] = []; - + const streetParts: string[] = []; + for (let i = 0; i < parts.length; i++) { const part = parts[i]; - + if (!foundNumber && /^\d/.test(part)) { foundNumber = true; numberParts.push(part); - + if (i + 1 < parts.length) { const nextPart = parts[i + 1]; - if (/^[A-Za-z0-9]+$/.test(nextPart) && !/^[A-Z][a-z]+$/.test(nextPart)) { + if ( + /^[A-Za-z0-9]+$/.test(nextPart) && + !/^[A-Z][a-z]+$/.test(nextPart) + ) { numberParts.push(nextPart); i++; } } } else if (foundNumber && numberParts.length > 0) { const nextPart = part; - if (/^[0-9A-Za-z]+$/.test(nextPart) && !/^[A-Z][a-z]+$/.test(nextPart)) { + if ( + /^[0-9A-Za-z]+$/.test(nextPart) && + !/^[A-Z][a-z]+$/.test(nextPart) + ) { numberParts.push(nextPart); } else { break; @@ -2261,29 +2388,29 @@ export class PropertyServices { streetParts.push(part); } } - + if (foundNumber && numberParts.length > 0) { return { - street: streetParts.join(' '), - number: numberParts.join(' ') + street: streetParts.join(" "), + number: numberParts.join(" "), }; } - + return { street: trimmed, number: null }; } private compareNumbers(num1: string | null, num2: string | null): boolean { - if ((!num1 || num1.trim() === '') && (!num2 || num2.trim() === '')) { + if ((!num1 || num1.trim() === "") && (!num2 || num2.trim() === "")) { return true; } - - if (!num1 || num1.trim() === '' || !num2 || num2.trim() === '') { + + if (!num1 || num1.trim() === "" || !num2 || num2.trim() === "") { return false; } - - const n1 = num1.trim().replace(/\s+/g, ' '); - const n2 = num2.trim().replace(/\s+/g, ' '); - + + const n1 = num1.trim().replace(/\s+/g, " "); + const n2 = num2.trim().replace(/\s+/g, " "); + return n1 === n2; } } diff --git a/backend/src/presentation/services/tenant.services.ts b/backend/src/presentation/services/tenant.services.ts index e07fd84..43ac4d5 100644 --- a/backend/src/presentation/services/tenant.services.ts +++ b/backend/src/presentation/services/tenant.services.ts @@ -1,220 +1,267 @@ -import { TransactionHelper } from '../../data/postgres/transaction.helper'; -import { - ClientRentalModel, - RentalModel, - CurrencyTypeModel, - PropertyModel, - PropertyTypeModel, - PropertyStatusModel, - ClientPropertyInterestModel -} from '../../data/postgres/models'; -import { CreateTenantDto } from '../../domain/dtos/clients/create-tenant.dto'; -import { CustomError, ClientEntity } from '../../domain'; -import { ClientCreationHelper } from './helpers/client-creation.helper'; - +import { + ClientPropertyInterestModel, + ClientRentalModel, + CurrencyTypeModel, + PropertyModel, + PropertyStatusModel, + PropertyTypeModel, + RentalModel, +} from "../../data/postgres/models"; +import { TransactionHelper } from "../../data/postgres/transaction.helper"; +import { CustomError } from "../../domain"; +import type { CreateTenantDto } from "../../domain/dtos/clients/create-tenant.dto"; +import { ClientCreationHelper } from "./helpers/client-creation.helper"; export class TenantServices { - - - async createTenantWithProperty( - createTenantDto: CreateTenantDto, - createdByUserId: number - ) { - return await TransactionHelper.executeInTransaction(async () => { - const categoryId = await ClientCreationHelper.resolveContactCategory('Inquilino'); - - let propertyId: number | undefined = undefined; - if (createTenantDto.property_id) { - await ClientCreationHelper.validatePropertyExists(createTenantDto.property_id); - propertyId = createTenantDto.property_id; - - const activeRental = await RentalModel.findActiveByPropertyId(propertyId); - if (activeRental) { - throw CustomError.badRequest( - 'La propiedad ya está alquilada a otro inquilino. No se puede crear un nuevo alquiler.' - ); - } - } - - let currencyTypeId: number | undefined = undefined; - if (createTenantDto.currency_type_id) { - currencyTypeId = createTenantDto.currency_type_id; - } else if (createTenantDto.currency_type) { - let currencyType = await CurrencyTypeModel.findByName(createTenantDto.currency_type); - - if (!currencyType || !currencyType.id) { - currencyType = await CurrencyTypeModel.findBySymbol(createTenantDto.currency_type); - } - - if (!currencyType || !currencyType.id) { - throw CustomError.badRequest( - `Currency type "${createTenantDto.currency_type}" not found. Use the name (e.g., "Peso Argentino") or symbol (e.g., "ARS")` - ); - } - currencyTypeId = currencyType.id; - } - - const { client: newClient, wasCreated } = await ClientCreationHelper.createBaseClient({ - first_name: createTenantDto.first_name, - last_name: createTenantDto.last_name, - phone: createTenantDto.phone, - email: createTenantDto.email, - dni: createTenantDto.dni, - address: createTenantDto.address, - notes: createTenantDto.notes, - rental_interest: true, - }, categoryId); - - let clientRental = null; - let rental = null; - - if (propertyId && createTenantDto.contract_start_date && newClient.id) { - const normalizeDate = (date: string | Date | undefined): Date | undefined => { - if (!date) return undefined; - - let dateStr: string; - if (typeof date === 'string') { - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { - throw CustomError.badRequest(`Invalid date format. Expected YYYY-MM-DD, got: ${date}`); - } - dateStr = date; - } else { - dateStr = date.toISOString().split('T')[0]; - } - - const [year, month, day] = dateStr.split('-').map(Number); - return new Date(Date.UTC(year, month - 1, day)); - }; - - const normalizedStartDate = normalizeDate(createTenantDto.contract_start_date); - const normalizedEndDate = normalizeDate(createTenantDto.contract_end_date); - const normalizedIncreaseDate = normalizeDate(createTenantDto.next_increase_date); - - clientRental = await ClientRentalModel.create({ - client_id: newClient.id, - property_id: propertyId, - external_reference: createTenantDto.external_reference, - contract_start_date: normalizedStartDate, - contract_end_date: normalizedEndDate, - next_increase_date: normalizedIncreaseDate, - remind_increase: createTenantDto.remind_increase ?? false, - remind_contract_end: createTenantDto.remind_contract_end ?? false, - }); - - if (createTenantDto.monthly_amount && currencyTypeId && clientRental.id) { - rental = await RentalModel.create({ - property_id: propertyId, - client_rental_id: clientRental.id, - start_date: normalizedStartDate!, - end_date: normalizedEndDate, - next_increase_date: normalizedIncreaseDate, - monthly_amount: createTenantDto.monthly_amount, - currency_type_id: currencyTypeId, - created_by_user_id: createdByUserId, - remind_increase: createTenantDto.remind_increase ?? false, - remind_contract_end: createTenantDto.remind_contract_end ?? false, - }); - - await PropertyModel.update(propertyId, { - property_status_id: 3 - }); - } - } - - const clientEntity = ClientEntity.fromDatabaseObject(newClient); - - let rentedProperty = null; - if (clientRental && propertyId) { - const property = await PropertyModel.findById(propertyId); - if (property) { - const [propertyType, propertyStatus] = await Promise.all([ - PropertyTypeModel.findById(property.property_type_id), - PropertyStatusModel.findById(property.property_status_id) - ]); - - let currency = null; - if (rental?.currency_type_id) { - currency = await CurrencyTypeModel.findById(rental.currency_type_id); - } - - rentedProperty = { - id: property.id, - title: property.title, - description: property.description, - property_type: propertyType ? { id: propertyType.id, name: propertyType.name } : null, - property_status: propertyStatus ? { id: propertyStatus.id, name: propertyStatus.name } : null, - rental: { - id: clientRental.id, - contract_start_date: clientRental.contract_start_date, - contract_end_date: clientRental.contract_end_date, - next_increase_date: clientRental.next_increase_date, - monthly_amount: rental?.monthly_amount || null, - currency: currency ? { id: currency.id, name: currency.name, symbol: currency.symbol } : null, - external_reference: clientRental.external_reference || null - } - }; - } - } - - let propertiesOfInterest: Array<{ - id: number; - title: string; - property_type: { id: number; name: string } | null; - property_status: { id: number; name: string } | null; - interest_created_at: Date | undefined; - interest_notes: string | undefined; - }> = []; - if (newClient.id) { - const interests = await ClientPropertyInterestModel.findByClientId(newClient.id); - if (interests.length > 0) { - const properties = await Promise.all( - interests.map(async (interest) => { - const property = await PropertyModel.findById(interest.property_id); - if (!property || !property.id) return null; - - const [propertyType, propertyStatus] = await Promise.all([ - PropertyTypeModel.findById(property.property_type_id), - PropertyStatusModel.findById(property.property_status_id) - ]); - - return { - id: property.id, - title: property.title, - property_type: propertyType && propertyType.id ? { id: propertyType.id, name: propertyType.name } : null, - property_status: propertyStatus && propertyStatus.id ? { id: propertyStatus.id, name: propertyStatus.name } : null, - interest_created_at: interest.created_at, - interest_notes: interest.notes - }; - }) - ); - propertiesOfInterest = properties.filter((p): p is NonNullable => p !== null); - } - } - - const clientPublic = clientEntity.toPublicObject(); - delete (clientPublic as Record).property_interest_phone; - delete (clientPublic as Record).property_search_type_id; - - return { - client: clientPublic, - rented_property: rentedProperty, - properties_of_interest: propertiesOfInterest, - client_rental: clientRental ? { - id: clientRental.id, - property_id: clientRental.property_id, - contract_start_date: clientRental.contract_start_date, - contract_end_date: clientRental.contract_end_date, - next_increase_date: clientRental.next_increase_date, - } : null, - rental: rental ? { - id: rental.id, - monthly_amount: rental.monthly_amount, - currency_type_id: rental.currency_type_id, - start_date: rental.start_date, - end_date: rental.end_date, - } : null, - }; - }); - } -} + async createTenantWithProperty( + createTenantDto: CreateTenantDto, + createdByUserId: number, + ) { + return await TransactionHelper.executeInTransaction(async () => { + const categoryId = + await ClientCreationHelper.resolveContactCategory("Inquilino"); + + let propertyId: number | undefined; + if (createTenantDto.property_id) { + await ClientCreationHelper.validatePropertyExists( + createTenantDto.property_id, + ); + propertyId = createTenantDto.property_id; + + const activeRental = + await RentalModel.findActiveByPropertyId(propertyId); + if (activeRental) { + throw CustomError.badRequest( + "La propiedad ya está alquilada a otro inquilino. No se puede crear un nuevo alquiler.", + ); + } + } + + let currencyTypeId: number | undefined; + if (createTenantDto.currency_type_id) { + currencyTypeId = createTenantDto.currency_type_id; + } else if (createTenantDto.currency_type) { + let currencyType = await CurrencyTypeModel.findByName( + createTenantDto.currency_type, + ); + + if (!currencyType || !currencyType.id) { + currencyType = await CurrencyTypeModel.findBySymbol( + createTenantDto.currency_type, + ); + } + + if (!currencyType || !currencyType.id) { + throw CustomError.badRequest( + `Currency type "${createTenantDto.currency_type}" not found. Use the name (e.g., "Peso Argentino") or symbol (e.g., "ARS")`, + ); + } + currencyTypeId = currencyType.id; + } + + const { client: newClient } = await ClientCreationHelper.createBaseClient( + { + first_name: createTenantDto.first_name, + last_name: createTenantDto.last_name, + phone: createTenantDto.phone, + email: createTenantDto.email, + dni: createTenantDto.dni, + address: createTenantDto.address, + notes: createTenantDto.notes, + rental_interest: true, + }, + categoryId, + ); + + let clientRental = null; + let rental = null; + + if (propertyId && createTenantDto.contract_start_date && newClient.id) { + const normalizeDate = ( + date: string | Date | undefined, + ): Date | undefined => { + if (!date) return undefined; + + let dateStr: string; + if (typeof date === "string") { + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + throw CustomError.badRequest( + `Invalid date format. Expected YYYY-MM-DD, got: ${date}`, + ); + } + dateStr = date; + } else { + dateStr = date.toISOString().split("T")[0]; + } + + const [year, month, day] = dateStr.split("-").map(Number); + return new Date(Date.UTC(year, month - 1, day)); + }; + + const normalizedStartDate = normalizeDate( + createTenantDto.contract_start_date, + ); + const normalizedEndDate = normalizeDate( + createTenantDto.contract_end_date, + ); + const normalizedIncreaseDate = normalizeDate( + createTenantDto.next_increase_date, + ); + clientRental = await ClientRentalModel.create({ + client_id: newClient.id, + property_id: propertyId, + external_reference: createTenantDto.external_reference, + contract_start_date: normalizedStartDate, + contract_end_date: normalizedEndDate, + next_increase_date: normalizedIncreaseDate, + remind_increase: createTenantDto.remind_increase ?? false, + remind_contract_end: createTenantDto.remind_contract_end ?? false, + }); + + if ( + createTenantDto.monthly_amount && + currencyTypeId && + clientRental.id + ) { + rental = await RentalModel.create({ + property_id: propertyId, + client_rental_id: clientRental.id, + start_date: normalizedStartDate || new Date(), + end_date: normalizedEndDate, + next_increase_date: normalizedIncreaseDate, + monthly_amount: createTenantDto.monthly_amount, + currency_type_id: currencyTypeId, + created_by_user_id: createdByUserId, + remind_increase: createTenantDto.remind_increase ?? false, + remind_contract_end: createTenantDto.remind_contract_end ?? false, + }); + + await PropertyModel.update(propertyId, { + property_status_id: 3, + }); + } + } + + const clientEntity = newClient; + + let rentedProperty = null; + if (clientRental && propertyId) { + const property = await PropertyModel.findById(propertyId); + if (property) { + const [propertyType, propertyStatus] = await Promise.all([ + PropertyTypeModel.findById(property.property_type_id), + PropertyStatusModel.findById(property.property_status_id), + ]); + + let currency = null; + if (rental?.currency_type_id) { + currency = await CurrencyTypeModel.findById( + rental.currency_type_id, + ); + } + + rentedProperty = { + id: property.id, + title: property.title, + description: property.description, + property_type: propertyType + ? { id: propertyType.id, name: propertyType.name } + : null, + property_status: propertyStatus + ? { id: propertyStatus.id, name: propertyStatus.name } + : null, + rental: { + id: clientRental.id, + contract_start_date: clientRental.contract_start_date, + contract_end_date: clientRental.contract_end_date, + next_increase_date: clientRental.next_increase_date, + monthly_amount: rental?.monthly_amount || null, + currency: currency + ? { + id: currency.id, + name: currency.name, + symbol: currency.symbol, + } + : null, + external_reference: clientRental.external_reference || null, + }, + }; + } + } + + let propertiesOfInterest: Array<{ + id: number; + title: string; + property_type: { id: number; name: string } | null; + property_status: { id: number; name: string } | null; + interest_created_at: Date | undefined; + interest_notes: string | undefined; + }> = []; + if (newClient.id) { + const interests = await ClientPropertyInterestModel.findByClientId( + newClient.id, + ); + if (interests.length > 0) { + const properties = await Promise.all( + interests.map(async (interest) => { + const property = await PropertyModel.findById( + interest.property_id, + ); + if (!property || !property.id) return null; + + const [propertyType, propertyStatus] = await Promise.all([ + PropertyTypeModel.findById(property.property_type_id), + PropertyStatusModel.findById(property.property_status_id), + ]); + + return { + id: property.id, + title: property.title, + property_type: propertyType?.id + ? { id: propertyType.id, name: propertyType.name } + : null, + property_status: propertyStatus?.id + ? { id: propertyStatus.id, name: propertyStatus.name } + : null, + interest_created_at: interest.created_at, + interest_notes: interest.notes, + }; + }), + ); + propertiesOfInterest = properties.filter( + (p): p is NonNullable => p !== null, + ); + } + } + + const clientPublic = { ...clientEntity.toPublicObject() }; + delete (clientPublic as Record).property_interest_phone; + delete (clientPublic as Record).property_search_type_id; + + return { + client: clientPublic, + rented_property: rentedProperty, + properties_of_interest: propertiesOfInterest, + client_rental: clientRental + ? { + id: clientRental.id, + property_id: clientRental.property_id, + contract_start_date: clientRental.contract_start_date, + contract_end_date: clientRental.contract_end_date, + next_increase_date: clientRental.next_increase_date, + } + : null, + rental: rental + ? { + id: rental.id, + monthly_amount: rental.monthly_amount, + currency_type_id: rental.currency_type_id, + start_date: rental.start_date, + end_date: rental.end_date, + } + : null, + }; + }); + } +} diff --git a/backend/src/presentation/services/user.services.ts b/backend/src/presentation/services/user.services.ts index fbd5101..a168244 100644 --- a/backend/src/presentation/services/user.services.ts +++ b/backend/src/presentation/services/user.services.ts @@ -1,164 +1,179 @@ -import { ProfileModel, RoleModel } from '../../data/postgres/models'; -import { CustomError, UpdateUserDto } from '../../domain'; -import { HashAdapter } from '../../domain/interfaces/hash.adapter'; - +import { ProfileModel, RoleModel } from "../../data/postgres/models"; +import { CustomError, type UpdateUserDto } from "../../domain"; +import type { HashAdapter } from "../../domain/interfaces/hash.adapter"; export class UserServices { - constructor( - private readonly hashAdapter: HashAdapter - ) {} - - async listUsers() { - try { - const users = await ProfileModel.findAll(); - - const enrichedUsers = await Promise.all( - users.map(async (user) => { - const role = user.role_id ? await RoleModel.findById(user.role_id) : null; - - return { - id: user.id, - first_name: user.first_name, - last_name: user.last_name, - email: user.email, - phone: user.phone || null, - active: user.active ?? true, - role: role ? role.name : null, - created_at: user.created_at - }; - }) - ); - - return { - users: enrichedUsers, - count: enrichedUsers.length - }; - } catch (error) { - throw CustomError.internalServerError(`Error listing users: ${error}`); - } - } - - async getUserById(userId: number | string) { - try { - const user = await ProfileModel.findById(userId); - if (!user) { - throw CustomError.notFound('User not found'); - } - - const role = user.role_id ? await RoleModel.findById(user.role_id) : null; - - return { - id: user.id, - first_name: user.first_name, - last_name: user.last_name, - email: user.email, - phone: user.phone || null, - active: user.active ?? true, - role: role ? role.name : null, - created_at: user.created_at - }; - } catch (error) { - if (error instanceof CustomError) { - throw error; - } - throw CustomError.internalServerError(`Error getting user: ${error}`); - } - } - - - async updateUser(userId: number, updateUserDto: UpdateUserDto, isAdmin: boolean = false) { - try { - const existingUser = await ProfileModel.findById(userId); - if (!existingUser) { - throw CustomError.notFound('User not found'); - } - - const updateData: Record = {}; - - if (updateUserDto.first_name !== undefined) { - updateData.first_name = updateUserDto.first_name; - } - if (updateUserDto.last_name !== undefined) { - updateData.last_name = updateUserDto.last_name; - } - if (updateUserDto.email !== undefined) { - const existingEmailUser = await ProfileModel.findByEmail(updateUserDto.email); - if (existingEmailUser && existingEmailUser.id !== userId) { - throw CustomError.badRequest('Email is already in use'); - } - updateData.email = updateUserDto.email; - } - if (updateUserDto.phone !== undefined) { - updateData.phone = updateUserDto.phone || null; - } - if (updateUserDto.password !== undefined) { - updateData.password_hash = await this.hashAdapter.hash(updateUserDto.password); - } - - if (isAdmin) { - if (updateUserDto.role !== undefined) { - const role = await RoleModel.findByName(updateUserDto.role); - if (!role || !role.id) { - throw CustomError.badRequest(`Role "${updateUserDto.role}" not found. Valid roles: admin, agent`); - } - updateData.role_id = role.id; - } else if (updateUserDto.role_id !== undefined) { - updateData.role_id = updateUserDto.role_id; - } - - if (updateUserDto.active !== undefined) { - updateData.active = updateUserDto.active; - } - } else { - if (updateUserDto.role !== undefined || updateUserDto.role_id !== undefined || updateUserDto.active !== undefined) { - throw CustomError.badRequest('Only admin can change role or active status'); - } - } - - const updatedUser = await ProfileModel.update(userId, updateData); - if (!updatedUser) { - throw CustomError.internalServerError('Failed to update user'); - } - - const role = updatedUser.role_id ? await RoleModel.findById(updatedUser.role_id) : null; - - return { - id: updatedUser.id, - first_name: updatedUser.first_name, - last_name: updatedUser.last_name, - email: updatedUser.email, - phone: updatedUser.phone || null, - active: updatedUser.active ?? true, - role: role ? role.name : null, - created_at: updatedUser.created_at - }; - } catch (error) { - if (error instanceof CustomError) { - throw error; - } - throw CustomError.internalServerError(`Error updating user: ${error}`); - } - } - - async deleteUser(userId: number) { - try { - const user = await ProfileModel.findById(userId); - if (!user) { - throw CustomError.notFound('User not found'); - } - - const deleted = await ProfileModel.delete(userId); - if (!deleted) { - throw CustomError.internalServerError('Failed to delete user'); - } - - return { message: 'User deleted successfully' }; - } catch (error) { - if (error instanceof CustomError) { - throw error; - } - throw CustomError.internalServerError(`Error deleting user: ${error}`); - } - } -} + constructor(private readonly hashAdapter: HashAdapter) {} + + async listUsers() { + try { + const users = await ProfileModel.findAll(); + + const enrichedUsers = await Promise.all( + users.map(async (user) => { + const role = user.role_id + ? await RoleModel.findById(user.role_id) + : null; + + return { + id: user.id, + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + phone: user.phone || null, + active: user.active ?? true, + role: role ? role.name : null, + created_at: user.created_at, + }; + }), + ); + + return { + users: enrichedUsers, + count: enrichedUsers.length, + }; + } catch (error) { + throw CustomError.internalServerError(`Error listing users: ${error}`); + } + } + + async getUserById(userId: number | string) { + try { + const user = await ProfileModel.findById(userId); + if (!user) { + throw CustomError.notFound("User not found"); + } + + const role = user.role_id ? await RoleModel.findById(user.role_id) : null; + + return { + id: user.id, + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + phone: user.phone || null, + active: user.active ?? true, + role: role ? role.name : null, + created_at: user.created_at, + }; + } catch (error) { + if (error instanceof CustomError) { + throw error; + } + throw CustomError.internalServerError(`Error getting user: ${error}`); + } + } + + async updateUser( + userId: number, + updateUserDto: UpdateUserDto, + isAdmin: boolean = false, + ) { + try { + const existingUser = await ProfileModel.findById(userId); + if (!existingUser) { + throw CustomError.notFound("User not found"); + } + const updateData: Record = {}; + + if (updateUserDto.first_name !== undefined) { + updateData.first_name = updateUserDto.first_name; + } + if (updateUserDto.last_name !== undefined) { + updateData.last_name = updateUserDto.last_name; + } + if (updateUserDto.email !== undefined) { + const existingEmailUser = await ProfileModel.findByEmail( + updateUserDto.email, + ); + if (existingEmailUser && existingEmailUser.id !== userId) { + throw CustomError.badRequest("Email is already in use"); + } + updateData.email = updateUserDto.email; + } + if (updateUserDto.phone !== undefined) { + updateData.phone = updateUserDto.phone || null; + } + if (updateUserDto.password !== undefined) { + updateData.password_hash = await this.hashAdapter.hash( + updateUserDto.password, + ); + } + + if (isAdmin) { + if (updateUserDto.role !== undefined) { + const role = await RoleModel.findByName(updateUserDto.role); + if (!role || !role.id) { + throw CustomError.badRequest( + `Role "${updateUserDto.role}" not found. Valid roles: admin, agent`, + ); + } + updateData.role_id = role.id; + } else if (updateUserDto.role_id !== undefined) { + updateData.role_id = updateUserDto.role_id; + } + + if (updateUserDto.active !== undefined) { + updateData.active = updateUserDto.active; + } + } else { + if ( + updateUserDto.role !== undefined || + updateUserDto.role_id !== undefined || + updateUserDto.active !== undefined + ) { + throw CustomError.badRequest( + "Only admin can change role or active status", + ); + } + } + + const updatedUser = await ProfileModel.update(userId, updateData); + if (!updatedUser) { + throw CustomError.internalServerError("Failed to update user"); + } + + const role = updatedUser.role_id + ? await RoleModel.findById(updatedUser.role_id) + : null; + + return { + id: updatedUser.id, + first_name: updatedUser.first_name, + last_name: updatedUser.last_name, + email: updatedUser.email, + phone: updatedUser.phone || null, + active: updatedUser.active ?? true, + role: role ? role.name : null, + created_at: updatedUser.created_at, + }; + } catch (error) { + if (error instanceof CustomError) { + throw error; + } + throw CustomError.internalServerError(`Error updating user: ${error}`); + } + } + + async deleteUser(userId: number) { + try { + const user = await ProfileModel.findById(userId); + if (!user) { + throw CustomError.notFound("User not found"); + } + + const deleted = await ProfileModel.delete(userId); + if (!deleted) { + throw CustomError.internalServerError("Failed to delete user"); + } + + return { message: "User deleted successfully" }; + } catch (error) { + if (error instanceof CustomError) { + throw error; + } + throw CustomError.internalServerError(`Error deleting user: ${error}`); + } + } +} diff --git a/backend/src/presentation/shared/error-handler.util.ts b/backend/src/presentation/shared/error-handler.util.ts index 3a8e692..68f8e98 100644 --- a/backend/src/presentation/shared/error-handler.util.ts +++ b/backend/src/presentation/shared/error-handler.util.ts @@ -1,100 +1,110 @@ -import { Response } from 'express'; -import { CustomError } from '../../domain'; +import type { Response } from "express"; +import { CustomError } from "../../domain"; export class ErrorHandlerUtil { - - /** - * Maneja errores y retorna respuesta HTTP apropiada - * @param error - Error a manejar - * @param res - Objeto Response de Express - * @param controllerName - Nombre del controlador para logging (opcional) - */ - static handleError(error: unknown, res: Response, controllerName?: string): void { - if (error instanceof CustomError) { - res.status(error.statusCode).json({ - message: error.message - }); - return; - } + /** + * Maneja errores y retorna respuesta HTTP apropiada + * @param error - Error a manejar + * @param res - Objeto Response de Express + * @param controllerName - Nombre del controlador para logging (opcional) + */ + static handleError( + error: unknown, + res: Response, + controllerName?: string, + ): void { + if (error instanceof CustomError) { + res.status(error.statusCode).json({ + message: error.message, + }); + return; + } - if (error && typeof error === 'object' && 'code' in error) { - const pgError = error as Error & { code?: string; constraint?: string; detail?: string; column?: string }; - - if (pgError.code === '23505') { - const constraint = pgError.constraint || ''; - const detail = pgError.detail || ''; - - if (constraint.includes('email')) { - const emailMatch = detail.match(/\(email\)=\(([^)]+)\)/); - const email = emailMatch ? emailMatch[1] : 'el email proporcionado'; - res.status(409).json({ - message: `El email ${email} ya está registrado. Por favor, use otro email.` - }); - return; - } - - if (constraint.includes('dni')) { - const dniMatch = detail.match(/\(dni\)=\(([^)]+)\)/); - const dni = dniMatch ? dniMatch[1] : 'el DNI proporcionado'; - res.status(409).json({ - message: `El DNI ${dni} ya está registrado. Por favor, use otro DNI.` - }); - return; - } - - if (constraint.includes('phone')) { - res.status(409).json({ - message: 'El teléfono ya está registrado. Por favor, use otro teléfono.' - }); - return; - } - - res.status(409).json({ - message: 'Ya existe un registro con estos datos. Por favor, verifique la información.' - }); - return; - } - - if (pgError.code === '23503') { - res.status(400).json({ - message: 'Los datos proporcionados no son válidos. Verifique las referencias a otros registros.' - }); - return; - } - - if (pgError.code === '23502') { - const column = pgError.column || 'campo'; - res.status(400).json({ - message: `El campo ${column} es requerido y no puede estar vacío.` - }); - return; - } - - if (pgError.code === '22001') { - const errorMessage = pgError.message || ''; - let fieldName = 'campo'; - - if (errorMessage.includes('phone')) { - fieldName = 'teléfono'; - } else if (errorMessage.includes('dni')) { - fieldName = 'DNI'; - } else if (errorMessage.includes('email')) { - fieldName = 'email'; - } - - res.status(400).json({ - message: `El valor del campo ${fieldName} es demasiado largo. Por favor, verifique que no exceda el límite permitido.` - }); - return; - } - } + if (error && typeof error === "object" && "code" in error) { + const pgError = error as Error & { + code?: string; + constraint?: string; + detail?: string; + column?: string; + }; - const controllerLabel = controllerName ? `${controllerName} ` : ''; - console.error(`${controllerLabel}Controller Error:`, error); - res.status(500).json({ - message: 'Internal server error' - }); - } -} + if (pgError.code === "23505") { + const constraint = pgError.constraint || ""; + const detail = pgError.detail || ""; + + if (constraint.includes("email")) { + const emailMatch = detail.match(/\(email\)=\(([^)]+)\)/); + const email = emailMatch ? emailMatch[1] : "el email proporcionado"; + res.status(409).json({ + message: `El email ${email} ya está registrado. Por favor, use otro email.`, + }); + return; + } + + if (constraint.includes("dni")) { + const dniMatch = detail.match(/\(dni\)=\(([^)]+)\)/); + const dni = dniMatch ? dniMatch[1] : "el DNI proporcionado"; + res.status(409).json({ + message: `El DNI ${dni} ya está registrado. Por favor, use otro DNI.`, + }); + return; + } + + if (constraint.includes("phone")) { + res.status(409).json({ + message: + "El teléfono ya está registrado. Por favor, use otro teléfono.", + }); + return; + } + + res.status(409).json({ + message: + "Ya existe un registro con estos datos. Por favor, verifique la información.", + }); + return; + } + + if (pgError.code === "23503") { + res.status(400).json({ + message: + "Los datos proporcionados no son válidos. Verifique las referencias a otros registros.", + }); + return; + } + if (pgError.code === "23502") { + const column = pgError.column || "campo"; + res.status(400).json({ + message: `El campo ${column} es requerido y no puede estar vacío.`, + }); + return; + } + + if (pgError.code === "22001") { + const errorMessage = pgError.message || ""; + let fieldName = "campo"; + + if (errorMessage.includes("phone")) { + fieldName = "teléfono"; + } else if (errorMessage.includes("dni")) { + fieldName = "DNI"; + } else if (errorMessage.includes("email")) { + fieldName = "email"; + } + + res.status(400).json({ + message: `El valor del campo ${fieldName} es demasiado largo. Por favor, verifique que no exceda el límite permitido.`, + }); + return; + } + } + + const controllerLabel = controllerName ? `${controllerName} ` : ""; + console.error(`${controllerLabel}Controller Error:`, error); + res.status(500).json({ + message: "Internal server error", + }); + } +} diff --git a/backend/src/presentation/users/controller.ts b/backend/src/presentation/users/controller.ts index 4d8d88f..cef5982 100644 --- a/backend/src/presentation/users/controller.ts +++ b/backend/src/presentation/users/controller.ts @@ -1,134 +1,136 @@ -import { Request, Response } from 'express'; -import { CustomError, UpdateUserDto } from '../../domain'; -import { UserServices } from '../services/user.services'; +import type { Request, Response } from "express"; + +import { CustomError, UpdateUserDto } from "../../domain"; +import type { UserServices } from "../services/user.services"; export class UserController { - constructor( - private readonly userServices: UserServices - ) {} - - private handleError = (error: unknown, res: Response) => { - if (error instanceof CustomError) { - return res.status(error.statusCode).json({ - message: error.message - }); - } - console.error('User Controller Error:', error); - return res.status(500).json({ - message: 'Internal server error' - }); - } - - /** - * Lists all users - */ - listUsers = async (req: Request, res: Response) => { - try { - const result = await this.userServices.listUsers(); - return res.json(result); - } catch (error) { - this.handleError(error, res); - } - } - - /** - * Gets a single user by ID - */ - getUserById = async (req: Request, res: Response) => { - try { - const { id } = req.params; - - if (!id) { - return res.status(400).json({ - message: 'User ID is required' - }); - } - - const userId = Number(id); - if (isNaN(userId)) { - return res.status(400).json({ - message: 'Invalid user ID' - }); - } - - const user = await this.userServices.getUserById(userId); - return res.json({ user }); - } catch (error) { - this.handleError(error, res); - } - } - - /** - * Updates a user - * Admin can update any user, users can only update themselves - */ - updateUser = async (req: Request, res: Response) => { - try { - const { id } = req.params; - const user = req.user; - - if (!user) { - return res.status(401).json({ - message: 'User not authenticated' - }); - } - - if (!id) { - return res.status(400).json({ - message: 'User ID is required' - }); - } - - const userId = Number(id); - if (isNaN(userId)) { - return res.status(400).json({ - message: 'Invalid user ID' - }); - } - - const [error, updateUserDto] = UpdateUserDto.create(req.body); - if (error || !updateUserDto) { - return res.status(400).json({ - message: error || 'Invalid user data' - }); - } - - const isAdmin = user.isAdmin === true; - const updatedUser = await this.userServices.updateUser(userId, updateUserDto, isAdmin); - return res.json({ - message: 'User updated successfully', - user: updatedUser - }); - } catch (error) { - this.handleError(error, res); - } - } - - /** - * Soft deletes a user (Admin only) - */ - deleteUser = async (req: Request, res: Response) => { - try { - const { id } = req.params; - - if (!id) { - return res.status(400).json({ - message: 'User ID is required' - }); - } - - const userId = Number(id); - if (isNaN(userId)) { - return res.status(400).json({ - message: 'Invalid user ID' - }); - } - - const result = await this.userServices.deleteUser(userId); - return res.json(result); - } catch (error) { - this.handleError(error, res); - } - } -} + constructor(private readonly userServices: UserServices) {} + + private handleError = (error: unknown, res: Response) => { + if (error instanceof CustomError) { + return res.status(error.statusCode).json({ + message: error.message, + }); + } + console.error("User Controller Error:", error); + return res.status(500).json({ + message: "Internal server error", + }); + }; + + /** + * Lists all users + */ + listUsers = async (_req: Request, res: Response) => { + try { + const result = await this.userServices.listUsers(); + return res.json(result); + } catch (error) { + this.handleError(error, res); + } + }; + + /** + * Gets a single user by ID + */ + getUserById = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ + message: "User ID is required", + }); + } + + const userId = Number(id); + if (Number.isNaN(userId)) { + return res.status(400).json({ + message: "Invalid user ID", + }); + } + + const user = await this.userServices.getUserById(userId); + return res.json({ user }); + } catch (error) { + this.handleError(error, res); + } + }; + + /** + * Updates a user + * Admin can update any user, users can only update themselves + */ + updateUser = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const user = req.user; + if (!user) { + return res.status(401).json({ + message: "User not authenticated", + }); + } + + if (!id) { + return res.status(400).json({ + message: "User ID is required", + }); + } + + const userId = Number(id); + if (Number.isNaN(userId)) { + return res.status(400).json({ + message: "Invalid user ID", + }); + } + + const [error, updateUserDto] = UpdateUserDto.create(req.body); + if (error || !updateUserDto) { + return res.status(400).json({ + message: error || "Invalid user data", + }); + } + + const isAdmin = user.isAdmin === true; + const updatedUser = await this.userServices.updateUser( + userId, + updateUserDto, + isAdmin, + ); + return res.json({ + message: "User updated successfully", + user: updatedUser, + }); + } catch (error) { + this.handleError(error, res); + } + }; + + /** + * Soft deletes a user (Admin only) + */ + deleteUser = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ + message: "User ID is required", + }); + } + + const userId = Number(id); + if (Number.isNaN(userId)) { + return res.status(400).json({ + message: "Invalid user ID", + }); + } + + const result = await this.userServices.deleteUser(userId); + return res.json(result); + } catch (error) { + this.handleError(error, res); + } + }; +} diff --git a/backend/src/presentation/users/routes.ts b/backend/src/presentation/users/routes.ts index 6a9a9c5..c8bd96f 100644 --- a/backend/src/presentation/users/routes.ts +++ b/backend/src/presentation/users/routes.ts @@ -1,215 +1,215 @@ -import { Router } from 'express'; -import { UserController } from './controller'; -import { UserServices } from '../services/user.services'; -import { AuthMiddleware } from '../middlewares/auth.middleware'; -import { jwtAdapter, bcryptAdapter } from '../../config'; +import { Router } from "express"; + +import { bcryptAdapter, jwtAdapter } from "../../config"; +import { AuthMiddleware } from "../middlewares/auth.middleware"; +import { UserServices } from "../services/user.services"; +import { UserController } from "./controller"; export class UserRoutes { - static get routes(): Router { - const router = Router(); - - const userServices = new UserServices(bcryptAdapter); - const controller = new UserController(userServices); - const authMiddleware = new AuthMiddleware(jwtAdapter); + static get routes(): Router { + const router = Router(); - /** - * @swagger - * /api/users: - * get: - * summary: List all users (Admin only) - * tags: [Users] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: List of users with roles - * content: - * application/json: - * schema: - * type: object - * properties: - * users: - * type: array - * items: - * type: object - * properties: - * id: - * type: integer - * first_name: - * type: string - * last_name: - * type: string - * email: - * type: string - * phone: - * type: string - * active: - * type: boolean - * role: - * type: string - * created_at: - * type: string - * format: date-time - * count: - * type: integer - * 401: - * description: Unauthorized - * 403: - * description: Admin access required - */ - router.get( - '/', - authMiddleware.authenticate, - authMiddleware.requireAdmin, - (req, res) => controller.listUsers(req, res) - ); + const userServices = new UserServices(bcryptAdapter); + const controller = new UserController(userServices); + const authMiddleware = new AuthMiddleware(jwtAdapter); - /** - * @swagger - * /api/users/{id}: - * get: - * summary: Get a user by ID (Admin only) - * tags: [Users] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: User ID - * responses: - * 200: - * description: User details - * content: - * application/json: - * schema: - * type: object - * properties: - * user: - * type: object - * properties: - * id: - * type: integer - * first_name: - * type: string - * last_name: - * type: string - * email: - * type: string - * phone: - * type: string - * active: - * type: boolean - * role: - * type: string - * created_at: - * type: string - * format: date-time - * 404: - * description: User not found - * 401: - * description: Unauthorized - * 403: - * description: Admin access required - */ - router.get( - '/:id', - authMiddleware.authenticate, - authMiddleware.requireAdmin, - (req, res) => controller.getUserById(req, res) - ); + /** + * @swagger + * /api/users: + * get: + * summary: List all users (Admin only) + * tags: [Users] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of users with roles + * content: + * application/json: + * schema: + * type: object + * properties: + * users: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * first_name: + * type: string + * last_name: + * type: string + * email: + * type: string + * phone: + * type: string + * active: + * type: boolean + * role: + * type: string + * created_at: + * type: string + * format: date-time + * count: + * type: integer + * 401: + * description: Unauthorized + * 403: + * description: Admin access required + */ + router.get( + "/", + authMiddleware.authenticate, + authMiddleware.requireAdmin, + (req, res) => controller.listUsers(req, res), + ); - /** - * @swagger - * /api/users/{id}: - * put: - * summary: Update a user (Admin can update any user, users can only update themselves) - * tags: [Users] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: User ID - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * first_name: - * type: string - * last_name: - * type: string - * email: - * type: string - * phone: - * type: string - * password: - * type: string - * role: - * type: string - * enum: [admin, agent] - * description: Role name (Only admin can change this). Use 'admin' or 'agent' - * active: - * type: boolean - * description: Only admin can change this - * responses: - * 200: - * description: User updated successfully - * 400: - * description: Bad request - * 401: - * description: Unauthorized - * 403: - * description: Access denied - */ - router.put( - '/:id', - authMiddleware.authenticate, - authMiddleware.requireAdminOrOwner, - (req, res) => controller.updateUser(req, res) - ); + /** + * @swagger + * /api/users/{id}: + * get: + * summary: Get a user by ID (Admin only) + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: User ID + * responses: + * 200: + * description: User details + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: integer + * first_name: + * type: string + * last_name: + * type: string + * email: + * type: string + * phone: + * type: string + * active: + * type: boolean + * role: + * type: string + * created_at: + * type: string + * format: date-time + * 404: + * description: User not found + * 401: + * description: Unauthorized + * 403: + * description: Admin access required + */ + router.get( + "/:id", + authMiddleware.authenticate, + authMiddleware.requireAdmin, + (req, res) => controller.getUserById(req, res), + ); - /** - * @swagger - * /api/users/{id}: - * delete: - * summary: Soft delete a user (Admin only) - * tags: [Users] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: User ID - * responses: - * 200: - * description: User deleted successfully - * 401: - * description: Unauthorized - * 403: - * description: Admin access required - * 404: - * description: User not found - */ - router.delete( - '/:id', - authMiddleware.authenticate, - authMiddleware.requireAdmin, - (req, res) => controller.deleteUser(req, res) - ); + /** + * @swagger + * /api/users/{id}: + * put: + * summary: Update a user (Admin can update any user, users can only update themselves) + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: User ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * first_name: + * type: string + * last_name: + * type: string + * email: + * type: string + * phone: + * type: string + * password: + * type: string + * role: + * type: string + * enum: [admin, agent] + * description: Role name (Only admin can change this). Use 'admin' or 'agent' + * active: + * type: boolean + * description: Only admin can change this + * responses: + * 200: + * description: User updated successfully + * 400: + * description: Bad request + * 401: + * description: Unauthorized + * 403: + * description: Access denied + */ + router.put( + "/:id", + authMiddleware.authenticate, + authMiddleware.requireAdminOrOwner, + (req, res) => controller.updateUser(req, res), + ); - return router; - } -} + /** + * @swagger + * /api/users/{id}: + * delete: + * summary: Soft delete a user (Admin only) + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: User ID + * responses: + * 200: + * description: User deleted successfully + * 401: + * description: Unauthorized + * 403: + * description: Admin access required + * 404: + * description: User not found + */ + router.delete( + "/:id", + authMiddleware.authenticate, + authMiddleware.requireAdmin, + (req, res) => controller.deleteUser(req, res), + ); + return router; + } +} diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index c3f19bd..902f967 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -1,4 +1,4 @@ -import { JwtPayload } from '../../domain/interfaces/jwt-payload'; +import type { JwtPayload } from "../../domain/interfaces/jwt-payload"; declare global { namespace Express { @@ -7,5 +7,3 @@ declare global { } } } - -export {}; diff --git a/backend/src/types/json.types.ts b/backend/src/types/json.types.ts index 2889890..b0b1108 100644 --- a/backend/src/types/json.types.ts +++ b/backend/src/types/json.types.ts @@ -1,4 +1,3 @@ - export type JsonPrimitive = string | number | boolean | null; export type JsonValue = JsonPrimitive | JsonObject | JsonArray; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 465b8be..b17db3b 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -33,7 +33,10 @@ "@/*": ["src/*"] } /* Specify a set of entries that re-map imports to additional lookup locations. */, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - "typeRoots": ["./node_modules/@types", "./src/types"] /* Specify multiple folders that act like './node_modules/@types'. */, + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ] /* Specify multiple folders that act like './node_modules/@types'. */, // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ From 7ed87ce1fdcf8883c73318294f178b0bd52fed85 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 18:20:17 -0500 Subject: [PATCH 10/18] refactor(backend): fix remaining implicit any and unused variables --- biome.json | 12 ++++++++++++ package.json | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 15cc742..2e1909b 100644 --- a/biome.json +++ b/biome.json @@ -62,6 +62,18 @@ "react": "recommended" } }, + "overrides": [ + { + "includes": ["backend/**/*"], + "linter": { + "rules": { + "complexity": { + "noStaticOnlyClass": "off" + } + } + } + } + ], "javascript": { "formatter": { "quoteStyle": "double" diff --git a/package.json b/package.json index 9572f32..e0ce49b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "build:frontend": "cd frontend && npm run build", "build:backend": "cd backend && npm run build", "dev:backend": "npm --prefix backend run dev", - "start:backend": "npm --prefix backend run start", "db:migrate": "npm --prefix backend run db:migrate", "db:setup": "npm --prefix backend run db:setup", "db:reset": "npm --prefix backend run db:reset", @@ -21,6 +20,8 @@ "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", + "format:backend": "biome check --write backend", + "format:frontend": "biome check --write frontend", "release": "standard-version" }, "lint-staged": { From 676143a42e1307efdb66e988f1257cfa90d2cd4e Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 18:30:03 -0500 Subject: [PATCH 11/18] fix(deps): update typescript to resolve npm ci error --- package-lock.json | 17 ++++++++++++++++- package.json | 3 ++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b190154..3aceef8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "@commitlint/config-conventional": "^20.0.0", "husky": "^9.1.7", "lint-staged": "^16.2.6", - "standard-version": "^9.5.0" + "standard-version": "^9.5.0", + "typescript": "^5.9.3" } }, "node_modules/@babel/code-frame": { @@ -4442,6 +4443,20 @@ "dev": true, "license": "MIT" }, + "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/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", diff --git a/package.json b/package.json index e0ce49b..9253bc5 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@commitlint/config-conventional": "^20.0.0", "husky": "^9.1.7", "lint-staged": "^16.2.6", - "standard-version": "^9.5.0" + "standard-version": "^9.5.0", + "typescript": "^5.9.3" } } From 39bb6aff0ac19358939a0999d99d71d4387f685b Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 18:41:45 -0500 Subject: [PATCH 12/18] fix(backend): install openapi-types to resolve npm ci error --- backend/package-lock.json | 23 ++++++++--------------- backend/package.json | 1 + 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 89f9efa..47edac9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -31,6 +31,7 @@ "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.8", "@types/uuid": "^10.0.0", + "openapi-types": "^12.1.3", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", @@ -1107,21 +1108,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1755,6 +1741,13 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT" + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 527e4bc..0de34d9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,6 +34,7 @@ "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.8", "@types/uuid": "^10.0.0", + "openapi-types": "^12.1.3", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", From 266e20cbbce5e6ebf00f6a2c6968314fba0e9d30 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 18:47:57 -0500 Subject: [PATCH 13/18] fix(deps): add biome linux cli to optionalDependencies for CI --- package-lock.json | 22 ++++++++++++++++++++++ package.json | 3 +++ 2 files changed, 25 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3aceef8..7c0ec56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "redprop", "version": "0.0.1", + "dependencies": { + "@biomejs/cli-linux-x64": "*" + }, "devDependencies": { "@biomejs/biome": "^2.3.5", "@commitlint/cli": "^20.1.0", @@ -15,6 +18,9 @@ "lint-staged": "^16.2.6", "standard-version": "^9.5.0", "typescript": "^5.9.3" + }, + "optionalDependencies": { + "@biomejs/cli-linux-x64": "^2.3.10" } }, "node_modules/@babel/code-frame": { @@ -69,6 +75,22 @@ "@biomejs/cli-win32-x64": "2.3.10" } }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.10.tgz", + "integrity": "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@biomejs/cli-win32-x64": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.10.tgz", diff --git a/package.json b/package.json index 9253bc5..ed76d94 100644 --- a/package.json +++ b/package.json @@ -40,5 +40,8 @@ "lint-staged": "^16.2.6", "standard-version": "^9.5.0", "typescript": "^5.9.3" + }, + "optionalDependencies": { + "@biomejs/cli-linux-x64": "^2.3.10" } } From c13b85b63ddcc54f3e05554bd6bfa8ae84cb485a Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 19:01:35 -0500 Subject: [PATCH 14/18] fix(frontend): replace any with unknown in search-context type definitions --- frontend/src/contexts/search-context.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/contexts/search-context.tsx b/frontend/src/contexts/search-context.tsx index 1388019..6732e00 100644 --- a/frontend/src/contexts/search-context.tsx +++ b/frontend/src/contexts/search-context.tsx @@ -6,7 +6,7 @@ import { createContext, useContext, useMemo, useState } from "react"; interface SearchContextType { searchTerm: string; setSearchTerm: (term: string) => void; - filterData: >( + filterData: >( data: T[], searchFields: (keyof T)[], ) => T[]; @@ -22,7 +22,7 @@ export function SearchProvider({ children }: SearchProviderProps) { const [searchTerm, setSearchTerm] = useState(""); const filterData = useMemo(() => { - return >( + return >( data: T[], searchFields: (keyof T)[], ) => { From 8d9c59236388faddca7ae364b00f97a4a8e52973 Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 28 Dec 2025 19:06:26 -0500 Subject: [PATCH 15/18] refactor(frontend): fix lint warnings and errores --- frontend/src/app/(auth)/login/page.tsx | 4 +- frontend/src/app/(auth)/register/page.tsx | 4 +- .../(marketing)/_components/banner/index.tsx | 6 +-- .../_components/properties-list/index.tsx | 8 +-- .../_components/general-contact-form.tsx | 2 +- frontend/src/app/(marketing)/contact/page.tsx | 2 +- .../property-contact-form/index.tsx | 2 +- .../_components/property-gallery/index.tsx | 12 ++--- .../app/(marketing)/properties/[id]/page.tsx | 36 ++++++------- .../(protected)/admin/users/create/page.tsx | 48 +++++++++--------- .../src/app/(protected)/admin/users/page.tsx | 14 +++--- .../agent/clients/create/layout.tsx | 12 ++--- .../(protected)/agent/clients/edit/layout.tsx | 2 +- .../agent/clients/edit/leads/[id]/page.tsx | 2 +- .../agent/clients/edit/owners/[id]/page.tsx | 2 +- .../agent/clients/edit/tenants/[id]/page.tsx | 2 +- .../agent/clients/leads/[id]/page.tsx | 6 +-- .../agent/clients/owners/[id]/page.tsx | 6 +-- .../agent/clients/tenants/[id]/page.tsx | 6 +-- .../app/(protected)/agent/dashboard/page.tsx | 6 +-- .../app/(protected)/agent/properties/page.tsx | 4 +- frontend/src/app/(protected)/layout.tsx | 4 +- frontend/src/app/unauthorized/page.tsx | 10 ++-- .../src/components/layouts/client-layout.tsx | 14 +++--- .../src/components/layouts/main-layout.tsx | 2 +- .../src/components/protected-header/index.tsx | 8 +-- .../notifications-sheet/index.tsx | 2 +- .../notification-card/index.tsx | 6 +-- .../protected-header/search-bar/index.tsx | 22 ++++---- .../user-menu-trigger/index.tsx | 4 +- .../src/components/public-header/index.tsx | 6 +-- .../src/components/section-heading/index.tsx | 4 +- .../sidebars/notification-sidebar/index.tsx | 2 +- .../sidebars/protected-sidebar/index.tsx | 18 +++---- frontend/src/components/ui/accordion.tsx | 6 +-- frontend/src/components/ui/alert-dialog.tsx | 12 ++--- frontend/src/components/ui/alert.tsx | 6 +-- frontend/src/components/ui/avatar.tsx | 2 +- frontend/src/components/ui/badge.tsx | 8 +-- frontend/src/components/ui/breadcrumb.tsx | 6 +-- frontend/src/components/ui/button-group.tsx | 6 +-- frontend/src/components/ui/button.tsx | 30 +++++------ frontend/src/components/ui/calendar.tsx | 36 ++++++------- frontend/src/components/ui/card.tsx | 4 +- frontend/src/components/ui/carousel.tsx | 2 +- frontend/src/components/ui/chart.tsx | 10 ++-- frontend/src/components/ui/checkbox.tsx | 2 +- frontend/src/components/ui/command.tsx | 16 +++--- frontend/src/components/ui/context-menu.tsx | 18 +++---- frontend/src/components/ui/dialog.tsx | 8 +-- frontend/src/components/ui/drawer.tsx | 8 +-- frontend/src/components/ui/dropdown-menu.tsx | 18 +++---- frontend/src/components/ui/empty.tsx | 12 ++--- .../src/components/ui/fancy-multi-select.tsx | 4 +- frontend/src/components/ui/field.tsx | 14 +++--- frontend/src/components/ui/file-upload.tsx | 2 +- frontend/src/components/ui/form.tsx | 4 +- frontend/src/components/ui/heading.tsx | 8 +-- frontend/src/components/ui/hover-card.tsx | 2 +- frontend/src/components/ui/input-group.tsx | 20 ++++---- frontend/src/components/ui/input-otp.tsx | 4 +- frontend/src/components/ui/input.tsx | 8 +-- frontend/src/components/ui/item.tsx | 16 +++--- frontend/src/components/ui/kbd.tsx | 2 +- frontend/src/components/ui/label.tsx | 2 +- frontend/src/components/ui/menubar.tsx | 22 ++++---- .../src/components/ui/navigation-menu.tsx | 14 +++--- frontend/src/components/ui/phone-input.tsx | 6 +-- frontend/src/components/ui/popover.tsx | 2 +- frontend/src/components/ui/progress.tsx | 4 +- frontend/src/components/ui/radio-group.tsx | 4 +- frontend/src/components/ui/resizable.tsx | 4 +- frontend/src/components/ui/scroll-area.tsx | 6 +-- frontend/src/components/ui/select.tsx | 12 ++--- frontend/src/components/ui/separator.tsx | 2 +- frontend/src/components/ui/sheet.tsx | 14 +++--- frontend/src/components/ui/sidebar.tsx | 42 ++++++++-------- frontend/src/components/ui/skeleton.tsx | 2 +- frontend/src/components/ui/slider.tsx | 8 +-- frontend/src/components/ui/status-badge.tsx | 6 +-- frontend/src/components/ui/switch.tsx | 6 +-- frontend/src/components/ui/table.tsx | 10 ++-- frontend/src/components/ui/tabs.tsx | 10 ++-- frontend/src/components/ui/textarea.tsx | 2 +- frontend/src/components/ui/toggle-group.tsx | 2 +- frontend/src/components/ui/toggle.tsx | 8 +-- frontend/src/components/ui/tooltip.tsx | 4 +- .../admin/components/admin-create-button.tsx | 4 +- .../admin/components/delete-user-alert.tsx | 4 +- .../src/modules/admin/components/dropdown.tsx | 2 +- .../admin/components/edit-user-modal.tsx | 40 +++++++-------- .../modules/admin/components/user-card.tsx | 10 ++-- .../modules/auth/components/login-form.tsx | 16 +++--- .../modules/auth/components/register-form.tsx | 26 +++++----- .../client-detail/client-contact-info.tsx | 26 +++++----- .../client-detail/client-header.tsx | 10 ++-- .../components/client-detail/client-notes.tsx | 6 +-- .../client-detail/client-properties.tsx | 10 ++-- .../client-detail/client-property-card.tsx | 32 ++++++------ .../clients/components/leads/leads-form.tsx | 42 ++++++++-------- .../clients/components/leads/leads-list.tsx | 2 +- .../components/owners/owners-forms.tsx | 46 ++++++++--------- .../clients/components/owners/owners-list.tsx | 2 +- .../components/tenants/tenants-form.tsx | 50 +++++++++---------- .../components/tenants/tenants-list.tsx | 2 +- .../modules/clients/ui/clients-pagination.tsx | 2 +- .../modules/clients/ui/date-picker-field.tsx | 8 +-- .../src/modules/clients/ui/leads-card.tsx | 20 ++++---- .../src/modules/clients/ui/owners-card.tsx | 22 ++++---- .../modules/clients/ui/property-select.tsx | 8 +-- .../src/modules/clients/ui/tenants-card.tsx | 20 ++++---- frontend/src/modules/clients/ui/tip-alert.tsx | 8 +-- .../consultation-actions.tsx | 4 +- .../consultation-contact-info.tsx | 6 +-- .../consultation-detail-sheet.tsx | 42 ++++++++-------- .../components/consultation-filters.tsx | 4 +- .../components/consultation-results.tsx | 4 +- .../components/list-consultations.tsx | 2 +- .../consultations/ui/consultation-card.tsx | 26 +++++----- .../ui/consultations-skeleton.tsx | 8 +-- .../dashboard/components/info-agents-card.tsx | 4 +- .../dashboard/components/info-list.tsx | 8 +-- .../components/create-property/index.tsx | 30 +++++------ .../property-documents-form/index.tsx | 20 ++++---- .../property-features-form/index.tsx | 4 +- .../property-gallery-form/index.tsx | 12 ++--- .../property-info-form/index.tsx | 4 +- .../property-publication-status/index.tsx | 2 +- .../property-services-form/index.tsx | 2 +- .../property-surfaces-form/index.tsx | 4 +- .../property-values-form/index.tsx | 6 +-- .../components/property-detail/index.tsx | 4 +- .../property-gallery/index.tsx | 10 ++-- .../property-featured-action/index.tsx | 4 +- .../property-detail/property-tabs/index.tsx | 4 +- .../property-documents/index.tsx | 14 +++--- .../property-tabs/property-info/index.tsx | 48 +++++++++--------- .../property-info/owner-card.tsx | 6 +-- .../property-value-card/index.tsx | 10 ++-- .../property-info/recent-activity-card.tsx | 6 +-- .../property-info/related-inquiries-card.tsx | 10 ++-- .../property-tabs/property-info/section.tsx | 6 +-- .../property-multimedia/index.tsx | 6 +-- .../property-filter-sheet/index.tsx | 2 +- .../components/property-list/index.tsx | 2 +- .../property-card-default/index.tsx | 26 +++++----- .../property-card-public/index.tsx | 24 ++++----- .../components/property-search/index.tsx | 2 +- 148 files changed, 784 insertions(+), 784 deletions(-) diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index 90091a2..c882506 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -7,8 +7,8 @@ export const metadata: Metadata = { }; export default function LoginPage() { return ( -
-
+
+
diff --git a/frontend/src/app/(auth)/register/page.tsx b/frontend/src/app/(auth)/register/page.tsx index e4fc750..f2a0313 100644 --- a/frontend/src/app/(auth)/register/page.tsx +++ b/frontend/src/app/(auth)/register/page.tsx @@ -9,8 +9,8 @@ export const metadata: Metadata = { export default function RegisterPage() { return ( -
-
+
+
diff --git a/frontend/src/app/(marketing)/_components/banner/index.tsx b/frontend/src/app/(marketing)/_components/banner/index.tsx index b07e2fa..a54ba8f 100644 --- a/frontend/src/app/(marketing)/_components/banner/index.tsx +++ b/frontend/src/app/(marketing)/_components/banner/index.tsx @@ -9,11 +9,11 @@ import { paths } from "@src/lib/paths"; export default function Banner() { return ( -
+
Banner @@ -24,7 +24,7 @@ export default function Banner() { size="lg" variant="default" asChild - className="w-full max-w-[167px] mx-auto" + className="mx-auto w-full max-w-[167px]" > Contáctanos diff --git a/frontend/src/app/(marketing)/_components/properties-list/index.tsx b/frontend/src/app/(marketing)/_components/properties-list/index.tsx index 2c05814..4c76807 100644 --- a/frontend/src/app/(marketing)/_components/properties-list/index.tsx +++ b/frontend/src/app/(marketing)/_components/properties-list/index.tsx @@ -21,7 +21,7 @@ export default async function PropertiesList() { ); return ( - + Propiedades disponibles @@ -29,7 +29,7 @@ export default async function PropertiesList() { {salesProperties.length > 0 ? ( -
+
{salesProperties.map((property) => ( {rentProperties.length > 0 ? ( -
+
{rentProperties.map((property) => ( diff --git a/frontend/src/app/(marketing)/contact/page.tsx b/frontend/src/app/(marketing)/contact/page.tsx index b6b268e..377b6e7 100644 --- a/frontend/src/app/(marketing)/contact/page.tsx +++ b/frontend/src/app/(marketing)/contact/page.tsx @@ -5,7 +5,7 @@ import GeneralContactForm from "./_components/general-contact-form"; export default function ContactPage() { return ( -
+
diff --git a/frontend/src/app/(marketing)/properties/[id]/_components/property-contact-form/index.tsx b/frontend/src/app/(marketing)/properties/[id]/_components/property-contact-form/index.tsx index ac0b89e..5ec1cf1 100644 --- a/frontend/src/app/(marketing)/properties/[id]/_components/property-contact-form/index.tsx +++ b/frontend/src/app/(marketing)/properties/[id]/_components/property-contact-form/index.tsx @@ -153,7 +153,7 @@ export default function PropertyContactForm({ -
+
{property.title}
-

+

{property.addresses[0].city.name}

@@ -69,7 +69,7 @@ export default async function PropertyDetailPage({ params }: Props) { {property.expenses[0].currency?.symbol || "USD"}{" "} @@ -80,16 +80,16 @@ export default async function PropertyDetailPage({ params }: Props) {
-

+

{property.description || "Sin descripción."}

-
+
-

Dirección

+

Dirección

{property.addresses[0].full_address || "-"} @@ -98,7 +98,7 @@ export default async function PropertyDetailPage({ params }: Props) {
-

Barrio

+

Barrio

{property.addresses[0].neighborhood || property.addresses[0].city?.name || @@ -109,7 +109,7 @@ export default async function PropertyDetailPage({ params }: Props) {
-

Tipo de operación

+

Tipo de operación

{property.prices .map((p) => p.operation_type.name) @@ -120,7 +120,7 @@ export default async function PropertyDetailPage({ params }: Props) {
-

Tipo de propiedad

+

Tipo de propiedad

{property.property_type.name} @@ -129,7 +129,7 @@ export default async function PropertyDetailPage({ params }: Props) {
-

Ambientes

+

Ambientes

{property.rooms_count} ambientes ({property.bedrooms_count}{" "} dormitorios) @@ -139,7 +139,7 @@ export default async function PropertyDetailPage({ params }: Props) {
-

Superficie total

+

Superficie total

{property.total_area} m² @@ -148,7 +148,7 @@ export default async function PropertyDetailPage({ params }: Props) {
-

Superficie cubierta

+

Superficie cubierta

{property.covered_area} m² @@ -157,7 +157,7 @@ export default async function PropertyDetailPage({ params }: Props) {
-
+
@@ -193,7 +193,7 @@ export default async function PropertyDetailPage({ params }: Props) {
-
+
{property.services && property.services.length > 0 ? ( -
+
{property.services.map((service) => (
{service.name} @@ -240,9 +240,9 @@ function InfoRow({ value: string | number | null | undefined; }) { return ( -
+
{label}: - + {value || "--"}
diff --git a/frontend/src/app/(protected)/admin/users/create/page.tsx b/frontend/src/app/(protected)/admin/users/create/page.tsx index ea900ed..56173dd 100644 --- a/frontend/src/app/(protected)/admin/users/create/page.tsx +++ b/frontend/src/app/(protected)/admin/users/create/page.tsx @@ -30,18 +30,18 @@ export default function CreateUserPage() { return (
-
-
+
+
- +

Volver a la lista de agentes

-

Nuevo agente

+

Nuevo agente

@@ -49,13 +49,13 @@ export default function CreateUserPage() { onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-4 shadow-create-user" > -
+
( - + Nombre * @@ -64,7 +64,7 @@ export default function CreateUserPage() { placeholder="Juan" {...field} value={field.value || ""} - className="placeholder:text-pagination-border border border-input-border shadow-input-border h-10" + className="h-10 border border-input-border shadow-input-border placeholder:text-pagination-border" /> @@ -77,7 +77,7 @@ export default function CreateUserPage() { name="last_name" render={({ field }) => ( - + Apellido * @@ -86,7 +86,7 @@ export default function CreateUserPage() { placeholder="Pérez" {...field} value={field.value || ""} - className="placeholder:text-pagination-border border border-input-border shadow-input-border h-10" + className="h-10 border border-input-border shadow-input-border placeholder:text-pagination-border" /> @@ -99,7 +99,7 @@ export default function CreateUserPage() { name="email" render={({ field }) => ( - + Correo electrónico * @@ -109,7 +109,7 @@ export default function CreateUserPage() { placeholder="correo@ejemplo.com" {...field} value={field.value || ""} - className="placeholder:text-pagination-border border border-input-border shadow-input-border h-10" + className="h-10 border border-input-border shadow-input-border placeholder:text-pagination-border" /> @@ -122,7 +122,7 @@ export default function CreateUserPage() { name="phone" render={({ field }) => ( - + Teléfono * @@ -131,7 +131,7 @@ export default function CreateUserPage() { placeholder="04121234567" {...field} value={field.value || ""} - className="placeholder:text-pagination-border border border-input-border shadow-input-border h-10" + className="h-10 border border-input-border shadow-input-border placeholder:text-pagination-border" /> @@ -144,7 +144,7 @@ export default function CreateUserPage() { name="password" render={({ field }) => ( - + Contraseña * @@ -155,12 +155,12 @@ export default function CreateUserPage() { placeholder="*********" {...field} value={field.value || ""} - className="placeholder:text-pagination-border border border-input-border shadow-input-border h-10" + className="h-10 border border-input-border shadow-input-border placeholder:text-pagination-border" />
-
+

Nota: Todos los nuevos agentes serán creados como Agentes (Rol: Agent). Solo los @@ -215,10 +215,10 @@ export default function CreateUserPage() { {error &&

{error}

} -
+
-
+
{user?.role === ROLES.AGENT ? ( diff --git a/frontend/src/components/protected-header/notifications-sheet/index.tsx b/frontend/src/components/protected-header/notifications-sheet/index.tsx index 15e8b38..cd33aaf 100644 --- a/frontend/src/components/protected-header/notifications-sheet/index.tsx +++ b/frontend/src/components/protected-header/notifications-sheet/index.tsx @@ -39,7 +39,7 @@ export default function NotificationsSheet() { diff --git a/frontend/src/components/protected-header/notifications-sheet/notification-card/index.tsx b/frontend/src/components/protected-header/notifications-sheet/notification-card/index.tsx index 729f43b..0d56edb 100644 --- a/frontend/src/components/protected-header/notifications-sheet/notification-card/index.tsx +++ b/frontend/src/components/protected-header/notifications-sheet/notification-card/index.tsx @@ -38,12 +38,12 @@ export default function NotificationCard({ return ( {IconComponent && ( -
+
)} @@ -59,7 +59,7 @@ export default function NotificationCard({ )} {schedule && ( -
+

{schedule}

diff --git a/frontend/src/components/protected-header/search-bar/index.tsx b/frontend/src/components/protected-header/search-bar/index.tsx index 5ca71bc..cf1a430 100644 --- a/frontend/src/components/protected-header/search-bar/index.tsx +++ b/frontend/src/components/protected-header/search-bar/index.tsx @@ -5,28 +5,28 @@ import { HomeIcon, Search, UserIcon } from "lucide-react"; export default function SearchBar() { return ( -
+
-
- - -
diff --git a/frontend/src/components/protected-header/user-menu-trigger/index.tsx b/frontend/src/components/protected-header/user-menu-trigger/index.tsx index 1727d7d..8f09e30 100644 --- a/frontend/src/components/protected-header/user-menu-trigger/index.tsx +++ b/frontend/src/components/protected-header/user-menu-trigger/index.tsx @@ -42,7 +42,7 @@ export default function UserMenuTrigger({ user }: Props) { CN -
+

{`${user.first_name} ${user.last_name}`}

diff --git a/frontend/src/components/public-header/index.tsx b/frontend/src/components/public-header/index.tsx index 56aad57..83d57e6 100644 --- a/frontend/src/components/public-header/index.tsx +++ b/frontend/src/components/public-header/index.tsx @@ -8,7 +8,7 @@ export default async function PublicHeader() { const settings = await getCompanySettings(); return ( -
+
{settings?.logo_url ? ( {settings.company_name ) : ( -
+
{showBackButton && (
diff --git a/frontend/src/components/sidebars/notification-sidebar/index.tsx b/frontend/src/components/sidebars/notification-sidebar/index.tsx index 7ae674b..3fef07a 100644 --- a/frontend/src/components/sidebars/notification-sidebar/index.tsx +++ b/frontend/src/components/sidebars/notification-sidebar/index.tsx @@ -53,7 +53,7 @@ export default function NotificationSidebar() { - diff --git a/frontend/src/components/sidebars/protected-sidebar/index.tsx b/frontend/src/components/sidebars/protected-sidebar/index.tsx index c8744b5..4bd4bbd 100644 --- a/frontend/src/components/sidebars/protected-sidebar/index.tsx +++ b/frontend/src/components/sidebars/protected-sidebar/index.tsx @@ -142,15 +142,15 @@ export default function ProtectedSidebar({ role }: Props) { } return ( - - + + {isLoading ? ( -
+
) : ( )} {role === ROLES.ADMIN && ( @@ -159,7 +159,7 @@ export default function ProtectedSidebar({ role }: Props) { @@ -176,18 +176,18 @@ export default function ProtectedSidebar({ role }: Props) { maxFiles={1} maxSize={2 * 1024 * 1024} > - + -

+

JPG o PNG, máximo 2MB

{selectedFile && ( -

+

{selectedFile.name}

)} @@ -241,7 +241,7 @@ export default function ProtectedSidebar({ role }: Props) { - + {item.items.map((subItem) => { const isSubItemActive = pathname === subItem.href; diff --git a/frontend/src/components/ui/accordion.tsx b/frontend/src/components/ui/accordion.tsx index b38297a..80a1571 100644 --- a/frontend/src/components/ui/accordion.tsx +++ b/frontend/src/components/ui/accordion.tsx @@ -35,13 +35,13 @@ function AccordionTrigger({ svg]:rotate-180", + "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", className, )} {...props} > {children} - + ); @@ -55,7 +55,7 @@ function AccordionContent({ return (
{children}
diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index fafc348..48b12a1 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -47,7 +47,7 @@ function AlertDialogOverlay({