commit ecd4edac022d0c62f9c625071d15532c7ad62e59 Author: Consult AS Technology Date: Fri Aug 21 22:27:36 2026 +0000 feat: ecosistema FRASA — apps Cotizador, Alta Clientes, Cobranza y Landing - Cotizador (/quote): 4 propuestas con formulas validadas del Excel, comparador, combobox de unidades, vigencia, PDF ANEXO A - Alta Clientes (/client): wizard 8 pasos, validacion CURP RENAPO, OCR de INE, checklist de expediente, firma digital, 5 PDFs legales - Cobranza (/pay): planes de enganche, mora, vista cobranza del dia, recibos PDF, bitacora, dashboard con filtros - Landing (/): entrada al ecosistema - Design system StudioBlank (monocromo, acento #1B4332) en las 4 apps - 123 tests vitest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa45378 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Dependencias y builds +node_modules/ +dist/ +*.tsbuildinfo + +# Entorno y secretos +.env +.env.* +!.env.example + +# Material de referencia (contiene datos personales de formatos reales) +reference/ + +# Sistema +.DS_Store +*.log diff --git a/apps/alta-clientes/.dockerignore b/apps/alta-clientes/.dockerignore new file mode 100644 index 0000000..7f1ee88 --- /dev/null +++ b/apps/alta-clientes/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.git +.gitignore +*.log +coverage +.vscode +Dockerfile +.dockerignore diff --git a/apps/alta-clientes/.gitignore b/apps/alta-clientes/.gitignore new file mode 100644 index 0000000..12b87fe --- /dev/null +++ b/apps/alta-clientes/.gitignore @@ -0,0 +1,10 @@ +node_modules +dist +dist-ssr +*.local +*.tsbuildinfo +coverage +.vscode/* +!.vscode/extensions.json +.DS_Store +*.log diff --git a/apps/alta-clientes/Dockerfile b/apps/alta-clientes/Dockerfile new file mode 100644 index 0000000..43d5c34 --- /dev/null +++ b/apps/alta-clientes/Dockerfile @@ -0,0 +1,14 @@ +# Etapa 1: build +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Etapa 2: servidor estático +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/alta-clientes/index.html b/apps/alta-clientes/index.html new file mode 100644 index 0000000..89c95eb --- /dev/null +++ b/apps/alta-clientes/index.html @@ -0,0 +1,12 @@ + + + + + + FRASA Desarrollos — Alta de Clientes + + +
+ + + diff --git a/apps/alta-clientes/nginx.conf b/apps/alta-clientes/nginx.conf new file mode 100644 index 0000000..18a9c34 --- /dev/null +++ b/apps/alta-clientes/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # SPA fallback: cualquier ruta no existente sirve index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Assets con hash: cache agresivo + location /client/assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/apps/alta-clientes/package-lock.json b/apps/alta-clientes/package-lock.json new file mode 100644 index 0000000..8c18196 --- /dev/null +++ b/apps/alta-clientes/package-lock.json @@ -0,0 +1,4390 @@ +{ + "name": "alta-clientes", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "alta-clientes", + "version": "0.0.0", + "dependencies": { + "@fontsource/inter": "^5.3.0", + "@hookform/resolvers": "^3.9.1", + "jspdf": "^2.5.2", + "jspdf-autotable": "^3.8.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-dropzone": "^14.3.5", + "react-hook-form": "^7.54.2", + "react-router-dom": "^7.1.1", + "react-signature-canvas": "^1.1.0-alpha.2", + "tesseract.js": "^7.0.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", + "typescript": "~5.6.3", + "vite": "^6.0.7", + "vitest": "^2.1.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "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/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/signature_pad": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/signature_pad/-/signature_pad-2.3.6.tgz", + "integrity": "sha512-v3j92gCQJoxomHhd+yaG4Vsf8tRS/XbzWKqDv85UsqjMGy4zhokuwKe4b6vhbgncKkh+thF+gpz6+fypTtnFqQ==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", + "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.411", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", + "integrity": "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/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/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/idb-keyval": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz", + "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", + "license": "Apache-2.0" + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/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/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/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.8.4.tgz", + "integrity": "sha512-rSffGoBsJYX83iTRv8Ft7FhqfgEL2nLpGAIiqruEQQ3e4r0qdLFbPUB7N9HAle0I3XgpisvyW751VHCqKUVOgQ==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2.5.1" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/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/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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/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.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-dropzone": { + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.4.1.tgz", + "integrity": "sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz", + "integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==", + "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": "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/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-signature-canvas": { + "version": "1.1.0-alpha.2", + "resolved": "https://registry.npmjs.org/react-signature-canvas/-/react-signature-canvas-1.1.0-alpha.2.tgz", + "integrity": "sha512-tKUNk3Gmh04Ug4K8p5g8Is08BFUKvbXxi0PyetQ/f8OgCBzcx4vqNf9+OArY/TdNdfHtswXQNRwZD6tyELjkjQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.17.9", + "@types/signature_pad": "^2.3.0", + "signature_pad": "^2.3.2", + "trim-canvas": "^0.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/agilgur5" + }, + "peerDependencies": { + "@types/prop-types": "^15.7.3", + "@types/react": "0.14 - 19", + "prop-types": "^15.5.8", + "react": "0.14 - 19", + "react-dom": "0.14 - 19" + }, + "peerDependenciesMeta": { + "@types/prop-types": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/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/signature_pad": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-2.3.2.tgz", + "integrity": "sha512-peYXLxOsIY6MES2TrRLDiNg2T++8gGbpP2yaC+6Ohtxr+a2dzoaqWosWDY9sWqTAAk6E/TyQO+LJw9zQwyu5kA==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "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/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tesseract.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-7.0.0.tgz", + "integrity": "sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^7.0.0", + "wasm-feature-detect": "^1.8.0", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-7.0.0.tgz", + "integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==", + "license": "Apache-2.0" + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/trim-canvas": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/trim-canvas/-/trim-canvas-0.1.2.tgz", + "integrity": "sha512-nd4Ga3iLFV94mdhW9JFMLpQbHUyCQuhFOD71PEAt1NjtMD5wbZctzhX8c3agHNybMR5zXD1XTGoIEWk995E6pQ==", + "license": "Apache-2.0" + }, + "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/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "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/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.9.0.tgz", + "integrity": "sha512-zonE+xlIIYtxPy++L24ow0hAD8CICb4+FgPyROd3buyXIqsJvUEDkBgfCCoXOd1Hu3DUr0GOfnPIdcGV+YpNaA==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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/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/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/apps/alta-clientes/package.json b/apps/alta-clientes/package.json new file mode 100644 index 0000000..55b845a --- /dev/null +++ b/apps/alta-clientes/package.json @@ -0,0 +1,37 @@ +{ + "name": "alta-clientes", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "preview": "vite preview" + }, + "dependencies": { + "@fontsource/inter": "^5.3.0", + "@hookform/resolvers": "^3.9.1", + "jspdf": "^2.5.2", + "jspdf-autotable": "^3.8.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-dropzone": "^14.3.5", + "react-hook-form": "^7.54.2", + "react-router-dom": "^7.1.1", + "react-signature-canvas": "^1.1.0-alpha.2", + "tesseract.js": "^7.0.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", + "typescript": "~5.6.3", + "vite": "^6.0.7", + "vitest": "^2.1.8" + } +} diff --git a/apps/alta-clientes/public/tessdata/spa.traineddata b/apps/alta-clientes/public/tessdata/spa.traineddata new file mode 100644 index 0000000..72e901f Binary files /dev/null and b/apps/alta-clientes/public/tessdata/spa.traineddata differ diff --git a/apps/alta-clientes/src/components/BotonOcrIne.tsx b/apps/alta-clientes/src/components/BotonOcrIne.tsx new file mode 100644 index 0000000..4e04e12 --- /dev/null +++ b/apps/alta-clientes/src/components/BotonOcrIne.tsx @@ -0,0 +1,145 @@ +import { useState } from 'react' +import type { DatosGenerales } from '../types' +import { parsearTextoIne } from '../lib/ocrIne' +import type { SugerenciasIne } from '../lib/ocrIne' + +type CamposAplicables = Partial> + +interface Props { + /** Imagen (JPG/PNG) de la credencial INE ya cargada */ + archivo: File + /** Aplica un campo sugerido al formulario de datos generales */ + onAplicar: (campos: CamposAplicables) => void +} + +type Estado = + | { fase: 'inactivo' } + | { fase: 'procesando'; progreso: number } + | { fase: 'listo'; sugerencias: SugerenciasIne } + | { fase: 'error'; mensaje: string } + +interface SugerenciaVisible { + clave: keyof CamposAplicables | 'sexo' + etiqueta: string + valor: string + /** Solo estos campos tienen destino en el formulario */ + aplicable: boolean +} + +function aVisibles(s: SugerenciasIne): SugerenciaVisible[] { + const v: SugerenciaVisible[] = [] + if (s.curp) v.push({ clave: 'curp', etiqueta: 'CURP', valor: s.curp, aplicable: true }) + if (s.nombre) v.push({ clave: 'nombreCompleto', etiqueta: 'Nombre', valor: s.nombre, aplicable: true }) + if (s.fechaNacimiento) + v.push({ clave: 'fechaNacimiento', etiqueta: 'Fecha de nacimiento', valor: s.fechaNacimiento, aplicable: true }) + if (s.sexo) v.push({ clave: 'sexo', etiqueta: 'Sexo', valor: s.sexo === 'H' ? 'Hombre' : 'Mujer', aplicable: false }) + return v +} + +/** + * OCR del INE con tesseract.js (import dinámico para no inflar el bundle). + * El idioma español se sirve local desde /client/tessdata (base path /client/). + */ +export default function BotonOcrIne({ archivo, onAplicar }: Props) { + const [estado, setEstado] = useState({ fase: 'inactivo' }) + const [aplicados, setAplicados] = useState([]) + + const leer = async () => { + setEstado({ fase: 'procesando', progreso: 0 }) + setAplicados([]) + try { + const { createWorker } = await import('tesseract.js') + const worker = await createWorker('spa', undefined, { + langPath: `${import.meta.env.BASE_URL}tessdata`, + gzip: false, + logger: (m: { status: string; progress: number }) => { + if (m.status === 'recognizing text') { + setEstado({ fase: 'procesando', progreso: Math.round(m.progress * 100) }) + } + }, + }) + try { + const { data } = await worker.recognize(archivo) + const sugerencias = parsearTextoIne(data.text) + if (aVisibles(sugerencias).length === 0) { + setEstado({ + fase: 'error', + mensaje: 'No se detectaron datos legibles en la imagen. Intenta con una foto más nítida.', + }) + } else { + setEstado({ fase: 'listo', sugerencias }) + } + } finally { + await worker.terminate() + } + } catch { + setEstado({ fase: 'error', mensaje: 'No fue posible ejecutar el OCR. Verifica tu conexión e inténtalo de nuevo.' }) + } + } + + const aplicar = (s: SugerenciaVisible) => { + if (!s.aplicable) return + onAplicar({ [s.clave]: s.valor } as CamposAplicables) + setAplicados((prev) => [...prev, s.clave]) + } + + return ( +
+
+

Lectura automática (OCR)

+ +
+ + {estado.fase === 'procesando' && ( +

+ Reconociendo texto de la credencial… {estado.progreso}% +

+ )} + + {estado.fase === 'error' && ( +

+ {estado.mensaje} +

+ )} + + {estado.fase === 'listo' && ( +
+

+ Sugerencias detectadas — nada se sobreescribe sin tu confirmación: +

+
    + {aVisibles(estado.sugerencias).map((s) => ( +
  • + + + {s.etiqueta} + + {s.valor} + + {s.aplicable && + (aplicados.includes(s.clave) ? ( + ✓ + ) : ( + + ))} +
  • + ))} +
+
+ )} +
+ ) +} diff --git a/apps/alta-clientes/src/components/Campos.tsx b/apps/alta-clientes/src/components/Campos.tsx new file mode 100644 index 0000000..7be0ed0 --- /dev/null +++ b/apps/alta-clientes/src/components/Campos.tsx @@ -0,0 +1,108 @@ +import { useFormContext, get } from 'react-hook-form' + +export interface CampoBaseProps { + nombre: string + etiqueta: string + requerido?: boolean + className?: string +} + +const inputClase = + 'w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm uppercase transition-colors duration-150 focus:border-neutral-900 focus:outline-none' + +const labelClase = 'mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500' + +function Etiqueta({ etiqueta, requerido }: { etiqueta: string; requerido?: boolean }) { + return ( + + {etiqueta} + {requerido && *} + + ) +} + +/** Clase del input según haya error de validación */ +function claseInput(nombre: string, errors: unknown): string { + return get(errors, nombre) ? `${inputClase} border-red-400` : inputClase +} + +export function MensajeError({ nombre }: { nombre: string }) { + const { + formState: { errors }, + } = useFormContext() + const error = get(errors, nombre) + if (!error) return null + return

{String(error.message)}

+} + +interface CampoTextoProps extends CampoBaseProps { + tipo?: string + placeholder?: string + soloLectura?: boolean +} + +/** Input de texto conectado a react-hook-form (soporta rutas anidadas con punto) */ +export function CampoTexto({ nombre, etiqueta, tipo = 'text', placeholder, requerido, soloLectura, className }: CampoTextoProps) { + const { register, formState: { errors } } = useFormContext() + return ( + + ) +} + +interface CampoNumeroProps extends CampoBaseProps { + paso?: string +} + +/** Input numérico; cadena vacía se guarda como 0 */ +export function CampoNumero({ nombre, etiqueta, requerido, paso = '0.01', className }: CampoNumeroProps) { + const { register, formState: { errors } } = useFormContext() + return ( + + ) +} + +interface CampoSelectProps extends CampoBaseProps { + opciones: { valor: string; etiqueta: string }[] +} + +export function CampoSelect({ nombre, etiqueta, opciones, requerido, className }: CampoSelectProps) { + const { register, formState: { errors } } = useFormContext() + return ( + + ) +} diff --git a/apps/alta-clientes/src/components/ChecklistExpediente.tsx b/apps/alta-clientes/src/components/ChecklistExpediente.tsx new file mode 100644 index 0000000..d63f7b2 --- /dev/null +++ b/apps/alta-clientes/src/components/ChecklistExpediente.tsx @@ -0,0 +1,34 @@ +import { estadoExpediente } from '../lib/expediente' +import type { DocumentoCargado } from '../types' + +interface Props { + documentos: DocumentoCargado[] + firmaDataUrl: string +} + +/** Panel en vivo del expediente: "Expediente: N/6" con estado plano por item */ +export default function ChecklistExpediente({ documentos, firmaDataUrl }: Props) { + const estado = estadoExpediente(documentos, firmaDataUrl) + return ( +
+

+ Expediente: {estado.completos}/{estado.total} +

+
    + {estado.items.map((item) => ( +
  • + {item.etiqueta} + {item.completo ? ( + ✓ + ) : ( + Pendiente + )} +
  • + ))} +
+
+ ) +} diff --git a/apps/alta-clientes/src/components/FileUploader.tsx b/apps/alta-clientes/src/components/FileUploader.tsx new file mode 100644 index 0000000..8106477 --- /dev/null +++ b/apps/alta-clientes/src/components/FileUploader.tsx @@ -0,0 +1,123 @@ +import { useCallback, useEffect, useState } from 'react' +import { useDropzone } from 'react-dropzone' +import type { DocumentoCargado, SlotDocumento } from '../types' +import { TAMANO_MAX_DOCUMENTO } from '../data/constantes' + +interface Props { + slot: SlotDocumento + etiqueta: string + documento?: DocumentoCargado + onAgregar: (doc: DocumentoCargado) => void + onQuitar: (slot: SlotDocumento) => void +} + +function formatoTamano(bytes: number): string { + return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB` +} + +/** Miniatura del documento: imagen con object-cover o recuadro plano para PDF */ +function Miniatura({ documento, url }: { documento: DocumentoCargado; url: string }) { + const esImagen = documento.tipoMime.startsWith('image/') + return ( + + {esImagen ? ( + {`Vista + ) : ( + + PDF + {documento.nombre} + + )} + + ) +} + +/** Slot de carga de un documento (PDF/JPG/PNG, máx 10 MB) con vista previa */ +export default function FileUploader({ slot, etiqueta, documento, onAgregar, onQuitar }: Props) { + const [urlObjeto, setUrlObjeto] = useState('') + + // URL de objeto para la miniatura y para abrir en pestaña nueva; se revoca al quitar/desmontar + useEffect(() => { + if (!documento) { + setUrlObjeto('') + return + } + const url = URL.createObjectURL(documento.archivo) + setUrlObjeto(url) + return () => URL.revokeObjectURL(url) + }, [documento]) + + const onDrop = useCallback( + (aceptados: File[]) => { + const archivo = aceptados[0] + if (!archivo) return + onAgregar({ slot, nombre: archivo.name, tamano: archivo.size, tipoMime: archivo.type, archivo }) + }, + [slot, onAgregar], + ) + + const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ + onDrop, + multiple: false, + maxSize: TAMANO_MAX_DOCUMENTO, + accept: { + 'application/pdf': ['.pdf'], + 'image/jpeg': ['.jpg', '.jpeg'], + 'image/png': ['.png'], + }, + }) + + if (documento) { + return ( +
+
+ {urlObjeto && } +
+

{etiqueta}

+

+ {documento.nombre} · {formatoTamano(documento.tamano)} +

+
+
+ +
+ ) + } + + return ( +
+
+ +

{etiqueta}

+

Arrastra el archivo aquí · PDF/JPG/PNG · máx 10 MB

+ +
+ {fileRejections.length > 0 && ( +

Archivo rechazado: formato no permitido o mayor a 10 MB

+ )} +
+ ) +} diff --git a/apps/alta-clientes/src/components/SectionCard.tsx b/apps/alta-clientes/src/components/SectionCard.tsx new file mode 100644 index 0000000..9eaf7da --- /dev/null +++ b/apps/alta-clientes/src/components/SectionCard.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from 'react' + +interface Props { + titulo: string + descripcion?: string + children: ReactNode +} + +/** Tarjeta de sección del wizard */ +export default function SectionCard({ titulo, descripcion, children }: Props) { + return ( +
+

{titulo}

+ {descripcion &&

{descripcion}

} +
{children}
+
+ ) +} diff --git a/apps/alta-clientes/src/components/SignaturePad.tsx b/apps/alta-clientes/src/components/SignaturePad.tsx new file mode 100644 index 0000000..afbdc4a --- /dev/null +++ b/apps/alta-clientes/src/components/SignaturePad.tsx @@ -0,0 +1,48 @@ +import { useRef, useEffect } from 'react' +import SignatureCanvas from 'react-signature-canvas' + +interface Props { + onFirma: (dataUrl: string) => void +} + +/** Pad de firma del cliente; exporta PNG base64 al terminar cada trazo */ +export default function SignaturePad({ onFirma }: Props) { + const ref = useRef(null) + + useEffect(() => { + // Notifica firma vacía al montar (estado inicial) + onFirma('') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const limpiar = () => { + ref.current?.clear() + onFirma('') + } + + const alTerminar = () => { + const pad = ref.current + if (!pad) return + onFirma(pad.isEmpty() ? '' : pad.getTrimmedCanvas().toDataURL('image/png')) + } + + return ( +
+
+ +
+ +
+ ) +} diff --git a/apps/alta-clientes/src/components/Stepper.tsx b/apps/alta-clientes/src/components/Stepper.tsx new file mode 100644 index 0000000..9773d28 --- /dev/null +++ b/apps/alta-clientes/src/components/Stepper.tsx @@ -0,0 +1,40 @@ +interface Props { + pasos: string[] + pasoActual: number + onIrAPaso: (paso: number) => void +} + +/** Stepper de progreso del wizard; permite volver a pasos ya visitados */ +export default function Stepper({ pasos, pasoActual, onIrAPaso }: Props) { + return ( + + ) +} diff --git a/apps/alta-clientes/src/data/constantes.ts b/apps/alta-clientes/src/data/constantes.ts new file mode 100644 index 0000000..53660f7 --- /dev/null +++ b/apps/alta-clientes/src/data/constantes.ts @@ -0,0 +1,79 @@ +import type { + EstadoCivil, + RegimenBienes, + TipoVivienda, + TipoCompra, + TipoEmpleo, + TipoEconomia, + TipoProducto, + TipoCredito, + SlotDocumento, +} from '../types' + +export const PROYECTOS = ['RESIDENCIAL MONTEBELLO', 'SENDA', 'DIAGONAL', 'OTRO'] as const + +export const ESTADOS_CIVILES: { valor: EstadoCivil; etiqueta: string }[] = [ + { valor: 'SOLTERO', etiqueta: 'Soltero(a)' }, + { valor: 'CASADO', etiqueta: 'Casado(a)' }, + { valor: 'UNION_LIBRE', etiqueta: 'Unión libre' }, + { valor: 'DIVORCIADO', etiqueta: 'Divorciado(a)' }, + { valor: 'VIUDO', etiqueta: 'Viudo(a)' }, +] + +export const REGIMENES_BIENES: { valor: RegimenBienes; etiqueta: string }[] = [ + { valor: 'SOCIEDAD_CONYUGAL', etiqueta: 'Sociedad conyugal' }, + { valor: 'SEPARACION_BIENES', etiqueta: 'Separación de bienes' }, +] + +export const TIPOS_VIVIENDA: { valor: TipoVivienda; etiqueta: string }[] = [ + { valor: 'PRESTADA', etiqueta: 'Prestada' }, + { valor: 'RENTADA', etiqueta: 'Rentada' }, + { valor: 'FAMILIAR', etiqueta: 'Familiar' }, + { valor: 'PROPIA', etiqueta: 'Propia' }, +] + +export const TIPOS_COMPRA: { valor: TipoCompra; etiqueta: string }[] = [ + { valor: 'INVERSION', etiqueta: 'Inversión' }, + { valor: 'VIVIR_AHI', etiqueta: 'Vivir ahí' }, +] + +export const TIPOS_EMPLEO: { valor: TipoEmpleo; etiqueta: string }[] = [ + { valor: 'ASALARIADO', etiqueta: 'Asalariado' }, + { valor: 'INDEPENDIENTE', etiqueta: 'Independiente' }, +] + +export const TIPOS_ECONOMIA: { valor: TipoEconomia; etiqueta: string }[] = [ + { valor: 'E_MEXICANA', etiqueta: 'Economía mexicana' }, + { valor: 'E_AMERICANA', etiqueta: 'Economía americana' }, + { valor: 'MIXTO', etiqueta: 'Mixta' }, +] + +export const TIPOS_PRODUCTO: { valor: TipoProducto; etiqueta: string }[] = [ + { valor: 'PESOS_FIJOS', etiqueta: 'Pesos fijos' }, + { valor: 'PESOS_CRECIENTES', etiqueta: 'Pesos crecientes' }, + { valor: 'VSM', etiqueta: 'VSM' }, +] + +export const TIPOS_CREDITO: { valor: TipoCredito; etiqueta: string }[] = [ + { valor: 'INFONAVIT', etiqueta: 'Infonavit' }, + { valor: 'APOYO_INFONAVIT', etiqueta: 'Apoyo Infonavit' }, + { valor: 'COFINAVIT', etiqueta: 'Cofinavit' }, + { valor: 'SEGUNDO_CREDITO', etiqueta: 'Segundo crédito' }, + { valor: 'CONTADO', etiqueta: 'Contado' }, + { valor: 'FOVISSSTE', etiqueta: 'Fovissste' }, + { valor: 'COFI_FOVISSSTE', etiqueta: 'Cofi-Fovissste' }, + { valor: 'BANCARIO', etiqueta: 'Bancario' }, +] + +export const SLOTS_DOCUMENTOS: { slot: SlotDocumento; etiqueta: string }[] = [ + { slot: 'INE', etiqueta: 'Identificación oficial (INE)' }, + { slot: 'CONSTANCIA_RFC', etiqueta: 'Constancia RFC / CSF' }, + { slot: 'CURP', etiqueta: 'CURP' }, + { slot: 'COMPROBANTE_DOMICILIO', etiqueta: 'Comprobante de domicilio' }, + { slot: 'ACTA_NACIMIENTO', etiqueta: 'Acta de nacimiento' }, +] + +export const TAMANO_MAX_DOCUMENTO = 10 * 1024 * 1024 // 10 MB + +export const ASESOR_DEFAULT = 'ESMERALDA BECERRA DOMINGUEZ' +export const AUTORIDAD_ID_DEFAULT = 'INSTITUTO NACIONAL ELECTORAL' diff --git a/apps/alta-clientes/src/data/textosLegales.ts b/apps/alta-clientes/src/data/textosLegales.ts new file mode 100644 index 0000000..01cabe7 --- /dev/null +++ b/apps/alta-clientes/src/data/textosLegales.ts @@ -0,0 +1,148 @@ +// Textos legales copiados textualmente de los formatos originales de Frasa Desarrollos +// (reference/alta-clientes/). NO corregir ortografía ni redacción: son documentos legales. + +/** Aviso de privacidad sinóptico — versión completa del Excel (excel_aviso_de_privacidad.txt) */ +export const AVISO_PRIVACIDAD_TITULO = 'AVISO DE PRIVACIDAD\n(SINOPTICO)' + +export const AVISO_PRIVACIDAD_PARRAFOS: string[] = [ + 'Se constituye el presente AVISO DE PRIVACIDAD de acuerdo a lo previsto en la Ley Federal de Protección de Datos Personales en Posesión de los Particulares, el reglamento de la misma, y demás leyes aplicables, declarando la moral, FRASA DESARROLLOS SAPI DE C.V, (en adelante “LA MORAL”) ser una Sociedad Mercantil legalmente constituida de conformidad con las leyes de los Estados Unidos Mexicanos, con domicilio en Blvd. Fundadores No. 3078-E, Colonia Juárez, Tijuana Baja California, misma que en adelante será responsable del tratamiento de sus datos personales y sensibles.', + '“LA MORAL” manifiesta que en cumplimiento de la Ley en referencia y su reglamento así como sus propios lineamientos corporativos, los datos personales y/o sensibles que usted nos proporcione, de manera enunciativa mas no limitativa se señalan como tales, nombre completo, domicilio, fecha de nacimiento, número telefónico, cuenta de correo particular, datos financieros, patrimoniales, entre otros; serán tratados como “Información Confidencial” y de conformidad con las leyes aplicables, y podrán ser recabados por “LA MORAL”, siempre y cuando el titular de los mismos datos personales y/o sensibles los proporcione voluntariamente a través de cualquier medio; manifestando que solo serán compartidos o transmitidos por empresas que integran el denominado grupo FRASA DESARROLLOS, no con personas distintas, ya que para evitar su divulgación toma las acciones necesarias y los procedimientos de seguridad que se requieran para ello. (A excepción de que sea solicitada por alguna autoridad, a quien le pudiera ser proporcionada).', + 'Derivado de lo anterior, el presente aviso de privacidad tiene como finalidad obtener información para realizar las gestiones correspondientes para la obtención de créditos, actualización de base de datos, relación comercial, de proveeduría, contractual, de servicios, y cualquier finalidad análoga o compatible con las anteriores, sin que la firma del presente implique una obligación de contratación o la celebración de una relación contractual. Asimismo, “LA MORAL” manifiesta que los datos personales, se podrán utilizar en campañas de publicidad y mercadotecnia, administración interna y otros fines análogos y lícitos.', + 'Se entiende que usted consciente tácitamente el tratamiento de sus datos personales y sensibles, cuando nos proporciona expresamente su autorización o voluntariamente su información, datos personales y sensibles por cualquier medio, y/o cuando se pone a su disposición el presente aviso de privacidad, y usted no manifiesta de manera expresa su oposición.', + 'Usted podrá actualizar su información o manifestar que no desea recibir información o revocar su consentimiento en cualquier momento sin que se le atribuyan efectos retroactivos, en caso de no contar con oposición expresa o especificación, “LA MORAL” podrá a través de los medios y procedimientos implementados libremente y a consideración de “LA MORAL”. Si desea conocer dichos medios y procedimientos, puede ponerse en contacto o podrá presentar sus solicitudes de derechos de acceso, rectificación, cancelación u oposición (ARCO) y/o la revocación de su consentimiento mediante envió al correo electrónico info@frasa.com.mx o al teléfono 6768100.', + 'Para mayor información acerca del contenido completo del presente, del tratamiento de los datos y de los derechos que puede hacer valer, usted puede acceder al aviso de privacidad integral a través del portal www.frasa.com.mx/aviso-de-privacidad/', + '“LA MORAL” se reserva el derecho de modificar o actualizar el presente aviso de privacidad, por lo que cualquier modificación que en el futuro hiciéramos al presente aviso, estará disponible en nuestro portal de internet, y será responsabilidad de quien proporciono los datos personales o sensibles revisar periódicamente el contenido del mismo.', +] + +/** Párrafo final del aviso: interpola el nombre del cliente */ +export function avisoPrivacidadParrafoFinal(nombreCliente: string): string { + return `El suscrito (a) ${nombreCliente} bajo protesta de decir verdad manifiesto expresamente que he leído el presente, me ha sido explicado y estoy enterado (a) del valor y fuerza legal del mismo, y doy autorización expresa a FRASA DESARROLLOS SAPI DE C.V. para obtener, compilar, almacenar, custodiar, tratar, compartir, trasmitir y usar la información y/o datos personales y sensibles, de conformidad con los establecido en el presente, y en las leyes aplicables.` +} + +/** Carta de no pagos en efectivo (anti-lavado) — texto completo del Excel (excel_no_pagos_en_efectivo.txt) */ +export function cartaAntilavadoCuerpo(proyecto: string): string { + return `Por medio de la presente le enviamos un cordial saludo y aprovechamos para hacer de su conocimiento que derivado de nuestro compromiso en brindarle un servicio de calidad y al mismo tiempo cumplir con la legislación mexicana, es necesario informarle que en relación al Contrato de Compraventa que desea celebrar con mi representada (Frasa Desarrollos SAPI C.V.) y respecto a su interés de adquirir algún condominio del proyecto ${proyecto}, de esta Ciudad de Tijuana, B.C.; se le hace de su conocimiento que todos los pagos que realice en efectivo para cubrir el precio del inmueble, no deberán sumar la cantidad de $600,000.00 Pesos (SEISCIENTOS MIL PESOS 00/100 MONEDA NACIONAL) o su equivalente en Dólares Moneda Americana, lo anterior como límite durante toda la vigencia del futuro Contrato de Compraventa y hasta que el precio del inmueble haya sido cubierto en su totalidad, y su entonces obligación de pago haya finalizado. Indicándole que los pagos a cuenta del precio del inmueble que elija, y que sean por cantidad superior a la citada, deberán efectuarse por medio de cheque de su cuenta personal o transferencia electrónica de su cuenta bancaria.` +} + +export const CARTA_ANTILAVADO_CIERRE = + 'Lo anterior es en cumplimiento a lo estatuido en el Artículo 32, fracción I y demás relativos y aplicables de la Ley Federal para la Prevención e Identificación de Operaciones con Recursos de Procedencia Ilícita y demás disposiciones legales vigentes sobre la materia, por lo cual se le hace de su conocimiento que en caso de realizar operaciones o pagos en efectivo por la cantidad mencionada anteriormente, no le serán recibidos, e implicaría la imposición de sanciones en materia fiscal y penal de conformidad con la legislación vigente.' + +export const CARTA_ANTILAVADO_DESPEDIDA = + 'Sin más por el momento, quedo a su total disposición para cualquier duda o aclaración.' + +// Textos del formato PLD de identificación de clientes (pdf_formatos.txt página 4 / excel_antilavado.txt) +export const PLD_TEXTO_PROTECCION_DATOS = + 'En términos de lo dispuesto por los Artículos 8, 15, 16, 17 y demás relativos y aplicables de la Ley Federal de Protección de Datos Personales en Posesión de los Particulares, el que suscribe manifiesta que se ha puesto a mi disposición el Aviso de Privacidad a que se refiere la mencionada ley, y se me ha informado que el mismo que se puede consultar en www.frasa.com.mx\nEn cumplimiento a lo dispuesto por la Ley Federal de Protección de Datos Personales en Posesión de los Particulares, así como del Artículo 14 de las reglas de carácter general a que se refiere la Ley Federal para la Prevención e Identificación de Operaciones con Recursos de Procedencia Ilícita, el que suscribe, otorgo mi consentimiento para que el vendedor y/o arrendador designe para la custodia del expediente que contiene mi información personal a Frasa Desarrollos SAPI C.V. , aceptando que dicha información sea proporcionada a cualquiera de las empresas que integran el denominado grupo "frasa desarrollos", para el caso de que realice con cualquiera de ellos, alguna otra operación; y/o con alguna autoridad que lo requiera.' + +export const PLD_TEXTO_DOCUMENTOS_INTEGRAN = + 'Bajo protesta de decir verdad manifiesto expresamente que los documentos que se agregan al presente formato y que se describen a continuación fueron proporcionados libremente por el suscrito, siendo copia fiel de los originales que se tuvieron a la vista, y que conozco las penas en que incurren quienes declaran con falsedad, de conformidad con lo señalado en el Artículo 62 de la Ley Federal de Prevención e Identificación de Operaciones con Recursos de Procedencia Ilícita, y demás leyes aplicables.' + +export const PLD_TEXTO_BENEFICIARIO = + 'Bajo protesta de decir verdad manifiesto que tengo conocimiento de la existencia del dueño beneficiario/beneficiario controlador/beneficiario efectivo, firmando al calce para constancia' + +export const PLD_TEXTO_RESPONSABILIDAD = + 'El suscrito asume totalmente la responsabilidad por cualquier dato falso o inexactitud que existiera en lo declarado en el presente y por la información o documentación proporcionada.' + +export const PLD_TEXTO_OTRO_FORMATO = + 'Llenar otro formato con los datos y documentos del dueño beneficiario/beneficiario controlador/beneficiario efectivo.' + +// Formato de identificación del beneficiario controlador (pdf_formatos.txt página 10) +export function beneficiarioControladorIntro(nombreCliente: string, ubicacion: string, condominio: string): string { + return `El/La suscrito/a ${nombreCliente} en virtud de la futura operación de compraventa respecto del inmueble denominado como unidad habitacional identificada tentativamente como número ${ubicacion} del Condominio Habitacional ${condominio} de esta Ciudad de Tijuana, B.C., que deseo celebrar con la moral FRASA DESARROLLOS, S.A.P.I. de C.V., por medio de la presente expresamente manifiesto lo siguiente: Que estoy consciente que la naturaleza jurídica de la futura operación de compraventa que deseo celebrar, constituye una actividad vulnerable que podrá ser materia de verificación por la Unidad Especializada en Análisis Financieros de la Procuraduría General de Justicia de la Republica o por la Secretaria de Hacienda y Crédito Publico conforme a lo previsto por los artículos 3, 8, 17 Fracción V y 18 de la Ley Federal para la Prevención e Identificación de Operaciones con Recursos de Procedencia Ilícita. Asimismo, en virtud de lo estipulado en el artículo 3 Fracción III de la Ley Federal para la Prevención e Identificación de Operaciones con Recursos de Procedencia Ilícita, respecto a la definición del Beneficiario Controlador, misma que a continuación es agregada a la letra:` +} + +export const BENEFICIARIO_CONTROLADOR_ARTICULO3 = + 'Artículo 3. Para los efectos de esta Ley, se entenderá por: I. Actividades Vulnerables, a las actividades que realicen las Entidades Financieras en términos del artículo 14 y a las que se refiere el artículo 17 de esta Ley; II. Avisos, a aquellos que deben presentarse en términos del artículo 17 de la presente Ley, así como a los reportes que deben presentar las entidades financieras en términos del artículo 15, fracción II, de esta Ley; III. Beneficiario Controlador, a la persona o grupo de personas que: a) Por medio de otra o de cualquier acto, obtiene el beneficio derivado de éstos y es quien, en última instancia, ejerce los derechos de uso, goce, disfrute, aprovechamiento o disposición de un bien o servicio, o b) Ejerce el control de aquella persona moral que, en su carácter de cliente o usuario, lleve a cabo actos u operaciones con quien realice Actividades Vulnerables, así como las personas por cuenta de quienes celebra alguno de ellos.' + +export const BENEFICIARIO_CONTROLADOR_MANIFIESTO = + 'Bajo protesta de decir verdad, manifiesto que soy el único Beneficiario Controlador de la Actividad Vulnerable que se realizará, consistente en la futura compraventa o transmisión de derechos reales sobre el inmueble antes citado, ya que NO existe ningún otro beneficiario, para lo cual se agrega la siguiente información:' + +export const BENEFICIARIO_CONTROLADOR_CIERRE = + 'Bajo protesta de decir verdad manifiesto expresamente que la información que se agrega, es proporcionada libremente por el/la suscrito/a, por lo que manifiesto que conozco las penas en que incurre quien proporcione de manera dolosa (a quienes deba dar información), documentación, datos o imágenes que sean falsos, o que declare con falsedad. Derivado de lo anterior, manifiesto mi conformidad en que esta información sea parte del archivo de identificación del cliente que ordena la legislación aplicable, firma la presente declaración bajo protesta de decir verdad, en la ciudad de Tijuana, B.C.' + +// Checklist de expediente (pdf_formatos.txt página 1) +export const CHECKLIST_BASICO: string[] = [ + 'Formatos Generales***', + 'Identificación oficial', + 'Validación Identificación*', + 'CURP', + 'RFC', + 'Acta de nacimiento original', + 'Comprobante de domicilio', + 'Cotizador firmado', + 'Carta compromiso', + 'Contrato original (3 tantos)', + 'Copia de recibos', +] + +export const CHECKLIST_ANTES_ESCRITURA: string[] = [ + 'Comprobación de ingresos', + 'Acta de nacimiento conyuge', + 'Acta de matrimonio', + 'Solicitud de crédito', + 'Buró de crédito', +] + +export const CHECKLIST_NOTAS: string[] = [ + 'Los expedientes básicos deberán ser entregados con el mínimo de información requerida por administración y finanzas para hacer válido el pago de comisión, esto refiere a entregar: **Generales, **formatos de ley anti-lavado, contrato correctamente impreso y firmado en 3 tantos, carta compromiso, cotizador firmado con esquema de pago autorizado al cliente, Identificación oficial y comprobante de domicilio, copia de los recibos de pago y comprobantes de los mismos.', + 'Los expedientes se deberán entregar una vez realizada la venta (pago de apartado + enganche + firma de contrato y carta compromiso) en un máximo de 48h posteriores a realizar la operación.', + 'Será responsabilidad del asesor de ventas y de Gerencia el seguimiento a la integración del expediente para completar el proceso de venta hasta la escrituración.', +] + +// Requisitos generales (pdf_formatos.txt página 8) +export const REQUISITOS_GENERALES: { titulo: string; items: string[] }[] = [ + { + titulo: 'Documentos Generales:', + items: [ + 'Solicitud de crédito hipotecario (En caso de Infonavit, hoja número 3)', + 'Autorización de consulta Buró de crédito', + 'Copia de identificación oficial vigente con fotografía y firma de los solicitantes', + 'Acta de nacimiento original y matrimonio (en su caso)', + 'Comprobante de domicilio', + ], + }, + { + titulo: 'Documentos de Ingresos en México:', + items: [ + 'Carta original de empleo (Firmada por un funcionario autorizado en papelería membretada y con sello de la empresa, que contenga nombre del solicitante, R.F.C., puesto, antigüedad en la empresa, número de seguro social o de fovissste e ingresos mensuales brutos)', + 'Últimos 3 meses de recibos de nómina originales', + 'Estados de cuenta bancarios de 3 meses donde se refleje el concepto de abono de nómina', + ], + }, + { + titulo: 'Personas Físicas con actividad empresarial:', + items: [ + 'Copia de alta en Hacienda debidamente sellada', + 'Copia de la Declaración anual de los 2 últimos ejercicios y parciales del año en curso', + 'Copia de estados de cuenta de cheques de los últimos 6 meses', + ], + }, + { + titulo: 'Documento de ingresos en U.S.A.', + items: [ + 'Comprobante de Domicilio Reciente (no mayor a 3 meses)', + 'Carta original de empleo', + 'Formato W2 y 1040 (Income Tax) de 1 año en el mismo trabajo', + 'Recibos de nómina de 3 meses', + 'Reporte detallado de Buró de Crédito en U.S.A., con score superior a 680 puntos', + 'Copia de estados de cuenta de cheques de los últimos 6 meses', + 'Copia de Social Security', + ], + }, + { + titulo: 'ADEMÁS: Si es Persona Fisica con actividad empresarial formal o informal en U.S.A.', + items: [ + 'Carta a título personal de actividad (Mencionar ingreso promedio mensual, tipo de negocio y fecha de inicio)', + 'Copia de 6 meses de estados de cuenta', + 'Formato 1099 a 1040 (Income Tax del año anterior)', + 'Permiso de operación Vigente', + '3 Referencias en U.S.A.', + ], + }, +] + +export const REQUISITOS_CONTACTO_ENTREGA = + 'Contacto: Graciela Bautista Velasco\nEJECUTIVO DE CRÉDITO IN-HOUSE\nOficina: (664) 676 8120\nCorreo: gbautista@frasa.com.mx' + +export const REQUISITOS_CONTACTO_CONTRATO = + 'Asesor: ALEJANDRA GUERRERO SANCHEZ\nOficina: COLINAS DE SAN PABLO 17516 COLINAS DE LA PRESA 221\nCelular: 6644275217\nCorreo: auxventas@frasa.com.mx' diff --git a/apps/alta-clientes/src/forms/CoacreditadoForm.tsx b/apps/alta-clientes/src/forms/CoacreditadoForm.tsx new file mode 100644 index 0000000..a7e711d --- /dev/null +++ b/apps/alta-clientes/src/forms/CoacreditadoForm.tsx @@ -0,0 +1,45 @@ +import { useForm, FormProvider } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import type { Coacreditado } from '../types' +import { coacreditadoSchema } from '../lib/schemas' +import DatosGeneralesCampos from './DatosGeneralesCampos' +import EmpleoCampos from './EmpleoCampos' +import EconomicosCampos from './EconomicosCampos' +import Navegacion from './Navegacion' +import type { PasoProps } from './DatosGeneralesForm' + +/** Paso 4: co-acreditado o cónyuge (opcional, toggle) */ +export default function CoacreditadoForm({ valores, onAnterior, onSiguiente }: PasoProps) { + const metodos = useForm({ + resolver: zodResolver(coacreditadoSchema), + defaultValues: valores, + mode: 'onSubmit', + }) + const tieneCoacreditado = metodos.watch('tieneCoacreditado') + + return ( + +
+ + + {tieneCoacreditado && ( +
+ + + +
+ )} + + + +
+ ) +} diff --git a/apps/alta-clientes/src/forms/DatosGeneralesCampos.tsx b/apps/alta-clientes/src/forms/DatosGeneralesCampos.tsx new file mode 100644 index 0000000..a3172ac --- /dev/null +++ b/apps/alta-clientes/src/forms/DatosGeneralesCampos.tsx @@ -0,0 +1,69 @@ +import { useFormContext } from 'react-hook-form' +import { CampoTexto, CampoSelect } from '../components/Campos' +import SectionCard from '../components/SectionCard' +import { compararConDatos } from '../lib/curp' +import { ESTADOS_CIVILES, REGIMENES_BIENES, TIPOS_VIVIENDA, TIPOS_COMPRA } from '../data/constantes' + +interface Props { + prefix: string + compacto?: boolean +} + +/** Campos de datos generales; reutilizable con prefijo (cliente y co-acreditado) */ +export default function DatosGeneralesCampos({ prefix, compacto = false }: Props) { + const { watch } = useFormContext() + const p = (campo: string) => `${prefix}${campo}` + const estadoCivil = watch(p('estadoCivil')) as string + const curp = (watch(p('curp')) as string) ?? '' + const fechaNacimiento = (watch(p('fechaNacimiento')) as string) ?? '' + const lugarNacimiento = (watch(p('lugarNacimiento')) as string) ?? '' + // Advertencias ámbar en vivo: no bloquean, solo alertan inconsistencias con la CURP + const advertenciasCurp = compararConDatos(curp, { fechaNacimiento, lugarNacimiento }) + + return ( + + {!compacto && } + + + + + + {advertenciasCurp.length > 0 && ( +
+ {advertenciasCurp.map((aviso) => ( +

+ {aviso} +

+ ))} +
+ )} + + + + + + + + + + {estadoCivil === 'CASADO' && ( + + )} + + + + + + + + + {!compacto && ( + + )} +
+ ) +} diff --git a/apps/alta-clientes/src/forms/DatosGeneralesForm.tsx b/apps/alta-clientes/src/forms/DatosGeneralesForm.tsx new file mode 100644 index 0000000..b96ef91 --- /dev/null +++ b/apps/alta-clientes/src/forms/DatosGeneralesForm.tsx @@ -0,0 +1,30 @@ +import { useForm, FormProvider } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import type { DatosGenerales } from '../types' +import { datosGeneralesSchema } from '../lib/schemas' +import DatosGeneralesCampos from './DatosGeneralesCampos' +import Navegacion from './Navegacion' + +export interface PasoProps { + valores: T + onAnterior?: () => void + onSiguiente: (datos: T) => void +} + +/** Paso 1: datos generales del cliente */ +export default function DatosGeneralesForm({ valores, onSiguiente }: PasoProps) { + const metodos = useForm({ + resolver: zodResolver(datosGeneralesSchema), + defaultValues: valores, + mode: 'onSubmit', + }) + + return ( + +
+ + + +
+ ) +} diff --git a/apps/alta-clientes/src/forms/DocumentosForm.tsx b/apps/alta-clientes/src/forms/DocumentosForm.tsx new file mode 100644 index 0000000..f553a37 --- /dev/null +++ b/apps/alta-clientes/src/forms/DocumentosForm.tsx @@ -0,0 +1,59 @@ +import type { DatosGenerales, DocumentoCargado, SlotDocumento } from '../types' +import FileUploader from '../components/FileUploader' +import ChecklistExpediente from '../components/ChecklistExpediente' +import BotonOcrIne from '../components/BotonOcrIne' +import SectionCard from '../components/SectionCard' +import Navegacion from './Navegacion' +import { SLOTS_DOCUMENTOS } from '../data/constantes' + +type CamposAplicables = Partial> + +interface Props { + documentos: DocumentoCargado[] + firmaDataUrl: string + onAgregar: (doc: DocumentoCargado) => void + onQuitar: (slot: SlotDocumento) => void + onAplicarDatosGenerales: (campos: CamposAplicables) => void + onAnterior: () => void + onSiguiente: () => void +} + +/** Paso 7: carga de documentos del expediente (sin backend, se guardan en estado) */ +export default function DocumentosForm({ + documentos, + firmaDataUrl, + onAgregar, + onQuitar, + onAplicarDatosGenerales, + onAnterior, + onSiguiente, +}: Props) { + const ine = documentos.find((d) => d.slot === 'INE') + return ( +
+
+ +
+ + {SLOTS_DOCUMENTOS.map(({ slot, etiqueta }) => ( +
+ d.slot === slot)} + onAgregar={onAgregar} + onQuitar={onQuitar} + /> + {slot === 'INE' && ine && ine.tipoMime.startsWith('image/') && ( + + )} +
+ ))} +
+ +
+ ) +} diff --git a/apps/alta-clientes/src/forms/EconomicosCampos.tsx b/apps/alta-clientes/src/forms/EconomicosCampos.tsx new file mode 100644 index 0000000..601bd1f --- /dev/null +++ b/apps/alta-clientes/src/forms/EconomicosCampos.tsx @@ -0,0 +1,51 @@ +import { useFormContext } from 'react-hook-form' +import { CampoNumero, CampoTexto } from '../components/Campos' +import SectionCard from '../components/SectionCard' +import { formatoMoneda } from '../lib/formatters' + +interface Props { + prefix: string +} + +/** Campos de datos económicos con totales calculados de solo lectura */ +export default function EconomicosCampos({ prefix }: Props) { + const { watch } = useFormContext() + const p = (campo: string) => `${prefix}${campo}` + + const ingresosMensuales = Number(watch(p('ingresosMensuales'))) || 0 + const otrosIngresos = Number(watch(p('otrosIngresos'))) || 0 + const gastosLuz = Number(watch(p('gastosLuz'))) || 0 + const gastosOtros = Number(watch(p('gastosOtros'))) || 0 + + return ( + + + + + + + + + + + + ) +} diff --git a/apps/alta-clientes/src/forms/EconomicosForm.tsx b/apps/alta-clientes/src/forms/EconomicosForm.tsx new file mode 100644 index 0000000..bebf4e8 --- /dev/null +++ b/apps/alta-clientes/src/forms/EconomicosForm.tsx @@ -0,0 +1,25 @@ +import { useForm, FormProvider } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import type { DatosEconomicos } from '../types' +import { economicosSchema } from '../lib/schemas' +import EconomicosCampos from './EconomicosCampos' +import Navegacion from './Navegacion' +import type { PasoProps } from './DatosGeneralesForm' + +/** Paso 3: datos económicos del cliente */ +export default function EconomicosForm({ valores, onAnterior, onSiguiente }: PasoProps) { + const metodos = useForm({ + resolver: zodResolver(economicosSchema), + defaultValues: valores, + mode: 'onSubmit', + }) + + return ( + +
+ + + +
+ ) +} diff --git a/apps/alta-clientes/src/forms/EmpleoCampos.tsx b/apps/alta-clientes/src/forms/EmpleoCampos.tsx new file mode 100644 index 0000000..978ae20 --- /dev/null +++ b/apps/alta-clientes/src/forms/EmpleoCampos.tsx @@ -0,0 +1,30 @@ +import { CampoTexto, CampoSelect } from '../components/Campos' +import SectionCard from '../components/SectionCard' +import { TIPOS_EMPLEO, TIPOS_ECONOMIA } from '../data/constantes' + +interface Props { + prefix: string +} + +/** Campos de datos de empleo; reutilizable con prefijo (cliente y co-acreditado) */ +export default function EmpleoCampos({ prefix }: Props) { + const p = (campo: string) => `${prefix}${campo}` + + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/apps/alta-clientes/src/forms/EmpleoForm.tsx b/apps/alta-clientes/src/forms/EmpleoForm.tsx new file mode 100644 index 0000000..55439a4 --- /dev/null +++ b/apps/alta-clientes/src/forms/EmpleoForm.tsx @@ -0,0 +1,25 @@ +import { useForm, FormProvider } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import type { DatosEmpleo } from '../types' +import { empleoSchema } from '../lib/schemas' +import EmpleoCampos from './EmpleoCampos' +import Navegacion from './Navegacion' +import type { PasoProps } from './DatosGeneralesForm' + +/** Paso 2: datos de empleo del cliente */ +export default function EmpleoForm({ valores, onAnterior, onSiguiente }: PasoProps) { + const metodos = useForm({ + resolver: zodResolver(empleoSchema), + defaultValues: valores, + mode: 'onSubmit', + }) + + return ( + +
+ + + +
+ ) +} diff --git a/apps/alta-clientes/src/forms/FirmasForm.tsx b/apps/alta-clientes/src/forms/FirmasForm.tsx new file mode 100644 index 0000000..aa05356 --- /dev/null +++ b/apps/alta-clientes/src/forms/FirmasForm.tsx @@ -0,0 +1,172 @@ +import { useState } from 'react' +import type { AltaCliente, DatosFirma } from '../types' +import { firmaSchema } from '../lib/schemas' +import { formatoMoneda, formatoFechaCorta } from '../lib/formatters' +import { calcularTotalIngresos } from '../lib/calculations' +import { estadoExpediente } from '../lib/expediente' +import SectionCard from '../components/SectionCard' +import SignaturePad from '../components/SignaturePad' +import ChecklistExpediente from '../components/ChecklistExpediente' +import * as perfilCliente from '../lib/pdf/perfilCliente' +import * as identificacionCliente from '../lib/pdf/identificacionCliente' +import * as avisoPrivacidad from '../lib/pdf/avisoPrivacidad' +import * as cartaAntilavado from '../lib/pdf/cartaAntilavado' +import * as checklistExpediente from '../lib/pdf/checklistExpediente' + +interface Props { + alta: AltaCliente + onCambiarFirma: (firma: DatosFirma) => void + onAnterior: () => void +} + +const GENERADORES: { tipo: string; descargar: (alta: AltaCliente) => void }[] = [ + { tipo: 'Perfil del cliente', descargar: perfilCliente.descargar }, + { tipo: 'Identificación de cliente (PLD)', descargar: identificacionCliente.descargar }, + { tipo: 'Aviso de privacidad', descargar: avisoPrivacidad.descargar }, + { tipo: 'Carta anti-lavado', descargar: cartaAntilavado.descargar }, + { tipo: 'Checklist de expediente', descargar: checklistExpediente.descargar }, +] + +/** Paso 8: firma del cliente, nombre del asesor, resumen y generación de PDFs */ +export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props) { + const [error, setError] = useState('') + const [avisoExpediente, setAvisoExpediente] = useState('') + const { datosGenerales: g, viviendaCredito: v, economicos, firma } = alta + const proyecto = v.proyecto === 'OTRO' ? v.proyectoOtro : v.proyecto + const expediente = estadoExpediente(alta.documentos, firma.firmaDataUrl) + + const validar = (): boolean => { + const r = firmaSchema.safeParse(firma) + setError(r.success ? '' : (r.error.issues[0]?.message ?? 'Revisa la firma')) + return r.success + } + + // Advierte (sin bloquear) si el expediente está incompleto al generar PDFs + const avisarExpediente = () => { + setAvisoExpediente( + expediente.completoTodo + ? '' + : `Expediente incompleto (${expediente.completos}/${expediente.total}): puedes generar los PDFs, pero faltan documentos o la firma.`, + ) + } + + const generar = (descargar: (a: AltaCliente) => void) => { + avisarExpediente() + if (validar()) descargar(alta) + } + + const generarTodos = () => { + avisarExpediente() + if (!validar()) return + for (const g2 of GENERADORES) g2.descargar(alta) + } + + return ( +
+ +
+
+
Cliente
+
{g.nombreCompleto}
+
+
+
CURP
+
{g.curp}
+
+
+
RFC
+
{g.rfc}
+
+
+
Proyecto
+
{proyecto || '—'}
+
+
+
Ubicación / Modelo
+
+ {v.ubicacion || '—'} {v.modelo} +
+
+
+
Valor venta
+
{formatoMoneda(Number(v.valorVenta) || 0)}
+
+
+
Total ingresos
+
{formatoMoneda(calcularTotalIngresos(economicos))}
+
+
+
Fecha
+
{formatoFechaCorta(g.fecha)}
+
+
+
Documentos
+
{alta.documentos.length} de 5
+
+
+
+ + + + +
+ onCambiarFirma({ ...firma, firmaDataUrl: dataUrl })} /> +
+ +
+ + {error &&

{error}

} + + + {avisoExpediente && ( +

+ {avisoExpediente} +

+ )} +
+ {GENERADORES.map((g3) => ( + + ))} + +
+
+ +
+ +
+
+ ) +} diff --git a/apps/alta-clientes/src/forms/Navegacion.tsx b/apps/alta-clientes/src/forms/Navegacion.tsx new file mode 100644 index 0000000..872e029 --- /dev/null +++ b/apps/alta-clientes/src/forms/Navegacion.tsx @@ -0,0 +1,32 @@ +interface Props { + onAnterior?: () => void + /** Si se pasa, el botón Siguiente es type="button" (pasos sin formulario) */ + onSiguiente?: () => void + textoSiguiente?: string +} + +/** Botones de navegación del wizard */ +export default function Navegacion({ onAnterior, onSiguiente, textoSiguiente = 'Siguiente' }: Props) { + return ( +
+ {onAnterior ? ( + + ) : ( + + )} + +
+ ) +} diff --git a/apps/alta-clientes/src/forms/ReferenciasForm.tsx b/apps/alta-clientes/src/forms/ReferenciasForm.tsx new file mode 100644 index 0000000..4a77715 --- /dev/null +++ b/apps/alta-clientes/src/forms/ReferenciasForm.tsx @@ -0,0 +1,52 @@ +import { z } from 'zod' +import { useForm, FormProvider } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import type { Referencia } from '../types' +import { referenciasSchema } from '../lib/schemas' +import { CampoTexto } from '../components/Campos' +import SectionCard from '../components/SectionCard' +import Navegacion from './Navegacion' +import type { PasoProps } from './DatosGeneralesForm' + +// RHF trabaja con objetos raíz; el array va envuelto +const schemaFormulario = z.object({ referencias: referenciasSchema }) + +/** Paso 5: referencias personales (4 filas, mínimo 1 con nombre y teléfono) */ +export default function ReferenciasForm({ valores, onAnterior, onSiguiente }: PasoProps) { + const metodos = useForm<{ referencias: Referencia[] }>({ + resolver: zodResolver(schemaFormulario), + defaultValues: { referencias: valores }, + mode: 'onSubmit', + }) + const errorRaiz = metodos.formState.errors.referencias + + return ( + +
onSiguiente(datos.referencias))} + noValidate + > + + {[0, 1, 2, 3].map((i) => ( +
+

Referencia {i + 1}

+
+ + + + +
+
+ ))} +
+ {errorRaiz && typeof errorRaiz.message === 'string' && ( +

{errorRaiz.message}

+ )} + + +
+ ) +} diff --git a/apps/alta-clientes/src/forms/ViviendaCreditoForm.tsx b/apps/alta-clientes/src/forms/ViviendaCreditoForm.tsx new file mode 100644 index 0000000..6402f64 --- /dev/null +++ b/apps/alta-clientes/src/forms/ViviendaCreditoForm.tsx @@ -0,0 +1,112 @@ +import { useForm, FormProvider } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import type { ViviendaCredito, TipoCredito } from '../types' +import { viviendaCreditoSchema } from '../lib/schemas' +import { CampoTexto, CampoSelect, MensajeError } from '../components/Campos' +import SectionCard from '../components/SectionCard' +import Navegacion from './Navegacion' +import type { PasoProps } from './DatosGeneralesForm' +import { PROYECTOS, TIPOS_CREDITO, TIPOS_PRODUCTO } from '../data/constantes' + +/** Paso 6: datos de la vivienda, crédito e identificación oficial (PLD) */ +export default function ViviendaCreditoForm({ valores, onAnterior, onSiguiente }: PasoProps) { + const metodos = useForm({ + resolver: zodResolver(viviendaCreditoSchema), + defaultValues: valores, + mode: 'onSubmit', + }) + const proyecto = metodos.watch('proyecto') + const tiposSeleccionados = metodos.watch('tiposCredito') ?? [] + + const alternarCredito = (valor: TipoCredito) => { + const actuales = metodos.getValues('tiposCredito') ?? [] + metodos.setValue( + 'tiposCredito', + actuales.includes(valor) ? actuales.filter((t) => t !== valor) : [...actuales, valor], + { shouldDirty: true }, + ) + } + + return ( + +
+
+ + ({ valor: p, etiqueta: p === 'OTRO' ? 'Otro' : p }))} + /> + {proyecto === 'OTRO' && ( + + )} + + + + + + + + + + + + + +
+
+ {TIPOS_CREDITO.map((tc) => ( + + ))} +
+
+ + + + +
+ + + + + + + + + + + + + + + + + +
+ + + +
+ ) +} diff --git a/apps/alta-clientes/src/hooks/useAltaCliente.ts b/apps/alta-clientes/src/hooks/useAltaCliente.ts new file mode 100644 index 0000000..190413f --- /dev/null +++ b/apps/alta-clientes/src/hooks/useAltaCliente.ts @@ -0,0 +1,148 @@ +import { useState } from 'react' +import type { AltaCliente } from '../types' +import { fechaISOHoy } from '../lib/formatters' +import { ASESOR_DEFAULT, AUTORIDAD_ID_DEFAULT } from '../data/constantes' +import { useDocumentos } from './useDocumentos' + +const datosGeneralesVacios = (): AltaCliente['datosGenerales'] => ({ + campaniaVenta: '', + fecha: fechaISOHoy(), + nombreCompleto: '', + nacionalidad: 'MEXICANA', + nivelEstudios: '', + curp: '', + rfc: '', + nss: '', + delegacionMunicipio: '', + direccion: '', + codigoPostal: '', + antiguedadDomicilio: '', + fechaNacimiento: '', + lugarNacimiento: '', + estadoCivil: '', + regimenBienes: '', + peso: '', + estatura: '', + email: '', + telCasa: '', + celular: '', + viviendaAnios: '', + tipoVivienda: '', + tipoCompra: '', + actividadOcupacion: '', +}) + +const empleoVacio = (): AltaCliente['empleo'] => ({ + tipoEmpleo: '', + tipoEconomia: '', + compania: '', + antiguedadAnios: '', + antiguedadMeses: '', + direccion: '', + colonia: '', + numero: '', + codigoPostal: '', + puesto: '', + telefono: '', + ext: '', + delegacion: '', +}) + +const economicosVacios = (): AltaCliente['economicos'] => ({ + ingresosMensuales: 0, + otrosIngresos: 0, + gastosLuz: 0, + gastosOtros: 0, + dependientes: { personas: '', parentesco: '', edad: '' }, +}) + +const estadoInicial = (): Omit => ({ + datosGenerales: datosGeneralesVacios(), + empleo: empleoVacio(), + economicos: economicosVacios(), + coacreditado: { + tieneCoacreditado: false, + generales: datosGeneralesVacios(), + empleo: empleoVacio(), + economicos: economicosVacios(), + }, + referencias: [ + { nombre: '', telefono: '', parentesco: '', direccion: '' }, + { nombre: '', telefono: '', parentesco: '', direccion: '' }, + { nombre: '', telefono: '', parentesco: '', direccion: '' }, + { nombre: '', telefono: '', parentesco: '', direccion: '' }, + ], + viviendaCredito: { + proyecto: '', + proyectoOtro: '', + modelo: '', + manzana: '', + lote: '', + ubicacion: '', + superficieM2: '', + valorVenta: '', + enganche: '', + equipamientoAdicional: '', + fechaPagoEnganche: '', + firmaContrato: '', + tiposCredito: [], + montoCreditoInfoFovi: '', + ahorroVoluntarioSubcuenta: '', + tipoProducto: '', + descuentoMensualInfoFovissste: '', + idAutoridadEmisora: AUTORIDAD_ID_DEFAULT, + idNumero: '', + idVigencia: '', + domicilioFiscal: { + calle: '', + numExt: '', + numInt: '', + colonia: '', + municipio: '', + estado: '', + cp: '', + pais: 'MEXICO', + }, + }, + firma: { firmaDataUrl: '', asesorNombre: ASESOR_DEFAULT }, +}) + +export const PASOS = [ + 'Datos generales', + 'Empleo', + 'Económicos', + 'Co-acreditado', + 'Referencias', + 'Vivienda y crédito', + 'Documentos', + 'Firmas y generar', +] as const + +/** Estado global del wizard de alta de clientes */ +export function useAltaCliente() { + const [paso, setPaso] = useState(0) + const [datos, setDatos] = useState(estadoInicial) + const { documentos, agregarDocumento, quitarDocumento } = useDocumentos() + + const alta: AltaCliente = { ...datos, documentos } + + const actualizar = (seccion: K, valores: (typeof datos)[K]) => { + setDatos((prev) => ({ ...prev, [seccion]: valores })) + } + + const siguiente = () => setPaso((p) => Math.min(p + 1, PASOS.length - 1)) + const anterior = () => setPaso((p) => Math.max(p - 1, 0)) + const irAPaso = (p: number) => setPaso(p) + + return { + paso, + siguiente, + anterior, + irAPaso, + datos, + alta, + actualizar, + agregarDocumento, + quitarDocumento, + } +} diff --git a/apps/alta-clientes/src/hooks/useDocumentos.ts b/apps/alta-clientes/src/hooks/useDocumentos.ts new file mode 100644 index 0000000..e900639 --- /dev/null +++ b/apps/alta-clientes/src/hooks/useDocumentos.ts @@ -0,0 +1,17 @@ +import { useCallback, useState } from 'react' +import type { DocumentoCargado, SlotDocumento } from '../types' + +/** Maneja los documentos del expediente en memoria (uno por slot) */ +export function useDocumentos() { + const [documentos, setDocumentos] = useState([]) + + const agregarDocumento = useCallback((doc: DocumentoCargado) => { + setDocumentos((prev) => [...prev.filter((d) => d.slot !== doc.slot), doc]) + }, []) + + const quitarDocumento = useCallback((slot: SlotDocumento) => { + setDocumentos((prev) => prev.filter((d) => d.slot !== slot)) + }, []) + + return { documentos, agregarDocumento, quitarDocumento } +} diff --git a/apps/alta-clientes/src/index.css b/apps/alta-clientes/src/index.css new file mode 100644 index 0000000..7b9719e --- /dev/null +++ b/apps/alta-clientes/src/index.css @@ -0,0 +1,23 @@ +@import "tailwindcss"; + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; + --color-accent: #1B4332; + --radius-sm: 0; + --radius-md: 0; + --radius-lg: 0; + --radius-xl: 0; + --radius-2xl: 0; + --radius-3xl: 0; +} + +@layer base { + body { + @apply bg-white text-neutral-900 antialiased; + } + + :focus-visible { + outline: 2px solid #1b4332; + outline-offset: 2px; + } +} diff --git a/apps/alta-clientes/src/lib/calculations.test.ts b/apps/alta-clientes/src/lib/calculations.test.ts new file mode 100644 index 0000000..99caa16 --- /dev/null +++ b/apps/alta-clientes/src/lib/calculations.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest' +import { calcularTotalIngresos, calcularGastosTotal } from './calculations' + +describe('calcularTotalIngresos', () => { + it('suma ingresos mensuales y otros ingresos', () => { + expect(calcularTotalIngresos({ ingresosMensuales: 54988, otrosIngresos: 0 })).toBe(54988) + expect(calcularTotalIngresos({ ingresosMensuales: 20000, otrosIngresos: 5000 })).toBe(25000) + }) + + it('devuelve 0 cuando ambos son 0', () => { + expect(calcularTotalIngresos({ ingresosMensuales: 0, otrosIngresos: 0 })).toBe(0) + }) +}) + +describe('calcularGastosTotal', () => { + it('suma gastos de luz y otros gastos', () => { + expect(calcularGastosTotal({ gastosLuz: 500, gastosOtros: 5000 })).toBe(5500) + }) + + it('devuelve 0 cuando ambos son 0', () => { + expect(calcularGastosTotal({ gastosLuz: 0, gastosOtros: 0 })).toBe(0) + }) +}) diff --git a/apps/alta-clientes/src/lib/calculations.ts b/apps/alta-clientes/src/lib/calculations.ts new file mode 100644 index 0000000..96be170 --- /dev/null +++ b/apps/alta-clientes/src/lib/calculations.ts @@ -0,0 +1,11 @@ +import type { DatosEconomicos } from '../types' + +/** Total de ingresos: ingresos mensuales + otros ingresos */ +export function calcularTotalIngresos(economicos: Pick): number { + return economicos.ingresosMensuales + economicos.otrosIngresos +} + +/** Total de gastos aproximados: luz + otros */ +export function calcularGastosTotal(economicos: Pick): number { + return economicos.gastosLuz + economicos.gastosOtros +} diff --git a/apps/alta-clientes/src/lib/curp.test.ts b/apps/alta-clientes/src/lib/curp.test.ts new file mode 100644 index 0000000..1e9ef79 --- /dev/null +++ b/apps/alta-clientes/src/lib/curp.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect } from 'vitest' +import { + validarCurp, + extraerDeCurp, + compararConDatos, + digitoVerificadorCurp, + ENTIDADES_CURP, +} from './curp' + +// Fixture real del formato PDF de alta (reference/alta-clientes/pdf_formatos.txt) +const CURP_FIXTURE = 'TECP891124HBCLHS07' + +/** Construye una CURP de prueba con dígito verificador calculado por el propio algoritmo */ +function construirCurp(base17: string): string { + return `${base17}${digitoVerificadorCurp(base17)}` +} + +describe('validarCurp', () => { + it('acepta el fixture real TECP891124HBCLHS07 (formato y dígito)', () => { + expect(validarCurp(CURP_FIXTURE)).toEqual({ valida: true }) + }) + + it('acepta CURPs construidas con el propio algoritmo', () => { + const bases = [ + 'TECP891124HBCLHS0', + 'GOML900515HDFRRN0', + 'LOQA050312MCLPMA0', + 'HEMA000101HDFXXX0', + 'XAXX010101MNEXXXA', // genérica sin homoclave real, solo para dígito + ] + for (const base of bases) { + expect(validarCurp(construirCurp(base)).valida).toBe(true) + } + }) + + it('rechaza un dígito verificador alterado', () => { + const buena = construirCurp('TECP891124HBCLHS0') + const malDigito = `${buena.slice(0, 17)}${(Number(buena[17]) + 1) % 10}` + expect(validarCurp(malDigito)).toEqual({ valida: false, motivo: 'digito' }) + }) + + it('rechaza formatos inválidos', () => { + expect(validarCurp('TECP891124')).toEqual({ valida: false, motivo: 'formato' }) + expect(validarCurp('TECP891124XBCLHS07')).toEqual({ valida: false, motivo: 'formato' }) + expect(validarCurp('')).toEqual({ valida: false, motivo: 'formato' }) + }) + + it('acepta minúsculas y espacios alrededor', () => { + expect(validarCurp(' tecp891124hbclhs07 ').valida).toBe(true) + }) +}) + +describe('extraerDeCurp', () => { + it('parsea fecha, sexo y entidad del fixture real', () => { + const datos = extraerDeCurp(CURP_FIXTURE) + expect(datos).not.toBeNull() + expect(datos?.fechaNacimiento).toBe('1989-11-24') + expect(datos?.sexo).toBe('H') + expect(datos?.entidad).toBe('BC') + expect(datos?.entidadNombre).toBe('BAJA CALIFORNIA') + }) + + it('aplica la regla de siglo (YY <= año actual => 2000s)', () => { + const datos = extraerDeCurp(construirCurp('GOML050515HDFRRN0')) + expect(datos?.fechaNacimiento).toBe('2005-05-15') + }) + + it('devuelve null con formato inválido', () => { + expect(extraerDeCurp('NO-ES-CURP')).toBeNull() + }) + + it('el mapa tiene las 33 entidades', () => { + expect(Object.keys(ENTIDADES_CURP)).toHaveLength(33) + expect(ENTIDADES_CURP.NE).toBe('NACIDO EN EL EXTRANJERO') + }) +}) + +describe('compararConDatos', () => { + it('sin advertencias cuando todo coincide', () => { + expect( + compararConDatos(CURP_FIXTURE, { + fechaNacimiento: '1989-11-24', + lugarNacimiento: 'Tijuana, Baja California', + }), + ).toEqual([]) + }) + + it('advierte cuando la fecha no coincide', () => { + const avisos = compararConDatos(CURP_FIXTURE, { fechaNacimiento: '1990-01-01' }) + expect(avisos).toHaveLength(1) + expect(avisos[0]).toContain('fecha de nacimiento') + }) + + it('advierte cuando el lugar no coincide con la entidad', () => { + const avisos = compararConDatos(CURP_FIXTURE, { lugarNacimiento: 'Guadalajara, Jalisco' }) + expect(avisos).toHaveLength(1) + expect(avisos[0]).toContain('lugar de nacimiento') + expect(avisos[0]).toContain('BAJA CALIFORNIA') + }) + + it('advierte dígito verificador incorrecto', () => { + const avisos = compararConDatos('TECP891124HBCLHS09', {}) + expect(avisos.some((a) => a.includes('dígito verificador'))).toBe(true) + }) + + it('no advierte nada con CURP vacía', () => { + expect(compararConDatos('', { fechaNacimiento: '1989-11-24' })).toEqual([]) + }) +}) diff --git a/apps/alta-clientes/src/lib/curp.ts b/apps/alta-clientes/src/lib/curp.ts new file mode 100644 index 0000000..725c56c --- /dev/null +++ b/apps/alta-clientes/src/lib/curp.ts @@ -0,0 +1,148 @@ +// Validación y extracción de datos de la CURP (Clave Única de Registro de Población) +// Algoritmo oficial del dígito verificador: alfabeto RENAPO, pesos 18..2, +// suma mod 10 y complemento a 10. + +export const REGEX_CURP_FORMATO = /^[A-Z]{4}\d{6}[HM][A-Z]{5}[0-9A-Z]\d$/ + +const ALFABETO = '0123456789ABCDEFGHIJKLMNÑOPQRSTUVWXYZ' + +export const ENTIDADES_CURP: Record = { + AS: 'AGUASCALIENTES', + BC: 'BAJA CALIFORNIA', + BS: 'BAJA CALIFORNIA SUR', + CC: 'CAMPECHE', + CL: 'COAHUILA', + CM: 'COLIMA', + CS: 'CHIAPAS', + CH: 'CHIHUAHUA', + DF: 'CIUDAD DE MEXICO', + DG: 'DURANGO', + GT: 'GUANAJUATO', + GR: 'GUERRERO', + HG: 'HIDALGO', + JC: 'JALISCO', + MC: 'MEXICO', + MN: 'MICHOACAN', + MS: 'MORELOS', + NT: 'NAYARIT', + NL: 'NUEVO LEON', + OC: 'OAXACA', + PL: 'PUEBLA', + QT: 'QUERETARO', + QR: 'QUINTANA ROO', + SP: 'SAN LUIS POTOSI', + SL: 'SINALOA', + SR: 'SONORA', + TC: 'TABASCO', + TS: 'TAMAULIPAS', + TL: 'TLAXCALA', + VZ: 'VERACRUZ', + YN: 'YUCATAN', + ZS: 'ZACATECAS', + NE: 'NACIDO EN EL EXTRANJERO', +} + +/** Quita acentos y pasa a mayúsculas para comparar textos */ +function normalizar(texto: string): string { + return texto + .normalize('NFD') + .replace(/[̀-ͯ]/g, '') + .toUpperCase() + .trim() +} + +/** Calcula el dígito verificador de los primeros 17 caracteres de la CURP */ +export function digitoVerificadorCurp(curp17: string): number { + let suma = 0 + for (let i = 0; i < 17; i++) { + const valor = ALFABETO.indexOf(curp17[i]) + if (valor === -1) return -1 + suma += valor * (18 - i) + } + return (10 - (suma % 10)) % 10 +} + +export interface ResultadoValidacionCurp { + valida: boolean + /** 'formato' si no cumple el patrón, 'digito' si el verificador no cuadra */ + motivo?: 'formato' | 'digito' +} + +/** Valida formato (regex) y dígito verificador de la CURP */ +export function validarCurp(curp: string): ResultadoValidacionCurp { + const limpia = normalizar(curp) + if (!REGEX_CURP_FORMATO.test(limpia)) return { valida: false, motivo: 'formato' } + const esperado = digitoVerificadorCurp(limpia.slice(0, 17)) + if (esperado !== Number(limpia[17])) return { valida: false, motivo: 'digito' } + return { valida: true } +} + +export interface DatosCurp { + /** Fecha de nacimiento en formato YYYY-MM-DD */ + fechaNacimiento: string + sexo: 'H' | 'M' + /** Código de entidad federativa (posiciones 12-13) */ + entidad: string + /** Nombre de la entidad federativa */ + entidadNombre: string +} + +/** + * Extrae fecha de nacimiento, sexo y entidad de una CURP con formato válido. + * Regla de siglo: YY <= año actual % 100 => 20YY, si no 19YY. + * Devuelve null si el formato no es válido. + */ +export function extraerDeCurp(curp: string): DatosCurp | null { + const limpia = normalizar(curp) + if (!REGEX_CURP_FORMATO.test(limpia)) return null + const yy = Number(limpia.slice(4, 6)) + const mm = limpia.slice(6, 8) + const dd = limpia.slice(8, 10) + const anioActual = new Date().getFullYear() % 100 + const anio = yy <= anioActual ? 2000 + yy : 1900 + yy + const entidad = limpia.slice(11, 13) + return { + fechaNacimiento: `${anio}-${mm}-${dd}`, + sexo: limpia[10] as 'H' | 'M', + entidad, + entidadNombre: ENTIDADES_CURP[entidad] ?? entidad, + } +} + +/** + * Compara la CURP con los datos capturados y devuelve advertencias + * (no bloquean; solo alertan inconsistencias). + */ +export function compararConDatos( + curp: string, + datos: { fechaNacimiento?: string; lugarNacimiento?: string }, +): string[] { + const advertencias: string[] = [] + const limpia = normalizar(curp) + if (limpia === '') return advertencias + + const validacion = validarCurp(limpia) + if (!validacion.valida && validacion.motivo === 'digito') { + advertencias.push('El dígito verificador de la CURP no coincide; verifica la captura') + } + if (!validacion.valida) return advertencias + + const extraidos = extraerDeCurp(limpia) + if (!extraidos) return advertencias + + const fechaCapturada = (datos.fechaNacimiento ?? '').trim() + if (fechaCapturada !== '' && fechaCapturada !== extraidos.fechaNacimiento) { + advertencias.push( + `La fecha de nacimiento no coincide con la CURP (${extraidos.fechaNacimiento} según CURP)`, + ) + } + + const lugar = normalizar(datos.lugarNacimiento ?? '') + if (lugar !== '' && !lugar.includes(extraidos.entidadNombre)) { + advertencias.push( + `El lugar de nacimiento no coincide con la entidad de la CURP (${extraidos.entidadNombre})`, + ) + } + + return advertencias +} diff --git a/apps/alta-clientes/src/lib/expediente.test.ts b/apps/alta-clientes/src/lib/expediente.test.ts new file mode 100644 index 0000000..2bc55aa --- /dev/null +++ b/apps/alta-clientes/src/lib/expediente.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest' +import { estadoExpediente } from './expediente' +import type { DocumentoCargado, SlotDocumento } from '../types' + +const doc = (slot: SlotDocumento): DocumentoCargado => ({ + slot, + nombre: `${slot}.pdf`, + tamano: 1024, + tipoMime: 'application/pdf', + archivo: new File(['x'], `${slot}.pdf`, { type: 'application/pdf' }), +}) + +const FIRMA = 'data:image/png;base64,iVBORw0KGgo=' + +describe('estadoExpediente', () => { + it('expediente vacío: 0/6 y todo pendiente', () => { + const e = estadoExpediente([], '') + expect(e.total).toBe(6) + expect(e.completos).toBe(0) + expect(e.completoTodo).toBe(false) + expect(e.items.every((i) => !i.completo)).toBe(true) + }) + + it('cuenta documentos cargados y firma por separado', () => { + const e = estadoExpediente([doc('INE'), doc('CURP')], FIRMA) + expect(e.completos).toBe(3) + expect(e.items.find((i) => i.clave === 'INE')?.completo).toBe(true) + expect(e.items.find((i) => i.clave === 'CONSTANCIA_RFC')?.completo).toBe(false) + expect(e.items.find((i) => i.clave === 'FIRMA')?.completo).toBe(true) + }) + + it('expediente completo: 6/6', () => { + const docs = [ + doc('INE'), + doc('CONSTANCIA_RFC'), + doc('CURP'), + doc('COMPROBANTE_DOMICILIO'), + doc('ACTA_NACIMIENTO'), + ] + const e = estadoExpediente(docs, FIRMA) + expect(e.completos).toBe(6) + expect(e.completoTodo).toBe(true) + }) + + it('una cadena que no es data:image no cuenta como firma', () => { + expect(estadoExpediente([], 'garabato').items.find((i) => i.clave === 'FIRMA')?.completo).toBe(false) + }) +}) diff --git a/apps/alta-clientes/src/lib/expediente.ts b/apps/alta-clientes/src/lib/expediente.ts new file mode 100644 index 0000000..3d2fab9 --- /dev/null +++ b/apps/alta-clientes/src/lib/expediente.ts @@ -0,0 +1,34 @@ +import type { DocumentoCargado } from '../types' +import { SLOTS_DOCUMENTOS } from '../data/constantes' + +export interface ItemExpediente { + clave: string + etiqueta: string + completo: boolean +} + +export interface EstadoExpediente { + items: ItemExpediente[] + completos: number + total: number + completoTodo: boolean +} + +/** + * Estado del expediente en vivo: los 5 documentos del formato + la firma. + * Función pura para poder probarla y reutilizarla en los pasos 7 y 8. + */ +export function estadoExpediente(documentos: DocumentoCargado[], firmaDataUrl: string): EstadoExpediente { + const items: ItemExpediente[] = SLOTS_DOCUMENTOS.map(({ slot, etiqueta }) => ({ + clave: slot, + etiqueta, + completo: documentos.some((d) => d.slot === slot), + })) + items.push({ + clave: 'FIRMA', + etiqueta: 'Firma del cliente', + completo: firmaDataUrl.startsWith('data:image/'), + }) + const completos = items.filter((i) => i.completo).length + return { items, completos, total: items.length, completoTodo: completos === items.length } +} diff --git a/apps/alta-clientes/src/lib/fixture.ts b/apps/alta-clientes/src/lib/fixture.ts new file mode 100644 index 0000000..1f1cca4 --- /dev/null +++ b/apps/alta-clientes/src/lib/fixture.ts @@ -0,0 +1,156 @@ +import type { AltaCliente } from '../types' + +// PNG de 1x1 px para ejercitar la incrustación de firma en los tests +export const FIRMA_PRUEBA = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' + +/** + * Fixture con los datos del cliente del PDF de referencia + * (PASCUAL MARTIN TELLEZ CHIGUINDO — pdf_formatos.txt) + */ +export const fixtureAlta: AltaCliente = { + datosGenerales: { + campaniaVenta: '', + fecha: '2026-07-19', + nombreCompleto: 'PASCUAL MARTIN TELLEZ CHIGUINDO', + nacionalidad: 'MEXICANA', + nivelEstudios: 'MAESTRIA', + curp: 'TECP891124HBCLHS07', + rfc: 'TECP8911246B4', + nss: '', + delegacionMunicipio: 'TIJUANA', + direccion: 'RIO EUFRATES EDIF 1 7145 203', + codigoPostal: '22223', + antiguedadDomicilio: '2', + fechaNacimiento: '1989-11-24', + lugarNacimiento: 'TIJUANA', + estadoCivil: 'SOLTERO', + regimenBienes: '', + peso: '87', + estatura: '1.78', + email: 'dantemartinchi@gmail.com', + telCasa: '6646935924', + celular: '6646935924', + viviendaAnios: '2', + tipoVivienda: 'PROPIA', + tipoCompra: 'VIVIR_AHI', + actividadOcupacion: 'AGENTE DEL MINISTERIO PUBLICO FEDERAL', + }, + empleo: { + tipoEmpleo: 'ASALARIADO', + tipoEconomia: 'E_MEXICANA', + compania: 'FISCALIA GENERAL DE LA REPUBLICA', + antiguedadAnios: '12', + antiguedadMeses: '10', + direccion: 'ZONA RIO TIJUANA', + colonia: 'ZONA RIO', + numero: '', + codigoPostal: '22230', + puesto: 'AGENTE DE MINISTERIO PUBLICO FEDERAL', + telefono: '6646336847', + ext: '', + delegacion: 'TIJUANA', + }, + economicos: { + ingresosMensuales: 54988, + otrosIngresos: 0, + gastosLuz: 500, + gastosOtros: 5000, + dependientes: { personas: '', parentesco: '', edad: '' }, + }, + coacreditado: { + tieneCoacreditado: false, + generales: { + campaniaVenta: '', + fecha: '2026-07-19', + nombreCompleto: '', + nacionalidad: '', + nivelEstudios: '', + curp: '', + rfc: '', + nss: '', + delegacionMunicipio: '', + direccion: '', + codigoPostal: '', + antiguedadDomicilio: '', + fechaNacimiento: '', + lugarNacimiento: '', + estadoCivil: '', + regimenBienes: '', + peso: '', + estatura: '', + email: '', + telCasa: '', + celular: '', + viviendaAnios: '', + tipoVivienda: '', + tipoCompra: '', + actividadOcupacion: '', + }, + empleo: { + tipoEmpleo: '', + tipoEconomia: '', + compania: '', + antiguedadAnios: '', + antiguedadMeses: '', + direccion: '', + colonia: '', + numero: '', + codigoPostal: '', + puesto: '', + telefono: '', + ext: '', + delegacion: '', + }, + economicos: { + ingresosMensuales: 0, + otrosIngresos: 0, + gastosLuz: 0, + gastosOtros: 0, + dependientes: { personas: '', parentesco: '', edad: '' }, + }, + }, + referencias: [ + { nombre: 'JOSE ARNOLDO MENDOZA', telefono: '6641887799', parentesco: 'AMIGO', direccion: 'ALTAMIRA' }, + { nombre: 'JAIME JOHAN LARA', telefono: '6644704362', parentesco: 'AMIGO', direccion: 'CAÑON OASIS' }, + { nombre: 'FANNY VANESSA CARDENAS', telefono: '6634381839', parentesco: 'AMIGO', direccion: 'VISTA LAGO' }, + { nombre: 'CYNTHIA ANAHI GARCIA', telefono: '6634303303', parentesco: 'AMIGO', direccion: 'ZONA RIO' }, + ], + viviendaCredito: { + proyecto: 'SENDA', + proyectoOtro: '', + modelo: 'LAUREL', + manzana: '11', + lote: '11', + ubicacion: '409-1-11-11', + superficieM2: '', + valorVenta: '2700000', + enganche: '0', + equipamientoAdicional: '', + fechaPagoEnganche: '', + firmaContrato: '', + tiposCredito: ['BANCARIO'], + montoCreditoInfoFovi: '', + ahorroVoluntarioSubcuenta: '', + tipoProducto: '', + descuentoMensualInfoFovissste: '', + idAutoridadEmisora: 'INSTITUTO NACIONAL ELECTORAL', + idNumero: 'IDMEX2311049617', + idVigencia: '2032-12-31', + domicilioFiscal: { + calle: 'FRAY PEDRO DE GANTE', + numExt: '97', + numInt: '0', + colonia: 'TIJUANA OTAY', + municipio: 'TIJUANA', + estado: 'BAJA CALIFORNIA', + cp: '22508', + pais: 'MEXICO', + }, + }, + documentos: [], + firma: { + firmaDataUrl: FIRMA_PRUEBA, + asesorNombre: 'ESMERALDA BECERRA DOMINGUEZ', + }, +} diff --git a/apps/alta-clientes/src/lib/formatters.ts b/apps/alta-clientes/src/lib/formatters.ts new file mode 100644 index 0000000..82eafa4 --- /dev/null +++ b/apps/alta-clientes/src/lib/formatters.ts @@ -0,0 +1,48 @@ +const formatoMXN = new Intl.NumberFormat('es-MX', { + style: 'currency', + currency: 'MXN', +}) + +/** Formato monetario MXN, p. ej. $2,700,000.00 */ +export function formatoMoneda(valor: number): string { + return formatoMXN.format(valor) +} + +/** Fecha en formato largo es-MX: "20 de agosto de 2026" */ +export function formatoFechaLarga(fecha: Date): string { + return fecha.toLocaleDateString('es-MX', { day: 'numeric', month: 'long', year: 'numeric' }) +} + +/** ISO 'YYYY-MM-DD' a fecha legible corta: "24/11/1989" */ +export function formatoFechaCorta(iso: string): string { + if (!iso) return '' + return new Date(`${iso}T00:00:00`).toLocaleDateString('es-MX', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }) +} + +/** Fecha ISO del día (para nombre de archivo): 'YYYY-MM-DD' */ +export function fechaISOHoy(fecha: Date = new Date()): string { + const y = fecha.getFullYear() + const m = String(fecha.getMonth() + 1).padStart(2, '0') + const d = String(fecha.getDate()).padStart(2, '0') + return `${y}-${m}-${d}` +} + +/** Normaliza un nombre para usarlo en nombre de archivo: sin acentos ni espacios */ +export function normalizarNombreArchivo(nombre: string): string { + return nombre + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .replace(/\s+/g, '_') + .replace(/[^A-Za-z0-9_]/g, '') + .toUpperCase() +} + +/** Construye el nombre de archivo PDF: {tipo}_{nombreCliente}_{fechaISO}.pdf */ +export function nombreArchivoPdf(tipo: string, nombreCliente: string, fecha: Date = new Date()): string { + return `${tipo}_${normalizarNombreArchivo(nombreCliente)}_${fechaISOHoy(fecha)}.pdf` +} diff --git a/apps/alta-clientes/src/lib/ocrIne.test.ts b/apps/alta-clientes/src/lib/ocrIne.test.ts new file mode 100644 index 0000000..4c7f15f --- /dev/null +++ b/apps/alta-clientes/src/lib/ocrIne.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest' +import { parsearTextoIne } from './ocrIne' + +// Texto OCR de ejemplo de una credencial INE (frente) +const TEXTO_OCR = ` +INSTITUTO NACIONAL ELECTORAL +CREDENCIAL PARA VOTAR +NOMBRE +TELLEZ CHIGUINDO +PASCUAL MARTIN +DOMICILIO +AV REVOLUCION 123 +TIJUANA BC 22000 +CLAVE DE ELECTOR TLCHPS89112402H400 +CURP TECP891124HBCLHS07 +FECHA DE NACIMIENTO 24/11/1989 +SEXO H +VIGENCIA 2020-2030 +` + +describe('parsearTextoIne', () => { + it('extrae CURP y deriva fecha y sexo', () => { + const s = parsearTextoIne(TEXTO_OCR) + expect(s.curp).toBe('TECP891124HBCLHS07') + expect(s.fechaNacimiento).toBe('1989-11-24') + expect(s.sexo).toBe('H') + }) + + it('extrae el nombre de las líneas tras NOMBRE', () => { + const s = parsearTextoIne(TEXTO_OCR) + expect(s.nombre).toBe('TELLEZ CHIGUINDO PASCUAL MARTIN') + }) + + it('sin CURP no deriva fecha ni sexo', () => { + const s = parsearTextoIne('NOMBRE\nPEREZ LOPEZ\nJUAN\nDOMICILIO') + expect(s.curp).toBeUndefined() + expect(s.fechaNacimiento).toBeUndefined() + expect(s.sexo).toBeUndefined() + expect(s.nombre).toBe('PEREZ LOPEZ JUAN') + }) + + it('texto sin nada útil devuelve sugerencias vacías', () => { + expect(parsearTextoIne('!!! ### 123')).toEqual({}) + }) + + it('tolerante a minúsculas del OCR', () => { + const s = parsearTextoIne('nombre\ntellez chiguindo\npascual martin\ncurp tecp891124hbclhs07') + expect(s.curp).toBe('TECP891124HBCLHS07') + expect(s.nombre).toBe('TELLEZ CHIGUINDO PASCUAL MARTIN') + }) +}) diff --git a/apps/alta-clientes/src/lib/ocrIne.ts b/apps/alta-clientes/src/lib/ocrIne.ts new file mode 100644 index 0000000..3c78e43 --- /dev/null +++ b/apps/alta-clientes/src/lib/ocrIne.ts @@ -0,0 +1,81 @@ +// Parser del texto OCR de una credencial INE: extrae sugerencias +// (CURP, nombre, y fecha/sexo derivados de la CURP) sin tocar el formulario. + +import { extraerDeCurp } from './curp' + +export interface SugerenciasIne { + curp?: string + nombre?: string + /** YYYY-MM-DD, derivada de la CURP detectada */ + fechaNacimiento?: string + sexo?: 'H' | 'M' +} + +const REGEX_CURP_TEXTO = /[A-ZÑ]{4}\d{6}[HM][A-Z]{5}[0-9A-Z]\d/ + +// Etiquetas típicas de la credencial que delimitan el bloque del nombre +const ETIQUETAS_CORTE = [ + 'DOMICILIO', + 'FECHA DE NACIMIENTO', + 'CLAVE DE ELECTOR', + 'CURP', + 'ESTADO', + 'MUNICIPIO', + 'LOCALIDAD', + 'SEXO', + 'VIGENCIA', + 'EMISION', + 'ANO DE REGISTRO', +] + +function esEtiquetaCorte(linea: string): boolean { + return ETIQUETAS_CORTE.some((e) => linea.startsWith(e)) +} + +/** + * Heurística de nombre: líneas en mayúsculas que siguen a la línea "NOMBRE", + * hasta topar con otra etiqueta de la credencial (máx 2 líneas). + */ +function extraerNombre(lineas: string[]): string | undefined { + const idx = lineas.findIndex((l) => l === 'NOMBRE' || l.startsWith('NOMBRE ')) + if (idx === -1) return undefined + const partes: string[] = [] + // Si la línea "NOMBRE" ya trae texto detrás, tómalo como primera parte + const enMismaLinea = lineas[idx].replace(/^NOMBRE\s*/, '').trim() + if (enMismaLinea !== '') partes.push(enMismaLinea) + for (let i = idx + 1; i < lineas.length && partes.length < 2; i++) { + const linea = lineas[i] + if (esEtiquetaCorte(linea)) break + if (!/^[A-ZÑÁÉÍÓÚ' -]{3,}$/.test(linea)) break + partes.push(linea) + } + const nombre = partes.join(' ').replace(/\s+/g, ' ').trim() + return nombre === '' ? undefined : nombre +} + +/** Convierte el texto OCR crudo en sugerencias para el formulario */ +export function parsearTextoIne(texto: string): SugerenciasIne { + const lineas = texto + .toUpperCase() + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l !== '') + + const sugerencias: SugerenciasIne = {} + + const textoPlano = lineas.join(' ') + const curp = textoPlano.match(REGEX_CURP_TEXTO)?.[0] + if (curp) { + sugerencias.curp = curp + const derivados = extraerDeCurp(curp) + if (derivados) { + sugerencias.fechaNacimiento = derivados.fechaNacimiento + sugerencias.sexo = derivados.sexo + } + } + + const nombre = extraerNombre(lineas) + if (nombre) sugerencias.nombre = nombre + + return sugerencias +} diff --git a/apps/alta-clientes/src/lib/pdf/avisoPrivacidad.ts b/apps/alta-clientes/src/lib/pdf/avisoPrivacidad.ts new file mode 100644 index 0000000..810509e --- /dev/null +++ b/apps/alta-clientes/src/lib/pdf/avisoPrivacidad.ts @@ -0,0 +1,39 @@ +import { jsPDF } from 'jspdf' +import type { AltaCliente } from '../../types' +import { formatoFechaLarga, nombreArchivoPdf } from '../formatters' +import { + AVISO_PRIVACIDAD_TITULO, + AVISO_PRIVACIDAD_PARRAFOS, + avisoPrivacidadParrafoFinal, +} from '../../data/textosLegales' +import { MARGEN, parrafo, titulo, lineaFirma, type DocumentoPdf } from './helpers' + +/** Construye el PDF "AVISO DE PRIVACIDAD (SINÓPTICO)" (sin descargarlo) */ +export function crearDocumento(alta: AltaCliente): DocumentoPdf { + const { datosGenerales: g, firma } = alta + const doc = new jsPDF() + let y = titulo(doc, MARGEN + 4, AVISO_PRIVACIDAD_TITULO, 13) + y += 2 + + for (const p of AVISO_PRIVACIDAD_PARRAFOS) { + y = parrafo(doc, y, p) + y += 1 + } + y = parrafo(doc, y, avisoPrivacidadParrafoFinal(g.nombreCompleto)) + y += 4 + + const fecha = g.fecha ? new Date(`${g.fecha}T00:00:00`) : new Date() + y = parrafo(doc, y, `Tijuana, Baja California, a ${formatoFechaLarga(fecha)}.`) + y += 2 + y = parrafo(doc, y, 'ENTERADO-CONFORME\nAUTORIZO EL USO DE MIS DATOS', { negritas: true }) + y += 12 + lineaFirma(doc, y, MARGEN + 40, 100, g.nombreCompleto, firma.firmaDataUrl) + + return { doc, nombreArchivo: nombreArchivoPdf('aviso_privacidad_firmado', g.nombreCompleto) } +} + +/** Genera y descarga el PDF del aviso de privacidad */ +export function descargar(alta: AltaCliente): void { + const { doc, nombreArchivo } = crearDocumento(alta) + doc.save(nombreArchivo) +} diff --git a/apps/alta-clientes/src/lib/pdf/cartaAntilavado.ts b/apps/alta-clientes/src/lib/pdf/cartaAntilavado.ts new file mode 100644 index 0000000..d4b89df --- /dev/null +++ b/apps/alta-clientes/src/lib/pdf/cartaAntilavado.ts @@ -0,0 +1,54 @@ +import { jsPDF } from 'jspdf' +import type { AltaCliente } from '../../types' +import { nombreArchivoPdf } from '../formatters' +import { + cartaAntilavadoCuerpo, + CARTA_ANTILAVADO_CIERRE, + CARTA_ANTILAVADO_DESPEDIDA, +} from '../../data/textosLegales' +import { MARGEN, parrafo, lineaFirma, type DocumentoPdf } from './helpers' + +/** Construye la carta de no pagos en efectivo (anti-lavado) sin descargarla */ +export function crearDocumento(alta: AltaCliente): DocumentoPdf { + const { datosGenerales: g, viviendaCredito: v, firma } = alta + const doc = new jsPDF() + const fecha = g.fecha ? new Date(`${g.fecha}T00:00:00`) : new Date() + const proyecto = v.proyecto === 'OTRO' ? v.proyectoOtro ?? '' : v.proyecto + + let y = MARGEN + 4 + doc.setFontSize(9) + doc.setFont('helvetica', 'normal') + const dd = String(fecha.getDate()).padStart(2, '0') + const mm = String(fecha.getMonth() + 1).padStart(2, '0') + doc.text(`Tijuana, B.C a ${dd} de ${mm} de ${fecha.getFullYear()}`, MARGEN, y) + y += 10 + + y = parrafo(doc, y, `Estimado (a) ${g.nombreCompleto}`, { negritas: true, tamano: 9 }) + y = parrafo(doc, y, 'P R E S E N T E', { negritas: true, tamano: 9 }) + y += 3 + + y = parrafo(doc, y, cartaAntilavadoCuerpo(proyecto), { tamano: 9 }) + y += 2 + y = parrafo(doc, y, CARTA_ANTILAVADO_CIERRE, { tamano: 9 }) + y += 2 + y = parrafo(doc, y, CARTA_ANTILAVADO_DESPEDIDA, { tamano: 9 }) + y += 6 + + y = parrafo(doc, y, 'A T E N T A M E N T E', { negritas: true, tamano: 9 }) + y += 12 + y = lineaFirma(doc, y, MARGEN + 40, 100, 'Frasa Desarrollos SAPI C.V.') + y = parrafo(doc, y, 'Por medio de su representante legal', { tamano: 9 }) + y += 8 + y = parrafo(doc, y, 'DE CONFORMIDAD', { negritas: true, tamano: 9 }) + y += 12 + y = lineaFirma(doc, y, MARGEN + 40, 100, g.nombreCompleto, firma.firmaDataUrl) + parrafo(doc, y, 'En mi carácter de “COMPRADOR”', { tamano: 9 }) + + return { doc, nombreArchivo: nombreArchivoPdf('carta_antilavado_firmada', g.nombreCompleto) } +} + +/** Genera y descarga la carta anti-lavado */ +export function descargar(alta: AltaCliente): void { + const { doc, nombreArchivo } = crearDocumento(alta) + doc.save(nombreArchivo) +} diff --git a/apps/alta-clientes/src/lib/pdf/checklistExpediente.ts b/apps/alta-clientes/src/lib/pdf/checklistExpediente.ts new file mode 100644 index 0000000..1b88abf --- /dev/null +++ b/apps/alta-clientes/src/lib/pdf/checklistExpediente.ts @@ -0,0 +1,116 @@ +import { jsPDF } from 'jspdf' +import autoTable from 'jspdf-autotable' +import type { AltaCliente } from '../../types' +import { formatoMoneda, nombreArchivoPdf } from '../formatters' +import { + CHECKLIST_BASICO, + CHECKLIST_ANTES_ESCRITURA, + CHECKLIST_NOTAS, + REQUISITOS_GENERALES, + REQUISITOS_CONTACTO_ENTREGA, + REQUISITOS_CONTACTO_CONTRATO, +} from '../../data/textosLegales' +import { MARGEN, finalY, parrafo, titulo, lineaFirma, numero, type DocumentoPdf } from './helpers' + +const vacio = (v: string | undefined | null): string => (v && v.trim() !== '' ? v : '—') + +/** + * Construye el PDF "FORMATO DE RECEPCIÓN Y CONTROL DE EXPEDIENTE" + + * página de "REQUISITOS GENERALES" (sin descargarlo) + */ +export function crearDocumento(alta: AltaCliente): DocumentoPdf { + const { datosGenerales: g, viviendaCredito: v, referencias, firma } = alta + const doc = new jsPDF() + const proyecto = v.proyecto === 'OTRO' ? v.proyectoOtro ?? '' : v.proyecto + + let y = titulo(doc, MARGEN + 4, 'FORMATO DE RECEPCIÓN Y\nCONTROL DE EXPEDIENTE', 13) + y += 2 + + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 8, cellPadding: 1.5 }, + headStyles: { fillColor: [4, 120, 87] }, + body: [ + ['CLIENTE', g.nombreCompleto], + ['PROYECTO', vacio(proyecto)], + ['UBICACIÓN / PROTOTIPO', `${vacio(v.ubicacion)} ${vacio(v.modelo)}`.replace('— —', '—')], + ['VALOR VIVIENDA', formatoMoneda(numero(v.valorVenta))], + ['ENGANCHE', formatoMoneda(numero(v.enganche))], + ['FECHA PAGO DE ENGANCHE', vacio(v.fechaPagoEnganche)], + ['FIRMA DE CONTRATO', vacio(v.firmaContrato)], + ], + }) + y = finalY(doc) + 6 + + // Checklists en dos columnas + doc.setFontSize(9) + doc.setFont('helvetica', 'bold') + doc.text('CHECK LIST EXPEDIENTE BÁSICO:', MARGEN, y) + doc.text('ANTES DE ESCRITURA:', 115, y) + y += 5 + doc.setFont('helvetica', 'normal') + doc.setFontSize(8) + const filas = Math.max(CHECKLIST_BASICO.length, CHECKLIST_ANTES_ESCRITURA.length) + for (let i = 0; i < filas; i++) { + if (CHECKLIST_BASICO[i]) doc.text(`[ ] ${CHECKLIST_BASICO[i]}`, MARGEN, y) + if (CHECKLIST_ANTES_ESCRITURA[i]) doc.text(`[ ] ${CHECKLIST_ANTES_ESCRITURA[i]}`, 115, y) + y += 5 + } + y += 4 + + for (const nota of CHECKLIST_NOTAS) { + y = parrafo(doc, y, nota) + y += 1 + } + y += 14 + + // Firmas de recepción del expediente + const yFirmas = y + lineaFirma(doc, yFirmas, MARGEN, 55, 'GLADIS VALENZUELA\nFECHA DE RECEPCIÓN') + lineaFirma(doc, yFirmas, 78, 55, 'DEYSI LORENA ALEMAN FIGUEROA\nCORPORATIVO') + lineaFirma(doc, yFirmas, 142, 55, 'DANIEL ALVARADO LUQUE\nRECIBIÓ') + + // Página 2: requisitos generales + doc.addPage() + y = titulo(doc, MARGEN + 4, 'REQUISITOS\nGENERALES', 13) + y = parrafo(doc, y, `NOMBRE: ${g.nombreCompleto}`, { negritas: true, tamano: 9 }) + y = parrafo(doc, y, `FECHA: ${vacio(g.fecha)}`, { negritas: true, tamano: 9 }) + y += 2 + + for (const grupo of REQUISITOS_GENERALES) { + y = parrafo(doc, y, grupo.titulo, { negritas: true, tamano: 9 }) + for (const item of grupo.items) { + y = parrafo(doc, y, `• ${item}`) + } + y += 2 + } + + // Referencias en U.S.A. (tabla vacía para llenar a mano) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['NOMBRE', 'TELÉFONO', 'PARENTESCO', 'DIRECCIÓN']], + body: referencias.map((r) => [vacio(r.nombre), vacio(r.telefono), vacio(r.parentesco), vacio(r.direccion)]), + }) + y = finalY(doc) + 6 + + y = parrafo(doc, y, 'ENTREGA DE DOCUMENTOS:', { negritas: true, tamano: 9 }) + y = parrafo(doc, y, REQUISITOS_CONTACTO_ENTREGA) + y += 2 + y = parrafo(doc, y, 'FIRMA DE CONTRATO:', { negritas: true, tamano: 9 }) + y = parrafo(doc, y, REQUISITOS_CONTACTO_CONTRATO) + y += 12 + lineaFirma(doc, y, MARGEN + 10, 70, `NOMBRE Y FIRMA DEL CLIENTE\n${g.nombreCompleto}`, firma.firmaDataUrl) + lineaFirma(doc, y, 120, 70, `NOMBRE Y FIRMA DEL ASESOR\n${firma.asesorNombre}`) + + return { doc, nombreArchivo: nombreArchivoPdf('checklist_expediente', g.nombreCompleto) } +} + +/** Genera y descarga el PDF del checklist de expediente */ +export function descargar(alta: AltaCliente): void { + const { doc, nombreArchivo } = crearDocumento(alta) + doc.save(nombreArchivo) +} diff --git a/apps/alta-clientes/src/lib/pdf/helpers.ts b/apps/alta-clientes/src/lib/pdf/helpers.ts new file mode 100644 index 0000000..3ade335 --- /dev/null +++ b/apps/alta-clientes/src/lib/pdf/helpers.ts @@ -0,0 +1,79 @@ +import { jsPDF } from 'jspdf' + +export const MARGEN = 14 +const ANCHO_UTIL = 210 - MARGEN * 2 +const LIMITE_PAGINA = 285 + +export interface DocumentoPdf { + doc: jsPDF + nombreArchivo: string +} + +// jspdf-autotable expone finalY en doc.lastAutoTable en runtime; sus tipos no lo declaran +type DocConTabla = jsPDF & { lastAutoTable: { finalY: number } } +export const finalY = (doc: jsPDF): number => (doc as DocConTabla).lastAutoTable.finalY + +export interface OpcionesParrafo { + negritas?: boolean + tamano?: number + alinear?: 'left' | 'center' | 'right' +} + +/** Escribe un párrafo con salto de página automático. Devuelve la nueva Y. */ +export function parrafo(doc: jsPDF, y: number, texto: string, opciones: OpcionesParrafo = {}): number { + const tamano = opciones.tamano ?? 8 + doc.setFontSize(tamano) + doc.setFont('helvetica', opciones.negritas ? 'bold' : 'normal') + const lineas = doc.splitTextToSize(texto, ANCHO_UTIL) as string[] + if (y + lineas.length * tamano * 0.4 > LIMITE_PAGINA) { + doc.addPage() + y = MARGEN + } + const alinear = opciones.alinear ?? 'left' + const x = alinear === 'center' ? 105 : alinear === 'right' ? 210 - MARGEN : MARGEN + doc.text(lineas, x, y, { align: alinear }) + return y + lineas.length * tamano * 0.4 + 3 +} + +/** Título centrado. Devuelve la nueva Y. */ +export function titulo(doc: jsPDF, y: number, texto: string, tamano = 12): number { + doc.setFontSize(tamano) + doc.setFont('helvetica', 'bold') + const lineas = doc.splitTextToSize(texto, ANCHO_UTIL) as string[] + doc.text(lineas, 105, y, { align: 'center' }) + return y + lineas.length * tamano * 0.45 + 3 +} + +/** + * Línea de firma con leyenda; si hay imagen de firma se incrusta sobre la línea. + * Devuelve la nueva Y. + */ +export function lineaFirma( + doc: jsPDF, + y: number, + x: number, + ancho: number, + leyenda: string, + firmaDataUrl?: string, +): number { + if (y + 25 > LIMITE_PAGINA) { + doc.addPage() + y = MARGEN + 10 + } + if (firmaDataUrl) { + doc.addImage(firmaDataUrl, 'PNG', x + ancho / 2 - 15, y - 14, 30, 14) + } + doc.setLineWidth(0.3) + doc.line(x, y, x + ancho, y) + doc.setFontSize(8) + doc.setFont('helvetica', 'bold') + const lineas = doc.splitTextToSize(leyenda, ancho) as string[] + doc.text(lineas, x + ancho / 2, y + 4, { align: 'center' }) + return y + 4 + lineas.length * 8 * 0.4 + 4 +} + +/** Parsea un texto numérico del formulario; vacío o inválido devuelve 0 */ +export function numero(valor: string): number { + const n = Number.parseFloat(valor) + return Number.isFinite(n) ? n : 0 +} diff --git a/apps/alta-clientes/src/lib/pdf/identificacionCliente.ts b/apps/alta-clientes/src/lib/pdf/identificacionCliente.ts new file mode 100644 index 0000000..312a144 --- /dev/null +++ b/apps/alta-clientes/src/lib/pdf/identificacionCliente.ts @@ -0,0 +1,233 @@ +import { jsPDF } from 'jspdf' +import autoTable from 'jspdf-autotable' +import type { AltaCliente } from '../../types' +import { formatoFechaCorta, nombreArchivoPdf } from '../formatters' +import { + PLD_TEXTO_PROTECCION_DATOS, + PLD_TEXTO_DOCUMENTOS_INTEGRAN, + PLD_TEXTO_BENEFICIARIO, + PLD_TEXTO_RESPONSABILIDAD, + PLD_TEXTO_OTRO_FORMATO, + beneficiarioControladorIntro, + BENEFICIARIO_CONTROLADOR_ARTICULO3, + BENEFICIARIO_CONTROLADOR_MANIFIESTO, + BENEFICIARIO_CONTROLADOR_CIERRE, +} from '../../data/textosLegales' +import { MARGEN, finalY, parrafo, titulo, lineaFirma, type DocumentoPdf } from './helpers' + +const vacio = (v: string | undefined | null): string => (v && v.trim() !== '' ? v : '') + +/** + * Construye el PDF "FORMATO DE IDENTIFICACION DE CLIENTES" (PLD) + + * "FORMATO PARA LA IDENTIFICACIÓN DEL BENEFICIARIO CONTROLADOR" (sin descargarlo) + */ +export function crearDocumento(alta: AltaCliente): DocumentoPdf { + const { datosGenerales: g, viviendaCredito: v, firma } = alta + const df = v.domicilioFiscal + const doc = new jsPDF() + let y = titulo( + doc, + MARGEN + 4, + 'FORMATO DE IDENTIFICACION DE CLIENTES\nPERSONA FÍSICA NACIONALIDAD MEXICANA O EXTRANJERA\nCON RESIDENCIA PERMANENTE O TEMPORAL', + ) + doc.setFontSize(8) + doc.setFont('helvetica', 'bold') + doc.text(`FECHA: ${formatoFechaCorta(g.fecha)}`, 210 - MARGEN, y, { align: 'right' }) + y += 5 + + // Campos 1 a 11 del formato PLD + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['1 - Nombre y apellidos del cliente']], + body: [[g.nombreCompleto]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['2 - Fecha de Nacimiento', '3 - Lugar de Nacimiento']], + body: [[formatoFechaCorta(g.fechaNacimiento), vacio(g.lugarNacimiento)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['4 - Nacionalizado o Residente', '4 - País']], + body: [[vacio(g.nacionalidad), 'MEXICO']], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['5 - Actividad, Ocupación o Profesión']], + body: [[vacio(g.actividadOcupacion)]], + }) + y = finalY(doc) + // 6 - Domicilio particular + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['6 - Domicilio Particular', '']], + body: [ + ['Calle o Avenida / Dirección', vacio(g.direccion)], + ['Colonia o Zona Urbana / Municipio', [vacio(g.delegacionMunicipio)].join(' ')], + ['Entidad Federativa / Código Postal / País', `BAJA CALIFORNIA ${vacio(g.codigoPostal)} MEXICO`.trim()], + ], + }) + y = finalY(doc) + // 6 - Domicilio fiscal (opcional) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['6 - Domicilio Fiscal (Art 29 y 29A CFF)', '']], + body: [ + ['Calle o Avenida', vacio(df.calle)], + ['Número Exterior / Número Interior', `${vacio(df.numExt)} ${vacio(df.numInt)}`.trim()], + ['Colonia o Zona Urbana', vacio(df.colonia)], + ['Municipio o Demarcación Territorial', vacio(df.municipio)], + ['Entidad Federativa / Código Postal / País', `${vacio(df.estado)} ${vacio(df.cp)} ${vacio(df.pais)}`.trim()], + ], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['7 - Número Telefónico Casa', '7 - Número Telefónico Celular']], + body: [[vacio(g.telCasa), vacio(g.celular)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['8 - Correo Electrónico Válido', '9 - CURP']], + body: [[g.email, g.curp]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['10 - RFC (SAT)']], + body: [[g.rfc]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['11 - Datos de la Identificación Oficial Vigente', '']], + body: [ + ['Nombre en Identificación', g.nombreCompleto], + ['Autoridad que la Emite', vacio(v.idAutoridadEmisora)], + ['Número de Identificación / Vigencia', `${vacio(v.idNumero)} ${formatoFechaCorta(v.idVigencia ?? '')}`.trim()], + ], + }) + y = finalY(doc) + 5 + + // Textos legales PLD + y = parrafo(doc, y, PLD_TEXTO_PROTECCION_DATOS) + y = parrafo(doc, y, 'IV.- DOCUMENTOS QUE SE INTEGRAN AL PRESENTE', { negritas: true }) + y = parrafo(doc, y, PLD_TEXTO_DOCUMENTOS_INTEGRAN) + y = parrafo(doc, y, 'Identificación Oficial con Fotografía Comprobante de Domicilio Actual RFC CURP', { + negritas: true, + }) + y = parrafo(doc, y, PLD_TEXTO_BENEFICIARIO) + y = parrafo(doc, y, PLD_TEXTO_RESPONSABILIDAD) + y = parrafo(doc, y, PLD_TEXTO_OTRO_FORMATO) + y += 10 + y = lineaFirma(doc, y, MARGEN + 40, 100, `NOMBRE Y FIRMA DEL CLIENTE\n${g.nombreCompleto}`, firma.firmaDataUrl) + + // Página 2: beneficiario controlador + doc.addPage() + y = titulo(doc, MARGEN + 4, 'FORMATO PARA LA IDENTIFICACIÓN DEL\nBENEFICIARIO CONTROLADOR') + const condominio = vacio(v.manzana) || '___' + y = parrafo(doc, y, beneficiarioControladorIntro(g.nombreCompleto, vacio(v.ubicacion) || '___', condominio)) + y = parrafo(doc, y, BENEFICIARIO_CONTROLADOR_ARTICULO3) + y = parrafo(doc, y, BENEFICIARIO_CONTROLADOR_MANIFIESTO) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['Nombre', 'Nacionalidad']], + body: [ + [g.nombreCompleto, vacio(g.nacionalidad)], + ], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['Fecha Nacimiento', 'Lugar Nacimiento']], + body: [[formatoFechaCorta(g.fechaNacimiento), vacio(g.lugarNacimiento)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['Domicilio']], + body: [[`${vacio(g.direccion)} ${vacio(g.codigoPostal)} ${vacio(g.delegacionMunicipio)} BC MEXICO`.trim()]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['Teléfono Fijo', 'Teléfono Celular']], + body: [[vacio(g.telCasa), vacio(g.celular)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['Correo Electrónico', 'Ocupación']], + body: [[g.email, vacio(g.actividadOcupacion)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['C.U.R.P', 'R.F.C']], + body: [[g.curp, g.rfc]], + }) + y = finalY(doc) + 5 + y = parrafo(doc, y, BENEFICIARIO_CONTROLADOR_CIERRE) + y += 10 + lineaFirma(doc, y, MARGEN + 40, 100, `NOMBRE Y FIRMA DEL CLIENTE\n${g.nombreCompleto}`, firma.firmaDataUrl) + + return { doc, nombreArchivo: nombreArchivoPdf('formato_identificacion_cliente', g.nombreCompleto) } +} + +/** Genera y descarga el PDF del formato PLD de identificación */ +export function descargar(alta: AltaCliente): void { + const { doc, nombreArchivo } = crearDocumento(alta) + doc.save(nombreArchivo) +} diff --git a/apps/alta-clientes/src/lib/pdf/pdf.test.ts b/apps/alta-clientes/src/lib/pdf/pdf.test.ts new file mode 100644 index 0000000..ba182a7 --- /dev/null +++ b/apps/alta-clientes/src/lib/pdf/pdf.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest' +import { fixtureAlta } from '../fixture' +import * as perfilCliente from './perfilCliente' +import * as identificacionCliente from './identificacionCliente' +import * as avisoPrivacidad from './avisoPrivacidad' +import * as cartaAntilavado from './cartaAntilavado' +import * as checklistExpediente from './checklistExpediente' + +const generadores = [ + ['perfil_cliente_completo', perfilCliente], + ['formato_identificacion_cliente', identificacionCliente], + ['aviso_privacidad_firmado', avisoPrivacidad], + ['carta_antilavado_firmada', cartaAntilavado], + ['checklist_expediente', checklistExpediente], +] as const + +describe('generadores de PDF (smoke test con fixture)', () => { + for (const [tipo, modulo] of generadores) { + it(`construye ${tipo} sin lanzar error`, () => { + const { doc, nombreArchivo } = modulo.crearDocumento(fixtureAlta) + expect(doc).toBeDefined() + expect(doc.getNumberOfPages()).toBeGreaterThan(0) + expect(nombreArchivo).toMatch( + new RegExp(`^${tipo}_PASCUAL_MARTIN_TELLEZ_CHIGUINDO_\\d{4}-\\d{2}-\\d{2}\\.pdf$`), + ) + }) + } + + it('el formato PLD y el checklist agregan la segunda página', () => { + expect(identificacionCliente.crearDocumento(fixtureAlta).doc.getNumberOfPages()).toBeGreaterThanOrEqual(2) + expect(checklistExpediente.crearDocumento(fixtureAlta).doc.getNumberOfPages()).toBeGreaterThanOrEqual(2) + }) +}) diff --git a/apps/alta-clientes/src/lib/pdf/perfilCliente.ts b/apps/alta-clientes/src/lib/pdf/perfilCliente.ts new file mode 100644 index 0000000..0c638ff --- /dev/null +++ b/apps/alta-clientes/src/lib/pdf/perfilCliente.ts @@ -0,0 +1,245 @@ +import { jsPDF } from 'jspdf' +import autoTable from 'jspdf-autotable' +import type { AltaCliente } from '../../types' +import { formatoMoneda, formatoFechaCorta, nombreArchivoPdf } from '../formatters' +import { calcularTotalIngresos, calcularGastosTotal } from '../calculations' +import { TIPOS_CREDITO } from '../../data/constantes' +import { MARGEN, finalY, titulo, lineaFirma, numero, type DocumentoPdf } from './helpers' + +const vacio = (v: string | undefined | null): string => (v && v.trim() !== '' ? v : '—') + +function seccion(doc: jsPDF, y: number, texto: string): number { + if (y > 260) { + doc.addPage() + y = MARGEN + } + doc.setFontSize(10) + doc.setFont('helvetica', 'bold') + doc.text(texto, MARGEN, y) + return y + 3 +} + +/** Construye el PDF "PERFIL DEL CLIENTE" (sin descargarlo) */ +export function crearDocumento(alta: AltaCliente): DocumentoPdf { + const { datosGenerales: g, empleo: e, economicos: eco, coacreditado, referencias, viviendaCredito: v, firma } = alta + const doc = new jsPDF() + let y = titulo(doc, MARGEN + 4, 'PERFIL DEL CLIENTE', 14) + + // PERSONALES + y = seccion(doc, y, 'PERSONALES') + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['FECHA', 'NOMBRE', 'RFC']], + body: [ + [formatoFechaCorta(g.fecha), g.nombreCompleto, g.rfc], + ], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['FECHA NACIMIENTO', 'LUGAR DE NACIMIENTO', 'NACIONALIDAD']], + body: [[formatoFechaCorta(g.fechaNacimiento), vacio(g.lugarNacimiento), vacio(g.nacionalidad)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['CURP', 'CORREO', 'TELÉFONO']], + body: [[g.curp, g.email, g.celular]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['ESTADO CIVIL', 'RÉGIMEN BIENES', 'PESO', 'ESTATURA']], + body: [[vacio(g.estadoCivil), vacio(g.regimenBienes), vacio(g.peso), vacio(g.estatura)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['NIVEL DE ESTUDIOS', 'DIRECCIÓN']], + body: [[vacio(g.nivelEstudios), vacio(g.direccion)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['DELEG./MUNICIPIO', 'C.P.', 'ANTIGÜEDAD', 'TIPO DE VIVIENDA', 'TIPO DE COMPRA']], + body: [ + [vacio(g.delegacionMunicipio), vacio(g.codigoPostal), vacio(g.antiguedadDomicilio), vacio(g.tipoVivienda), vacio(g.tipoCompra)], + ], + }) + y = finalY(doc) + 6 + + // EMPLEO + y = seccion(doc, y, 'EMPLEO') + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['TIPO DE EMPLEO', 'TIPO DE ECONOMÍA', 'COMPAÑÍA', 'ANTIGÜEDAD']], + body: [ + [ + vacio(e.tipoEmpleo), + vacio(e.tipoEconomia), + vacio(e.compania), + `${vacio(e.antiguedadAnios)} AÑOS ${vacio(e.antiguedadMeses)} MESES`, + ], + ], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['DIRECCIÓN', 'C.P.', 'PUESTO', 'TELÉFONO', 'EXT.']], + body: [ + [ + [vacio(e.direccion), vacio(e.colonia), vacio(e.numero)].filter((p) => p !== '—').join(' ') || '—', + vacio(e.codigoPostal), + vacio(e.puesto), + vacio(e.telefono), + vacio(e.ext), + ], + ], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['INGRESO MENSUAL', 'OTROS INGRESOS', 'TOTAL INGRESOS', 'GASTOS APROXIMADOS', 'DEPENDIENTES ECONÓMICOS']], + body: [ + [ + formatoMoneda(eco.ingresosMensuales), + formatoMoneda(eco.otrosIngresos), + formatoMoneda(calcularTotalIngresos(eco)), + formatoMoneda(calcularGastosTotal(eco)), + vacio(eco.dependientes.personas), + ], + ], + }) + y = finalY(doc) + 6 + + // CO-ACREDITADO O CÓNYUGE + if (coacreditado.tieneCoacreditado) { + const cg = coacreditado.generales + const ce = coacreditado.empleo + const ceco = coacreditado.economicos + y = seccion(doc, y, 'CO-ACREDITADO O CÓNYUGE') + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['NOMBRE', 'RFC', 'FECHA NACIMIENTO', 'CURP']], + body: [[cg.nombreCompleto, vacio(cg.rfc), formatoFechaCorta(cg.fechaNacimiento ?? '') || '—', vacio(cg.curp)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['CORREO', 'TELÉFONO', 'ESTADO CIVIL', 'COMPAÑÍA', 'PUESTO']], + body: [[vacio(cg.email), vacio(cg.celular), vacio(cg.estadoCivil), vacio(ce.compania), vacio(ce.puesto)]], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['INGRESO MENSUAL', 'OTROS INGRESOS', 'TOTAL INGRESOS', 'GASTOS APROXIMADOS']], + body: [ + [ + formatoMoneda(ceco.ingresosMensuales), + formatoMoneda(ceco.otrosIngresos), + formatoMoneda(calcularTotalIngresos(ceco)), + formatoMoneda(calcularGastosTotal(ceco)), + ], + ], + }) + y = finalY(doc) + 6 + } + + // REFERENCIAS PERSONALES + y = seccion(doc, y, 'REFERENCIAS PERSONALES') + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['NOMBRE', 'TELÉFONO', 'PARENTESCO', 'DIRECCIÓN']], + body: referencias.map((r) => [vacio(r.nombre), vacio(r.telefono), vacio(r.parentesco), vacio(r.direccion)]), + }) + y = finalY(doc) + 6 + + // DESARROLLO + y = seccion(doc, y, 'DESARROLLO') + const proyecto = v.proyecto === 'OTRO' ? v.proyectoOtro ?? '' : v.proyecto + const creditos = v.tiposCredito + .map((t) => TIPOS_CREDITO.find((tc) => tc.valor === t)?.etiqueta ?? t) + .join(', ') + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['PROYECTO', 'UBICACIÓN', 'PROTOTIPO']], + body: [ + [ + [`${vacio(v.manzana)} ${vacio(v.lote)}`.replace(/—/g, '').trim(), proyecto, vacio(v.modelo)].join(' ').trim(), + vacio(v.ubicacion), + vacio(v.modelo), + ], + ], + }) + y = finalY(doc) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 7, cellPadding: 1 }, + headStyles: { fillColor: [4, 120, 87] }, + head: [['VALOR VIVIENDA', 'ENGANCHE', 'TIPO DE CRÉDITO', 'MONTO CRÉDITO INFO/FOVI']], + body: [ + [ + formatoMoneda(numero(v.valorVenta)), + formatoMoneda(numero(v.enganche)), + creditos || '—', + formatoMoneda(numero(v.montoCreditoInfoFovi)), + ], + ], + }) + y = finalY(doc) + 18 + + // Firmas + lineaFirma(doc, y, MARGEN + 10, 70, g.nombreCompleto, firma.firmaDataUrl) + lineaFirma(doc, y, 120, 70, firma.asesorNombre || 'ASESOR') + + return { doc, nombreArchivo: nombreArchivoPdf('perfil_cliente_completo', g.nombreCompleto) } +} + +/** Genera y descarga el PDF del perfil del cliente */ +export function descargar(alta: AltaCliente): void { + const { doc, nombreArchivo } = crearDocumento(alta) + doc.save(nombreArchivo) +} diff --git a/apps/alta-clientes/src/lib/schemas.test.ts b/apps/alta-clientes/src/lib/schemas.test.ts new file mode 100644 index 0000000..9b463aa --- /dev/null +++ b/apps/alta-clientes/src/lib/schemas.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect } from 'vitest' +import { + datosGeneralesSchema, + coacreditadoSchema, + referenciasSchema, + REGEX_CURP, + REGEX_RFC, +} from './schemas' +import type { DatosGenerales } from '../types' + +// Datos válidos base con el fixture del cliente del PDF de referencia +const generalesValidos: DatosGenerales = { + campaniaVenta: '', + fecha: '2026-07-19', + nombreCompleto: 'PASCUAL MARTIN TELLEZ CHIGUINDO', + nacionalidad: 'MEXICANA', + nivelEstudios: 'MAESTRIA', + curp: 'TECP891124HBCLHS07', + rfc: 'TECP8911246B4', + nss: '', + delegacionMunicipio: 'TIJUANA', + direccion: 'RIO EUFRATES EDIF 1 7145 203', + codigoPostal: '22223', + antiguedadDomicilio: '2', + fechaNacimiento: '1989-11-24', + lugarNacimiento: 'TIJUANA', + estadoCivil: 'SOLTERO', + regimenBienes: '', + peso: '87', + estatura: '1.78', + email: 'dantemartinchi@gmail.com', + telCasa: '', + celular: '6646935924', + viviendaAnios: '2', + tipoVivienda: 'PROPIA', + tipoCompra: 'VIVIR_AHI', + actividadOcupacion: 'AGENTE DEL MINISTERIO PUBLICO FEDERAL', +} + +describe('regex de identificadores', () => { + it('acepta CURP válida', () => { + expect(REGEX_CURP.test('TECP891124HBCLHS07')).toBe(true) + }) + + it('rechaza CURP inválida', () => { + expect(REGEX_CURP.test('TECP891124HBCLHS0')).toBe(false) // corta + expect(REGEX_CURP.test('tecp891124hbclhs07')).toBe(false) // minúsculas + expect(REGEX_CURP.test('TECP89112XHBCLHS07')).toBe(false) // fecha inválida + }) + + it('acepta RFC válido', () => { + expect(REGEX_RFC.test('TECP8911246B4')).toBe(true) + }) + + it('rechaza RFC inválido', () => { + expect(REGEX_RFC.test('TECP8911246B')).toBe(false) // corto + expect(REGEX_RFC.test('TECP891124!B4')).toBe(false) + expect(REGEX_RFC.test('tecp8911246b4')).toBe(false) + }) +}) + +describe('datosGeneralesSchema', () => { + it('acepta los datos del fixture', () => { + expect(datosGeneralesSchema.safeParse(generalesValidos).success).toBe(true) + }) + + it('rechaza CURP inválida', () => { + const r = datosGeneralesSchema.safeParse({ ...generalesValidos, curp: 'XXXX000000XXXXXXX0' }) + expect(r.success).toBe(false) + }) + + it('rechaza RFC inválido', () => { + const r = datosGeneralesSchema.safeParse({ ...generalesValidos, rfc: '123' }) + expect(r.success).toBe(false) + }) + + it('rechaza email inválido', () => { + const r = datosGeneralesSchema.safeParse({ ...generalesValidos, email: 'no-es-correo' }) + expect(r.success).toBe(false) + }) + + it('rechaza código postal que no tenga 5 dígitos', () => { + const r = datosGeneralesSchema.safeParse({ ...generalesValidos, codigoPostal: '2223' }) + expect(r.success).toBe(false) + }) + + it('exige régimen de bienes solo cuando el estado civil es CASADO', () => { + const casado = datosGeneralesSchema.safeParse({ ...generalesValidos, estadoCivil: 'CASADO' }) + expect(casado.success).toBe(false) + const casadoConRegimen = datosGeneralesSchema.safeParse({ + ...generalesValidos, + estadoCivil: 'CASADO', + regimenBienes: 'SEPARACION_BIENES', + }) + expect(casadoConRegimen.success).toBe(true) + }) +}) + +describe('coacreditadoSchema', () => { + const base = { + generales: { ...generalesValidos, nombreCompleto: '', curp: '', rfc: '', email: '', fechaNacimiento: '', celular: '' }, + empleo: { + tipoEmpleo: '' as const, + tipoEconomia: '' as const, + compania: '', + antiguedadAnios: '', + antiguedadMeses: '', + direccion: '', + colonia: '', + numero: '', + codigoPostal: '', + puesto: '', + telefono: '', + ext: '', + delegacion: '', + }, + economicos: { + ingresosMensuales: 0, + otrosIngresos: 0, + gastosLuz: 0, + gastosOtros: 0, + dependientes: { personas: '', parentesco: '', edad: '' }, + }, + } + + it('no exige datos del co-acreditado cuando el toggle está apagado', () => { + const r = coacreditadoSchema.safeParse({ ...base, tieneCoacreditado: false }) + expect(r.success).toBe(true) + }) + + it('exige el nombre del co-acreditado cuando el toggle está activo', () => { + const r = coacreditadoSchema.safeParse({ ...base, tieneCoacreditado: true }) + expect(r.success).toBe(false) + }) + + it('acepta co-acreditado con nombre cuando el toggle está activo', () => { + const r = coacreditadoSchema.safeParse({ + ...base, + tieneCoacreditado: true, + generales: { ...base.generales, nombreCompleto: 'MARIA LOPEZ PEREZ' }, + }) + expect(r.success).toBe(true) + }) +}) + +describe('referenciasSchema', () => { + const vacia = { nombre: '', telefono: '', parentesco: '', direccion: '' } + + it('rechaza cuando ninguna referencia tiene nombre y teléfono', () => { + const r = referenciasSchema.safeParse([vacia, vacia, vacia, vacia]) + expect(r.success).toBe(false) + }) + + it('rechaza referencia con nombre pero sin teléfono', () => { + const r = referenciasSchema.safeParse([ + { ...vacia, nombre: 'JOSE ARNOLDO MENDOZA' }, + vacia, + vacia, + vacia, + ]) + expect(r.success).toBe(false) + }) + + it('acepta con al menos 1 referencia válida', () => { + const r = referenciasSchema.safeParse([ + { nombre: 'JOSE ARNOLDO MENDOZA', telefono: '6641887799', parentesco: 'AMIGO', direccion: 'ALTAMIRA' }, + vacia, + vacia, + vacia, + ]) + expect(r.success).toBe(true) + }) +}) diff --git a/apps/alta-clientes/src/lib/schemas.ts b/apps/alta-clientes/src/lib/schemas.ts new file mode 100644 index 0000000..cb18415 --- /dev/null +++ b/apps/alta-clientes/src/lib/schemas.ts @@ -0,0 +1,219 @@ +import { z } from 'zod' + +export const REGEX_CURP = /^[A-Z]{4}\d{6}[HM][A-Z]{5}[0-9A-Z]\d$/ +export const REGEX_RFC = /^[A-ZÑ&]{4}\d{6}[0-9A-Z]{3}$/ +export const REGEX_CP = /^\d{5}$/ + +// Texto opcional: cadena siempre presente (vacía si no se captura) +const textoOpcional = z.string().trim().max(200) + +const curpSchema = z + .string() + .trim() + .toUpperCase() + .regex(REGEX_CURP, 'CURP inválida (18 caracteres, formato oficial)') + +const rfcSchema = z + .string() + .trim() + .toUpperCase() + .regex(REGEX_RFC, 'RFC inválido (13 caracteres, formato persona física)') + +const cpOpcional = z + .string() + .trim() + .refine((v) => v === '' || REGEX_CP.test(v), 'Código postal de 5 dígitos') + +const curpOpcional = z + .string() + .trim() + .toUpperCase() + .refine((v) => v === '' || REGEX_CURP.test(v), 'CURP inválida') + +const rfcOpcional = z + .string() + .trim() + .toUpperCase() + .refine((v) => v === '' || REGEX_RFC.test(v), 'RFC inválido') + +const emailOpcional = z + .string() + .trim() + .refine((v) => v === '' || z.string().email().safeParse(v).success, 'Correo electrónico inválido') + +export const datosGeneralesSchema = z + .object({ + campaniaVenta: textoOpcional, + fecha: z.string().min(1, 'La fecha es requerida'), + nombreCompleto: z.string().trim().min(3, 'El nombre completo es requerido'), + nacionalidad: textoOpcional, + nivelEstudios: textoOpcional, + curp: curpSchema, + rfc: rfcSchema, + nss: textoOpcional, + delegacionMunicipio: textoOpcional, + direccion: textoOpcional, + codigoPostal: cpOpcional, + antiguedadDomicilio: textoOpcional, + fechaNacimiento: z.string().min(1, 'La fecha de nacimiento es requerida'), + lugarNacimiento: textoOpcional, + estadoCivil: z.enum(['SOLTERO', 'CASADO', 'UNION_LIBRE', 'DIVORCIADO', 'VIUDO', '']), + regimenBienes: z.enum(['SOCIEDAD_CONYUGAL', 'SEPARACION_BIENES', '']), + peso: textoOpcional, + estatura: textoOpcional, + email: z.string().trim().email('Correo electrónico inválido'), + telCasa: textoOpcional, + celular: z.string().trim().min(8, 'El celular es requerido'), + viviendaAnios: textoOpcional, + tipoVivienda: z.enum(['PRESTADA', 'RENTADA', 'FAMILIAR', 'PROPIA', '']), + tipoCompra: z.enum(['INVERSION', 'VIVIR_AHI', '']), + actividadOcupacion: textoOpcional, + }) + .superRefine((datos, ctx) => { + // Régimen de bienes solo aplica (y se exige) cuando el estado civil es CASADO + if (datos.estadoCivil === 'CASADO' && datos.regimenBienes === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['regimenBienes'], + message: 'Selecciona el régimen de bienes', + }) + } + }) + +export const empleoSchema = z.object({ + tipoEmpleo: z.enum(['ASALARIADO', 'INDEPENDIENTE', '']), + tipoEconomia: z.enum(['E_MEXICANA', 'E_AMERICANA', 'MIXTO', '']), + compania: textoOpcional, + antiguedadAnios: textoOpcional, + antiguedadMeses: textoOpcional, + direccion: textoOpcional, + colonia: textoOpcional, + numero: textoOpcional, + codigoPostal: cpOpcional, + puesto: textoOpcional, + telefono: textoOpcional, + ext: textoOpcional, + delegacion: textoOpcional, +}) + +const numeroNoNegativo = z.number({ invalid_type_error: 'Ingresa un número' }).min(0, 'No puede ser negativo') + +export const economicosSchema = z.object({ + ingresosMensuales: numeroNoNegativo, + otrosIngresos: numeroNoNegativo, + gastosLuz: numeroNoNegativo, + gastosOtros: numeroNoNegativo, + dependientes: z.object({ + personas: textoOpcional, + parentesco: textoOpcional, + edad: textoOpcional, + }), +}) + +// Datos generales del co-acreditado: el nombre se exige vía superRefine solo +// cuando el toggle está activo; el resto se valida solo si se captura +const coacreditadoGeneralesSchema = datosGeneralesSchema.innerType().extend({ + nombreCompleto: z.string().trim().max(200), + curp: curpOpcional, + rfc: rfcOpcional, + email: emailOpcional, + fechaNacimiento: textoOpcional, + celular: textoOpcional, +}) + +export const coacreditadoSchema = z + .object({ + tieneCoacreditado: z.boolean(), + generales: coacreditadoGeneralesSchema, + empleo: empleoSchema, + economicos: economicosSchema, + }) + .superRefine((datos, ctx) => { + // Los datos del co-acreditado solo se exigen si el toggle está activo + if (datos.tieneCoacreditado && datos.generales.nombreCompleto.trim().length < 3) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['generales', 'nombreCompleto'], + message: 'El nombre del co-acreditado es requerido', + }) + } + }) + +const referenciaSchema = z.object({ + nombre: textoOpcional, + telefono: textoOpcional, + parentesco: textoOpcional, + direccion: textoOpcional, +}) + +export const referenciasSchema = z + .array(referenciaSchema) + .length(4, 'Se capturan 4 referencias') + .refine( + (refs) => refs.some((r) => r.nombre.trim() !== '' && r.telefono.trim() !== ''), + 'Captura al menos 1 referencia con nombre y teléfono', + ) + +export const domicilioFiscalSchema = z.object({ + calle: textoOpcional, + numExt: textoOpcional, + numInt: textoOpcional, + colonia: textoOpcional, + municipio: textoOpcional, + estado: textoOpcional, + cp: cpOpcional, + pais: textoOpcional, +}) + +export const viviendaCreditoSchema = z + .object({ + proyecto: z.string().min(1, 'Selecciona el proyecto'), + proyectoOtro: textoOpcional, + modelo: textoOpcional, + manzana: textoOpcional, + lote: textoOpcional, + ubicacion: textoOpcional, + superficieM2: textoOpcional, + valorVenta: textoOpcional, + enganche: textoOpcional, + equipamientoAdicional: textoOpcional, + fechaPagoEnganche: textoOpcional, + firmaContrato: textoOpcional, + tiposCredito: z.array( + z.enum([ + 'INFONAVIT', + 'APOYO_INFONAVIT', + 'COFINAVIT', + 'SEGUNDO_CREDITO', + 'CONTADO', + 'FOVISSSTE', + 'COFI_FOVISSSTE', + 'BANCARIO', + ]), + ), + montoCreditoInfoFovi: textoOpcional, + ahorroVoluntarioSubcuenta: textoOpcional, + tipoProducto: z.enum(['PESOS_FIJOS', 'PESOS_CRECIENTES', 'VSM', '']), + descuentoMensualInfoFovissste: textoOpcional, + idAutoridadEmisora: textoOpcional, + idNumero: textoOpcional, + idVigencia: textoOpcional, + domicilioFiscal: domicilioFiscalSchema, + }) + .superRefine((datos, ctx) => { + if (datos.proyecto === 'OTRO' && datos.proyectoOtro.trim() === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['proyectoOtro'], + message: 'Escribe el nombre del proyecto', + }) + } + }) + +export const firmaSchema = z.object({ + firmaDataUrl: z + .string() + .min(1, 'La firma del cliente es requerida') + .startsWith('data:image/', 'La firma debe ser una imagen'), + asesorNombre: z.string().trim().min(3, 'El nombre del asesor es requerido'), +}) diff --git a/apps/alta-clientes/src/lib/uuid.ts b/apps/alta-clientes/src/lib/uuid.ts new file mode 100644 index 0000000..ce14dad --- /dev/null +++ b/apps/alta-clientes/src/lib/uuid.ts @@ -0,0 +1,14 @@ +/** + * Genera un UUID v4. crypto.randomUUID solo existe en contextos seguros + * (HTTPS o localhost); al servir por IP en HTTP hay que usar el fallback. + */ +export function uuid(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} diff --git a/apps/alta-clientes/src/main.tsx b/apps/alta-clientes/src/main.tsx new file mode 100644 index 0000000..22c867f --- /dev/null +++ b/apps/alta-clientes/src/main.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import '@fontsource/inter/300.css' +import '@fontsource/inter/400.css' +import '@fontsource/inter/500.css' +import '@fontsource/inter/700.css' +import './index.css' +import AltaClientePage from './pages/AltaClientePage' + +createRoot(document.getElementById('root')!).render( + + + + } /> + } /> + + + , +) diff --git a/apps/alta-clientes/src/pages/AltaClientePage.tsx b/apps/alta-clientes/src/pages/AltaClientePage.tsx new file mode 100644 index 0000000..7ffb85c --- /dev/null +++ b/apps/alta-clientes/src/pages/AltaClientePage.tsx @@ -0,0 +1,134 @@ +import { useState } from 'react' +import Stepper from '../components/Stepper' +import DatosGeneralesForm from '../forms/DatosGeneralesForm' +import EmpleoForm from '../forms/EmpleoForm' +import EconomicosForm from '../forms/EconomicosForm' +import CoacreditadoForm from '../forms/CoacreditadoForm' +import ReferenciasForm from '../forms/ReferenciasForm' +import ViviendaCreditoForm from '../forms/ViviendaCreditoForm' +import DocumentosForm from '../forms/DocumentosForm' +import FirmasForm from '../forms/FirmasForm' +import { useAltaCliente, PASOS } from '../hooks/useAltaCliente' +import { crearAltaCliente, subirDocumentos } from '../services/api' +import { enviarAMonday } from '../services/monday' + +export default function AltaClientePage() { + const { paso, siguiente, anterior, irAPaso, datos, alta, actualizar, agregarDocumento, quitarDocumento } = + useAltaCliente() + const [registro, setRegistro] = useState('') + + // Registra el alta (stub API + Monday) al llegar al paso de firmas + const registrarAlta = async () => { + const { id } = await crearAltaCliente(alta) + await subirDocumentos(id, alta) + await enviarAMonday(alta) + setRegistro(id) + } + + return ( +
+
+
+ FRASA +

Alta de Clientes

+
+
+ +
+ + +
+ {paso === 0 && ( + { + actualizar('datosGenerales', v) + siguiente() + }} + /> + )} + {paso === 1 && ( + { + actualizar('empleo', v) + siguiente() + }} + /> + )} + {paso === 2 && ( + { + actualizar('economicos', v) + siguiente() + }} + /> + )} + {paso === 3 && ( + { + actualizar('coacreditado', v) + siguiente() + }} + /> + )} + {paso === 4 && ( + { + actualizar('referencias', v) + siguiente() + }} + /> + )} + {paso === 5 && ( + { + actualizar('viviendaCredito', v) + siguiente() + }} + /> + )} + {paso === 6 && ( + + actualizar('datosGenerales', { ...datos.datosGenerales, ...campos }) + } + onAnterior={anterior} + onSiguiente={() => { + void registrarAlta() + siguiente() + }} + /> + )} + {paso === 7 && ( + <> + {registro && ( +

+ Alta registrada (stub API): {registro} +

+ )} + actualizar('firma', f)} + onAnterior={anterior} + /> + + )} +
+
+
+ ) +} diff --git a/apps/alta-clientes/src/services/api.ts b/apps/alta-clientes/src/services/api.ts new file mode 100644 index 0000000..22ddb7b --- /dev/null +++ b/apps/alta-clientes/src/services/api.ts @@ -0,0 +1,21 @@ +import type { AltaCliente } from '../types' +import { uuid } from '../lib/uuid' + +export interface AltaClienteCreada { + id: string +} + +/** Registra el alta de cliente en el backend */ +export async function crearAltaCliente(payload: AltaCliente): Promise { + // TODO: POST /api/clientes + await new Promise((resolve) => setTimeout(resolve, 400)) + console.log('[api] crearAltaCliente (stub):', payload.datosGenerales.nombreCompleto) + return { id: uuid() } +} + +/** Sube los documentos del expediente del cliente */ +export async function subirDocumentos(clienteId: string, payload: AltaCliente): Promise { + // TODO: POST multipart a /api/clientes/:id/documentos + await new Promise((resolve) => setTimeout(resolve, 200)) + console.log(`[api] subirDocumentos (stub) cliente=${clienteId}:`, payload.documentos.length, 'archivos') +} diff --git a/apps/alta-clientes/src/services/monday.ts b/apps/alta-clientes/src/services/monday.ts new file mode 100644 index 0000000..837d931 --- /dev/null +++ b/apps/alta-clientes/src/services/monday.ts @@ -0,0 +1,12 @@ +import type { AltaCliente } from '../types' + +/** Envía el alta al tablero de Monday (Contactos + Apartado) */ +export async function enviarAMonday(payload: AltaCliente): Promise { + // TODO: integración Monday — crear item en tablero "Contactos" y "Apartado" + // con nombre, teléfono, correo, proyecto y ubicación del cliente + console.log('[monday] enviarAMonday (stub):', { + nombre: payload.datosGenerales.nombreCompleto, + proyecto: payload.viviendaCredito.proyecto, + ubicacion: payload.viviendaCredito.ubicacion, + }) +} diff --git a/apps/alta-clientes/src/types/index.ts b/apps/alta-clientes/src/types/index.ts new file mode 100644 index 0000000..f24acb4 --- /dev/null +++ b/apps/alta-clientes/src/types/index.ts @@ -0,0 +1,157 @@ +// Tipos del dominio "Alta de Clientes" — basados en el formato Excel ALTA de Frasa Desarrollos + +export type EstadoCivil = 'SOLTERO' | 'CASADO' | 'UNION_LIBRE' | 'DIVORCIADO' | 'VIUDO' +export type RegimenBienes = 'SOCIEDAD_CONYUGAL' | 'SEPARACION_BIENES' +export type TipoVivienda = 'PRESTADA' | 'RENTADA' | 'FAMILIAR' | 'PROPIA' +export type TipoCompra = 'INVERSION' | 'VIVIR_AHI' +export type TipoEmpleo = 'ASALARIADO' | 'INDEPENDIENTE' +export type TipoEconomia = 'E_MEXICANA' | 'E_AMERICANA' | 'MIXTO' +export type TipoProducto = 'PESOS_FIJOS' | 'PESOS_CRECIENTES' | 'VSM' + +export type TipoCredito = + | 'INFONAVIT' + | 'APOYO_INFONAVIT' + | 'COFINAVIT' + | 'SEGUNDO_CREDITO' + | 'CONTADO' + | 'FOVISSSTE' + | 'COFI_FOVISSSTE' + | 'BANCARIO' + +export interface DatosGenerales { + campaniaVenta: string + fecha: string + nombreCompleto: string + nacionalidad: string + nivelEstudios: string + curp: string + rfc: string + nss: string + delegacionMunicipio: string + direccion: string + codigoPostal: string + antiguedadDomicilio: string + fechaNacimiento: string + lugarNacimiento: string + estadoCivil: EstadoCivil | '' + regimenBienes: RegimenBienes | '' + peso: string + estatura: string + email: string + telCasa: string + celular: string + viviendaAnios: string + tipoVivienda: TipoVivienda | '' + tipoCompra: TipoCompra | '' + actividadOcupacion: string +} + +export interface DatosEmpleo { + tipoEmpleo: TipoEmpleo | '' + tipoEconomia: TipoEconomia | '' + compania: string + antiguedadAnios: string + antiguedadMeses: string + direccion: string + colonia: string + numero: string + codigoPostal: string + puesto: string + telefono: string + ext: string + delegacion: string +} + +export interface Dependientes { + personas: string + parentesco: string + edad: string +} + +export interface DatosEconomicos { + ingresosMensuales: number + otrosIngresos: number + gastosLuz: number + gastosOtros: number + dependientes: Dependientes +} + +export interface Coacreditado { + tieneCoacreditado: boolean + generales: DatosGenerales + empleo: DatosEmpleo + economicos: DatosEconomicos +} + +export interface Referencia { + nombre: string + telefono: string + parentesco: string + direccion: string +} + +export interface DomicilioFiscal { + calle: string + numExt: string + numInt: string + colonia: string + municipio: string + estado: string + cp: string + pais: string +} + +export interface ViviendaCredito { + proyecto: string + proyectoOtro: string + modelo: string + manzana: string + lote: string + ubicacion: string + superficieM2: string + valorVenta: string + enganche: string + equipamientoAdicional: string + fechaPagoEnganche: string + firmaContrato: string + tiposCredito: TipoCredito[] + montoCreditoInfoFovi: string + ahorroVoluntarioSubcuenta: string + tipoProducto: TipoProducto | '' + descuentoMensualInfoFovissste: string + idAutoridadEmisora: string + idNumero: string + idVigencia: string + domicilioFiscal: DomicilioFiscal +} + +export type SlotDocumento = + | 'INE' + | 'CONSTANCIA_RFC' + | 'CURP' + | 'COMPROBANTE_DOMICILIO' + | 'ACTA_NACIMIENTO' + +export interface DocumentoCargado { + slot: SlotDocumento + nombre: string + tamano: number + tipoMime: string + archivo: File +} + +export interface DatosFirma { + firmaDataUrl: string + asesorNombre: string +} + +export interface AltaCliente { + datosGenerales: DatosGenerales + empleo: DatosEmpleo + economicos: DatosEconomicos + coacreditado: Coacreditado + referencias: Referencia[] + viviendaCredito: ViviendaCredito + documentos: DocumentoCargado[] + firma: DatosFirma +} diff --git a/apps/alta-clientes/tsconfig.app.json b/apps/alta-clientes/tsconfig.app.json new file mode 100644 index 0000000..f908190 --- /dev/null +++ b/apps/alta-clientes/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/apps/alta-clientes/tsconfig.json b/apps/alta-clientes/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/alta-clientes/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/alta-clientes/tsconfig.node.json b/apps/alta-clientes/tsconfig.node.json new file mode 100644 index 0000000..2af732d --- /dev/null +++ b/apps/alta-clientes/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts", "vitest.config.ts"] +} diff --git a/apps/alta-clientes/vite.config.ts b/apps/alta-clientes/vite.config.ts new file mode 100644 index 0000000..e263164 --- /dev/null +++ b/apps/alta-clientes/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +// La app se sirve detrás de NGINX bajo el prefijo /client +export default defineConfig({ + base: '/client/', + plugins: [react(), tailwindcss()], +}) diff --git a/apps/alta-clientes/vitest.config.ts b/apps/alta-clientes/vitest.config.ts new file mode 100644 index 0000000..58eb15a --- /dev/null +++ b/apps/alta-clientes/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +// Config de tests separada para no mezclar los tipos de Vite y Vitest +export default defineConfig({ + test: { + environment: 'node', + }, +}) diff --git a/apps/cobranza/.dockerignore b/apps/cobranza/.dockerignore new file mode 100644 index 0000000..7f1ee88 --- /dev/null +++ b/apps/cobranza/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.git +.gitignore +*.log +coverage +.vscode +Dockerfile +.dockerignore diff --git a/apps/cobranza/.gitignore b/apps/cobranza/.gitignore new file mode 100644 index 0000000..e7b5da1 --- /dev/null +++ b/apps/cobranza/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.log +coverage +*.tsbuildinfo diff --git a/apps/cobranza/Dockerfile b/apps/cobranza/Dockerfile new file mode 100644 index 0000000..43d5c34 --- /dev/null +++ b/apps/cobranza/Dockerfile @@ -0,0 +1,14 @@ +# Etapa 1: build +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Etapa 2: servidor estático +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/cobranza/index.html b/apps/cobranza/index.html new file mode 100644 index 0000000..61c2647 --- /dev/null +++ b/apps/cobranza/index.html @@ -0,0 +1,12 @@ + + + + + + FRASA Desarrollos — Cobranza + + +
+ + + diff --git a/apps/cobranza/nginx.conf b/apps/cobranza/nginx.conf new file mode 100644 index 0000000..f099859 --- /dev/null +++ b/apps/cobranza/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # SPA fallback: cualquier ruta no existente sirve index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Assets con hash: cache agresivo + location /pay/assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/apps/cobranza/package-lock.json b/apps/cobranza/package-lock.json new file mode 100644 index 0000000..2c67f62 --- /dev/null +++ b/apps/cobranza/package-lock.json @@ -0,0 +1,4525 @@ +{ + "name": "cobranza", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cobranza", + "version": "0.0.0", + "dependencies": { + "@fontsource/inter": "^5.3.0", + "@hookform/resolvers": "^3.9.1", + "jspdf": "^2.5.2", + "jspdf-autotable": "^3.8.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-router-dom": "^7.1.1", + "recharts": "^2.15.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", + "typescript": "~5.6.3", + "vite": "^6.0.7", + "vitest": "^2.1.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "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/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "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/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.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "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/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "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/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-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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/dompurify": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", + "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.411", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", + "integrity": "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-equals": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", + "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/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/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/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/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.8.4.tgz", + "integrity": "sha512-rSffGoBsJYX83iTRv8Ft7FhqfgEL2nLpGAIiqruEQQ3e4r0qdLFbPUB7N9HAle0I3XgpisvyW751VHCqKUVOgQ==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2.5.1" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "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/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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/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.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-hook-form": { + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz", + "integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==", + "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": "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/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "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-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==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "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/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "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/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "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/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "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": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/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/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": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/apps/cobranza/package.json b/apps/cobranza/package.json new file mode 100644 index 0000000..b071d05 --- /dev/null +++ b/apps/cobranza/package.json @@ -0,0 +1,34 @@ +{ + "name": "cobranza", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "test": "vitest run", + "preview": "vite preview" + }, + "dependencies": { + "@fontsource/inter": "^5.3.0", + "@hookform/resolvers": "^3.9.1", + "jspdf": "^2.5.2", + "jspdf-autotable": "^3.8.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-router-dom": "^7.1.1", + "recharts": "^2.15.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", + "typescript": "~5.6.3", + "vite": "^6.0.7", + "vitest": "^2.1.8" + } +} diff --git a/apps/cobranza/src/components/BadgeStatus.tsx b/apps/cobranza/src/components/BadgeStatus.tsx new file mode 100644 index 0000000..3cd8d5e --- /dev/null +++ b/apps/cobranza/src/components/BadgeStatus.tsx @@ -0,0 +1,18 @@ +import type { StatusPago } from '../types' + +const CLASES: Record = { + PENDIENTE: 'border-neutral-300 text-neutral-500', + PAGADO: 'border-green-300 text-green-700', + VENCIDO: 'border-red-300 text-red-700', + PARCIAL: 'border-amber-300 text-amber-700', +} + +export default function BadgeStatus({ status }: { status: StatusPago }) { + return ( + + {status} + + ) +} diff --git a/apps/cobranza/src/components/KpiCard.tsx b/apps/cobranza/src/components/KpiCard.tsx new file mode 100644 index 0000000..e841007 --- /dev/null +++ b/apps/cobranza/src/components/KpiCard.tsx @@ -0,0 +1,11 @@ +import type { ReactNode } from 'react' + +export default function KpiCard({ titulo, valor, detalle }: { titulo: string; valor: ReactNode; detalle?: string }) { + return ( +
+

{titulo}

+

{valor}

+ {detalle &&

{detalle}

} +
+ ) +} diff --git a/apps/cobranza/src/components/Layout.tsx b/apps/cobranza/src/components/Layout.tsx new file mode 100644 index 0000000..45084db --- /dev/null +++ b/apps/cobranza/src/components/Layout.tsx @@ -0,0 +1,38 @@ +import type { ReactNode } from 'react' +import { Link } from 'react-router-dom' + +export default function Layout({ children }: { children: ReactNode }) { + return ( +
+
+
+ + FRASA{' '} + COBRANZA + + +
+
+
{children}
+
+ ) +} diff --git a/apps/cobranza/src/components/ModalAbono.tsx b/apps/cobranza/src/components/ModalAbono.tsx new file mode 100644 index 0000000..58f6292 --- /dev/null +++ b/apps/cobranza/src/components/ModalAbono.tsx @@ -0,0 +1,141 @@ +import { useEffect, useRef } from 'react' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import type { Pago } from '../types' +import { fechaISOHoy, formatoMoneda } from '../lib/formatters' +import { round2 } from '../lib/cobranza' + +function esquemaAbono(restante: number) { + return z.object({ + monto: z + .number({ invalid_type_error: 'Captura un monto válido' }) + .positive('El monto debe ser mayor a 0') + .max(restante, `No puede exceder el restante (${formatoMoneda(restante)})`), + fechaPago: z.string().min(1, 'La fecha es requerida'), + referencia: z.string().trim().min(3, 'Referencia requerida (mín. 3 caracteres)'), + }) +} + +type AbonoForm = z.infer> + +interface Props { + pago: Pago + onCancelar: () => void + onGuardar: (datos: AbonoForm) => void +} + +const claseLabel = 'block text-xs font-bold uppercase tracking-widest text-neutral-500' + +/** Modal para registrar un abono (total o parcial) a un pago del plan */ +export default function ModalAbono({ pago, onCancelar, onGuardar }: Props) { + const restante = round2(pago.monto - pago.montoPagado) + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(esquemaAbono(restante)), + defaultValues: { monto: restante, fechaPago: fechaISOHoy(), referencia: '' }, + }) + + // Foco al primer input al abrir + const refMonto = useRef(null) + useEffect(() => { + refMonto.current?.focus() + }, []) + + // Escape cierra el modal + useEffect(() => { + const alTeclar = (e: KeyboardEvent) => { + if (e.key === 'Escape') onCancelar() + } + window.addEventListener('keydown', alTeclar) + return () => window.removeEventListener('keydown', alTeclar) + }, [onCancelar]) + + const regMonto = register('monto', { valueAsNumber: true }) + + return ( +
+
+

+ Registrar abono — Pago #{pago.numPago} +

+

+ Restante: {formatoMoneda(restante)} +

+
{ + void handleSubmit(onGuardar)(e) + }} + > +
+ + { + regMonto.ref(el) + refMonto.current = el + }} + /> + {errors.monto &&

{errors.monto.message}

} +
+
+ + + {errors.fechaPago &&

{errors.fechaPago.message}

} +
+
+ + + {errors.referencia &&

{errors.referencia.message}

} +
+
+ + +
+
+
+
+ ) +} diff --git a/apps/cobranza/src/components/Semaforo.tsx b/apps/cobranza/src/components/Semaforo.tsx new file mode 100644 index 0000000..dd4c55d --- /dev/null +++ b/apps/cobranza/src/components/Semaforo.tsx @@ -0,0 +1,45 @@ +import type { PlanPago } from '../types' +import { diasParaProximoVencimiento, statusPago, statusPlan } from '../lib/cobranza' + +export type Semaforo = 'VERDE' | 'AMBAR' | 'ROJO' | 'GRIS' + +const ETIQUETA: Record = { + VERDE: 'Al corriente', + AMBAR: 'Por vencer / parcial', + ROJO: 'Vencido', + GRIS: 'Cerrado', +} + +const CLASE_DOT: Record = { + VERDE: 'border-green-300 bg-white text-green-700', + AMBAR: 'border-amber-300 bg-white text-amber-700', + ROJO: 'border-red-300 bg-white text-red-700', + GRIS: 'border-neutral-300 bg-white text-neutral-500', +} + +/** + * Semáforo del plan: + * - gris: LIQUIDADO o CANCELADO + * - rojo: tiene pagos VENCIDOS + * - ámbar: tiene un pago PARCIAL o el próximo vencimiento es en 7 días o menos + * - verde: al corriente + */ +export function semaforoPlan(plan: PlanPago, hoy: Date = new Date()): Semaforo { + if (statusPlan(plan) !== 'ACTIVO') return 'GRIS' + const estados = plan.pagos.map((p) => statusPago(p, hoy)) + if (estados.includes('VENCIDO')) return 'ROJO' + if (estados.includes('PARCIAL')) return 'AMBAR' + const dias = diasParaProximoVencimiento(plan, hoy) + if (dias !== null && dias <= 7) return 'AMBAR' + return 'VERDE' +} + +export default function SemaforoDot({ valor }: { valor: Semaforo }) { + return ( + + {ETIQUETA[valor]} + + ) +} diff --git a/apps/cobranza/src/data/seed.ts b/apps/cobranza/src/data/seed.ts new file mode 100644 index 0000000..c1c9cc5 --- /dev/null +++ b/apps/cobranza/src/data/seed.ts @@ -0,0 +1,124 @@ +import type { PlanPago } from '../types' +import { generarCalendario, registrarAbono, sumarMeses } from '../lib/cobranza' +import { uuid } from '../lib/uuid' + +function aISO(fecha: Date): string { + const y = fecha.getFullYear() + const m = String(fecha.getMonth() + 1).padStart(2, '0') + const d = String(fecha.getDate()).padStart(2, '0') + return `${y}-${m}-${d}` +} + +interface DatosPlan { + clienteNombre: string + clienteTelefono?: string + unidadCodigo: string + proyecto: string + ventaRef: string + fechaInicio: string + engancheTotal: number + numPagos: number + tasaMoraMensual?: number +} + +function crearPlanBase(datos: DatosPlan): PlanPago { + const { mensualidad, pagos } = generarCalendario(datos.fechaInicio, datos.engancheTotal, datos.numPagos) + return { + id: uuid(), + ventaRef: datos.ventaRef, + clienteNombre: datos.clienteNombre, + clienteTelefono: datos.clienteTelefono, + unidadCodigo: datos.unidadCodigo, + proyecto: datos.proyecto, + fechaInicio: datos.fechaInicio, + engancheTotal: datos.engancheTotal, + numPagos: datos.numPagos, + mensualidad, + tasaMoraMensual: datos.tasaMoraMensual, + status: 'ACTIVO', + pagos, + notas: [], + createdAt: new Date().toISOString(), + } +} + +/** Abona por número de pago (1..N); `monto` por defecto cubre el pago completo */ +function abonar(plan: PlanPago, numPago: number, fechaPago: string, referencia: string, monto?: number): PlanPago { + const pago = plan.pagos[numPago - 1] + return registrarAbono(plan, { + pagoId: pago.id, + fechaPago, + monto: monto ?? pago.monto - pago.montoPagado, + referencia, + }) +} + +/** + * 4 planes de ejemplo con fechas relativas a `hoy`, para que el demo siempre + * muestre todos los estados: al corriente, con vencidos, con parcial y liquidado. + */ +export function generarSeed(hoy: Date = new Date()): PlanPago[] { + // 1. Al corriente: 6 pagos desde hace 3 meses, los primeros 3 pagados + let alCorriente = crearPlanBase({ + clienteNombre: 'María Fernanda López Ríos', + clienteTelefono: '55 1234 8890', + unidadCodigo: 'MB-204', + proyecto: 'RESIDENCIAL MONTEBELLO', + ventaRef: 'VTA-2026-014', + fechaInicio: aISO(sumarMeses(hoy, -3)), + engancheTotal: 180000, + numPagos: 6, + }) + for (let i = 1; i <= 3; i++) { + alCorriente = abonar(alCorriente, i, alCorriente.pagos[i - 1].fechaVencimiento, `SPEI-MB204-0${i}`) + } + + // 2. Con 2 pagos vencidos: 6 pagos desde hace 5 meses, solo 3 pagados + let conVencidos = crearPlanBase({ + clienteNombre: 'Jorge Alberto Camacho Vega', + clienteTelefono: '55 8842 1177', + unidadCodigo: 'U-101', + proyecto: 'SENDA', + ventaRef: 'VTA-2025-089', + fechaInicio: aISO(sumarMeses(hoy, -5)), + engancheTotal: 150000, + numPagos: 6, + tasaMoraMensual: 2, + }) + for (let i = 1; i <= 3; i++) { + conVencidos = abonar(conVencidos, i, conVencidos.pagos[i - 1].fechaVencimiento, `SPEI-U101-0${i}`) + } + + // 3. Con un pago PARCIAL: 4 pagos desde hace 2 meses; pago 1 completo, pago 2 con abono parcial ya vencido + let conParcial = crearPlanBase({ + clienteNombre: 'Ana Sofía Guillén Torres', + clienteTelefono: '55 6021 3345', + unidadCodigo: 'DG-302', + proyecto: 'DIAGONAL', + ventaRef: 'VTA-2026-021', + fechaInicio: aISO(sumarMeses(hoy, -2)), + engancheTotal: 120000, + numPagos: 4, + tasaMoraMensual: 1.5, + }) + conParcial = abonar(conParcial, 1, conParcial.pagos[0].fechaVencimiento, 'SPEI-DG302-01') + conParcial = abonar(conParcial, 2, aISO(sumarMeses(hoy, -1)), 'EFECTIVO-DG302-02', 10000) + + // 4. Liquidado: 6 pagos desde hace 6 meses, todos pagados + let liquidado = crearPlanBase({ + clienteNombre: 'Roberto Mendoza Salas', + clienteTelefono: '55 9987 4456', + unidadCodigo: 'MB-118', + proyecto: 'RESIDENCIAL MONTEBELLO', + ventaRef: 'VTA-2025-041', + fechaInicio: aISO(sumarMeses(hoy, -6)), + engancheTotal: 90000, + numPagos: 6, + }) + for (let i = 1; i <= 6; i++) { + liquidado = abonar(liquidado, i, liquidado.pagos[i - 1].fechaVencimiento, `SPEI-MB118-0${i}`) + } + liquidado = { ...liquidado, status: 'LIQUIDADO' } + + return [alCorriente, conVencidos, conParcial, liquidado] +} diff --git a/apps/cobranza/src/index.css b/apps/cobranza/src/index.css new file mode 100644 index 0000000..5de6ae6 --- /dev/null +++ b/apps/cobranza/src/index.css @@ -0,0 +1,23 @@ +@import "tailwindcss"; + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; + --color-accent: #1B4332; + --radius-sm: 0; + --radius-md: 0; + --radius-lg: 0; + --radius-xl: 0; + --radius-2xl: 0; + --radius-3xl: 0; +} + +@layer base { + body { + @apply bg-white text-neutral-900 antialiased; + } + + *:focus-visible { + outline: 2px solid #1B4332; + outline-offset: 2px; + } +} diff --git a/apps/cobranza/src/lib/cobranza.test.ts b/apps/cobranza/src/lib/cobranza.test.ts new file mode 100644 index 0000000..edbe434 --- /dev/null +++ b/apps/cobranza/src/lib/cobranza.test.ts @@ -0,0 +1,326 @@ +import { describe, expect, it } from 'vitest' +import type { Pago, PlanPago } from '../types' +import { + agregarNotaAPlan, + alCorriente, + avancePlan, + cobranzaDelDia, + diasAtraso, + generarCalendario, + moraDePago, + pagadoTotal, + proximoVencimiento, + registrarAbono, + round2, + saldoPlan, + statusPago, + statusPlan, + sumarMeses, + totalMora, +} from './cobranza' + +const pagoBase = (override: Partial = {}): Pago => ({ + id: 'p1', + numPago: 1, + fechaVencimiento: '2026-02-15', + monto: 10000, + montoPagado: 0, + aplicaciones: [], + ...override, +}) + +describe('generarCalendario', () => { + it('genera 12 pagos cuya suma es exactamente el enganche', () => { + const { mensualidad, pagos } = generarCalendario('2026-01-15', 180000, 12) + expect(pagos).toHaveLength(12) + expect(mensualidad).toBe(15000) + const suma = pagos.reduce((acc, p) => acc + p.monto, 0) + expect(round2(suma)).toBe(180000) + expect(pagos[0].numPago).toBe(1) + expect(pagos[11].numPago).toBe(12) + expect(pagos[0].fechaVencimiento).toBe('2026-01-15') + expect(pagos[11].fechaVencimiento).toBe('2026-12-15') + }) + + it('el último pago absorbe el residuo del redondeo (100000 / 3)', () => { + const { mensualidad, pagos } = generarCalendario('2026-03-01', 100000, 3) + expect(mensualidad).toBe(33333.33) + expect(pagos.map((p) => p.monto)).toEqual([33333.33, 33333.33, 33333.34]) + expect(round2(pagos.reduce((acc, p) => acc + p.monto, 0))).toBe(100000) + }) + + it('ajusta el día cuando el mes no lo tiene (31 de enero → 28 de febrero)', () => { + const { pagos } = generarCalendario('2025-01-31', 30000, 3) + expect(pagos[0].fechaVencimiento).toBe('2025-01-31') + expect(pagos[1].fechaVencimiento).toBe('2025-02-28') // 2025 no es bisiesto + expect(pagos[2].fechaVencimiento).toBe('2025-03-31') + }) + + it('respeta año bisiesto (31 de enero 2024 → 29 de febrero)', () => { + expect(sumarMeses(new Date(2024, 0, 31), 1)).toEqual(new Date(2024, 1, 29)) + }) + + it('rechaza parámetros inválidos', () => { + expect(() => generarCalendario('2026-01-01', 0, 6)).toThrow() + expect(() => generarCalendario('2026-01-01', 1000, 0)).toThrow() + }) +}) + +describe('statusPago', () => { + const hoy = new Date('2026-02-16T00:00:00') + + it('PAGADO cuando montoPagado cubre el monto', () => { + expect(statusPago(pagoBase({ montoPagado: 10000 }), hoy)).toBe('PAGADO') + }) + + it('PARCIAL con abono menor al monto, aunque ya venció', () => { + expect(statusPago(pagoBase({ montoPagado: 4000 }), hoy)).toBe('PARCIAL') + }) + + it('VENCIDO sin abono y hoy posterior al vencimiento', () => { + expect(statusPago(pagoBase(), hoy)).toBe('VENCIDO') + }) + + it('PENDIENTE sin abono y hoy en o antes del vencimiento', () => { + expect(statusPago(pagoBase(), new Date('2026-02-15T00:00:00'))).toBe('PENDIENTE') + expect(statusPago(pagoBase(), new Date('2026-02-01T00:00:00'))).toBe('PENDIENTE') + }) +}) + +function planDePrueba(): PlanPago { + const { mensualidad, pagos } = generarCalendario('2026-01-10', 30000, 3) + return { + id: 'plan-1', + ventaRef: 'VTA-001', + clienteNombre: 'Cliente Prueba', + unidadCodigo: 'U-1', + proyecto: 'SENDA', + fechaInicio: '2026-01-10', + engancheTotal: 30000, + numPagos: 3, + mensualidad, + status: 'ACTIVO', + pagos, + notas: [], + createdAt: '2026-01-01T00:00:00.000Z', + } +} + +describe('registrarAbono', () => { + it('aplica un abono parcial y lo agrega al historial', () => { + const plan = planDePrueba() + const actualizado = registrarAbono(plan, { + pagoId: plan.pagos[0].id, + fechaPago: '2026-01-10', + monto: 4000, + referencia: 'SPEI-123', + }) + expect(actualizado.pagos[0].montoPagado).toBe(4000) + expect(actualizado.pagos[0].aplicaciones).toHaveLength(1) + expect(actualizado.pagos[0].aplicaciones[0].referencia).toBe('SPEI-123') + // no muta el plan original + expect(plan.pagos[0].montoPagado).toBe(0) + }) + + it('permite completar un pago con varios abonos', () => { + let plan = planDePrueba() + plan = registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-10', monto: 4000, referencia: 'A1' }) + plan = registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-11', monto: 6000, referencia: 'A2' }) + expect(plan.pagos[0].montoPagado).toBe(10000) + expect(plan.pagos[0].aplicaciones).toHaveLength(2) + expect(statusPago(plan.pagos[0], new Date('2026-01-12T00:00:00'))).toBe('PAGADO') + }) + + it('rechaza abonos que exceden el restante', () => { + const plan = planDePrueba() + expect(() => + registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-10', monto: 10000.01, referencia: 'X1' }), + ).toThrow(/excede el restante/) + }) + + it('rechaza abonos menores o iguales a 0', () => { + const plan = planDePrueba() + expect(() => + registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-10', monto: 0, referencia: 'X1' }), + ).toThrow(/mayor a 0/) + expect(() => + registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-10', monto: -500, referencia: 'X1' }), + ).toThrow(/mayor a 0/) + }) + + it('rechaza un pagoId inexistente', () => { + const plan = planDePrueba() + expect(() => + registrarAbono(plan, { pagoId: 'no-existe', fechaPago: '2026-01-10', monto: 100, referencia: 'X1' }), + ).toThrow(/no encontrado/) + }) +}) + +describe('saldo, avance y status del plan', () => { + it('saldoPlan y pagadoTotal reflejan los abonos', () => { + let plan = planDePrueba() + expect(saldoPlan(plan)).toBe(30000) + expect(pagadoTotal(plan)).toBe(0) + plan = registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-10', monto: 10000, referencia: 'A1' }) + expect(saldoPlan(plan)).toBe(20000) + expect(pagadoTotal(plan)).toBe(10000) + expect(avancePlan(plan)).toBeCloseTo(1 / 3, 5) + }) + + it('el plan queda LIQUIDADO al cubrir todo el enganche', () => { + let plan = planDePrueba() + for (const pago of plan.pagos) { + plan = registrarAbono(plan, { + pagoId: pago.id, + fechaPago: '2026-03-10', + monto: pago.monto - pago.montoPagado, + referencia: `LIQ-${pago.numPago}`, + }) + } + expect(saldoPlan(plan)).toBe(0) + expect(statusPlan(plan)).toBe('LIQUIDADO') + }) + + it('CANCELADO prevalece aunque tenga saldo', () => { + const plan = { ...planDePrueba(), status: 'CANCELADO' as const } + expect(statusPlan(plan)).toBe('CANCELADO') + }) + + it('alCorriente es falso cuando hay pagos vencidos', () => { + const hoy = new Date('2026-02-15T00:00:00') // pago 1 (10-ene) ya venció + expect(alCorriente(planDePrueba(), hoy)).toBe(false) + expect(alCorriente(planDePrueba(), new Date('2026-01-05T00:00:00'))).toBe(true) + }) +}) + +describe('proximoVencimiento y diasAtraso', () => { + it('devuelve el primer pago no cubierto con fecha >= hoy', () => { + const plan = planDePrueba() // vencimientos 10-ene, 10-feb, 10-mar + const prox = proximoVencimiento(plan, new Date('2026-01-20T00:00:00')) + expect(prox?.numPago).toBe(2) + }) + + it('si no hay futuros, devuelve el vencido más antiguo', () => { + const plan = planDePrueba() + const prox = proximoVencimiento(plan, new Date('2026-04-01T00:00:00')) + expect(prox?.numPago).toBe(1) + }) + + it('devuelve null cuando todo está cubierto', () => { + let plan = planDePrueba() + for (const pago of plan.pagos) { + plan = registrarAbono(plan, { + pagoId: pago.id, + fechaPago: '2026-01-10', + monto: pago.monto, + referencia: 'X', + }) + } + expect(proximoVencimiento(plan, new Date('2026-01-15T00:00:00'))).toBeNull() + }) + + it('diasAtraso cuenta desde el vencido más antiguo', () => { + const plan = planDePrueba() // pago 1 vence 10-ene + expect(diasAtraso(plan, new Date('2026-01-25T00:00:00'))).toBe(15) + expect(diasAtraso(plan, new Date('2026-01-10T00:00:00'))).toBe(0) + }) + + it('un pago PARCIAL vencido no cuenta como atraso', () => { + let plan = planDePrueba() + plan = registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-10', monto: 100, referencia: 'A' }) + // pago 1 es PARCIAL (vencido pero con abono); pago 2 vence 10-feb + expect(diasAtraso(plan, new Date('2026-02-01T00:00:00'))).toBe(0) + }) +}) + +describe('moraDePago y totalMora', () => { + // pago de 10000 que venció el 15-feb; hoy = 17-mar → 30 días vencidos + const hoy = new Date('2026-03-17T00:00:00') + + it('es 0 cuando la tasa es 0', () => { + expect(moraDePago(pagoBase(), 0, hoy)).toBe(0) + }) + + it('es 0 cuando el pago no está vencido', () => { + expect(moraDePago(pagoBase(), 2, new Date('2026-02-15T00:00:00'))).toBe(0) + expect(moraDePago(pagoBase(), 2, new Date('2026-02-10T00:00:00'))).toBe(0) + }) + + it('es 0 cuando el pago ya está cubierto', () => { + expect(moraDePago(pagoBase({ montoPagado: 10000 }), 2, hoy)).toBe(0) + }) + + it('cálculo exacto: restante × (tasa/100) / 30 × días vencidos', () => { + // 10000 × 0.02 / 30 × 30 = 200 + expect(moraDePago(pagoBase(), 2, hoy)).toBe(200) + // 10000 × 0.03 / 30 × 10 = 100 (10 días de atraso) + expect(moraDePago(pagoBase(), 3, new Date('2026-02-25T00:00:00'))).toBe(100) + }) + + it('un pago PARCIAL vencido genera mora sobre su restante', () => { + // restante 6000 × 0.02 / 30 × 30 = 120 + expect(moraDePago(pagoBase({ montoPagado: 4000 }), 2, hoy)).toBe(120) + }) + + it('totalMora suma la mora de todos los pagos y usa tasa 0 por defecto', () => { + const plan = planDePrueba() // vence 10-ene, 10-feb, 10-mar; hoy 20-mar + const hoyPlan = new Date('2026-03-20T00:00:00') + expect(totalMora(plan, hoyPlan)).toBe(0) // sin tasa configurada + const conTasa: PlanPago = { ...plan, tasaMoraMensual: 3 } + // pago 1: 10000 × 0.001 × 69 = 690; pago 2: 10000 × 0.001 × 38 = 380; pago 3: 10000 × 0.001 × 10 = 100 + expect(totalMora(conTasa, hoyPlan)).toBe(1170) + }) +}) + +describe('cobranzaDelDia', () => { + it('incluye vencidos y los que vencen en los próximos 7 días, ordenados por urgencia', () => { + // plan: vencimientos 10-ene, 10-feb, 10-mar; hoy 05-mar + const hoy = new Date('2026-03-05T00:00:00') + const plan = planDePrueba() + const items = cobranzaDelDia([plan], hoy) + // pago 1 (54 días atraso), pago 2 (23 días), pago 3 (vence en 5 días) + expect(items.map((i) => i.pago.numPago)).toEqual([1, 2, 3]) + expect(items.map((i) => i.atraso)).toEqual([54, 23, 0]) + }) + + it('excluye pagos cubiertos, planes no activos y vencimientos lejanos', () => { + const hoy = new Date('2026-01-05T00:00:00') + let plan = planDePrueba() + // cubre el pago 1 (vence 10-ene, dentro de la ventana) + plan = registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-05', monto: 10000, referencia: 'A' }) + const cancelado: PlanPago = { ...planDePrueba(), status: 'CANCELADO' } + const items = cobranzaDelDia([plan, cancelado], hoy) + // pago 2 (10-feb) y pago 3 (10-mar) están a más de 7 días → lista vacía + expect(items).toHaveLength(0) + // con la ventana amplia solo aparecen los del plan activo + const items40 = cobranzaDelDia([plan, cancelado], hoy, 40) + expect(items40.map((i) => i.pago.numPago)).toEqual([2]) + }) + + it('un PARCIAL vencido aparece con sus días de atraso', () => { + const hoy = new Date('2026-02-01T00:00:00') + let plan = planDePrueba() + plan = registrarAbono(plan, { pagoId: plan.pagos[0].id, fechaPago: '2026-01-10', monto: 4000, referencia: 'A' }) + const items = cobranzaDelDia([plan], hoy) + expect(items[0].pago.numPago).toBe(1) + expect(items[0].atraso).toBe(22) + }) +}) + +describe('agregarNotaAPlan', () => { + it('agrega la nota con texto limpio sin mutar el plan', () => { + const plan = planDePrueba() + const actualizado = agregarNotaAPlan(plan, ' Cliente promete pago el viernes ') + expect(actualizado.notas).toHaveLength(1) + expect(actualizado.notas[0].texto).toBe('Cliente promete pago el viernes') + expect(actualizado.notas[0].id).toBeTruthy() + expect(actualizado.notas[0].fecha).toBeTruthy() + expect(plan.notas).toHaveLength(0) + }) + + it('rechaza notas de menos de 3 caracteres', () => { + const plan = planDePrueba() + expect(() => agregarNotaAPlan(plan, ' ok ')).toThrow(/al menos 3/) + expect(() => agregarNotaAPlan(plan, '')).toThrow(/al menos 3/) + }) +}) diff --git a/apps/cobranza/src/lib/cobranza.ts b/apps/cobranza/src/lib/cobranza.ts new file mode 100644 index 0000000..b2d7671 --- /dev/null +++ b/apps/cobranza/src/lib/cobranza.ts @@ -0,0 +1,267 @@ +import type { AplicacionPago, NotaPlan, Pago, PlanPago, StatusPago, StatusPlan } from '../types' +import { uuid } from './uuid' + +/** Redondeo monetario a 2 decimales */ +export const round2 = (x: number): number => Math.round(x * 100) / 100 + +/** Tolerancia para comparaciones de dinero (centavos) */ +export const EPS = 0.01 + +const gte = (a: number, b: number): boolean => a >= b - EPS + +/** Parsea 'YYYY-MM-DD' como fecha LOCAL (nunca `new Date(iso)` a secas, que interpreta UTC) */ +export function parseFechaLocal(iso: string): Date { + return new Date(`${iso}T00:00:00`) +} + +function aISO(fecha: Date): string { + const y = fecha.getFullYear() + const m = String(fecha.getMonth() + 1).padStart(2, '0') + const d = String(fecha.getDate()).padStart(2, '0') + return `${y}-${m}-${d}` +} + +/** Último día del mes (mes 0-based) */ +function ultimoDiaDelMes(anio: number, mes: number): number { + return new Date(anio, mes + 1, 0).getDate() +} + +/** + * Suma `meses` conservando el día del mes; si el mes destino no tiene ese día, + * usa su último día (p. ej. 31 de enero + 1 mes → 28/29 de febrero). + */ +export function sumarMeses(fecha: Date, meses: number): Date { + const totalMeses = fecha.getMonth() + meses + const anio = fecha.getFullYear() + Math.floor(totalMeses / 12) + const mes = ((totalMeses % 12) + 12) % 12 + const dia = Math.min(fecha.getDate(), ultimoDiaDelMes(anio, mes)) + return new Date(anio, mes, dia) +} + +/** Compara solo la parte de fecha (ignora horas) */ +function compararFechas(a: Date, b: Date): number { + const da = new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime() + const db = new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime() + return da - db +} + +export interface CalendarioGenerado { + mensualidad: number + pagos: Pago[] +} + +/** + * Genera N pagos con vencimiento mensual a partir de `fechaInicio`. + * mensualidad = round2(engancheTotal / numPagos); el ÚLTIMO pago absorbe + * el residuo del redondeo para que la suma sea exactamente `engancheTotal`. + */ +export function generarCalendario( + fechaInicio: string, + engancheTotal: number, + numPagos: number, +): CalendarioGenerado { + if (numPagos < 1) throw new Error('numPagos debe ser al menos 1') + if (engancheTotal <= 0) throw new Error('engancheTotal debe ser mayor a 0') + const inicio = parseFechaLocal(fechaInicio) + const mensualidad = round2(engancheTotal / numPagos) + const pagos: Pago[] = [] + for (let i = 0; i < numPagos; i++) { + const monto = + i < numPagos - 1 ? mensualidad : round2(engancheTotal - mensualidad * (numPagos - 1)) + pagos.push({ + id: uuid(), + numPago: i + 1, + fechaVencimiento: aISO(sumarMeses(inicio, i)), + monto, + montoPagado: 0, + aplicaciones: [], + }) + } + return { mensualidad, pagos } +} + +/** + * Estado de un pago a la fecha `hoy`. + * PAGADO si cubrió el monto; PARCIAL si tiene abonos sin cubrir; sin abonos: + * VENCIDO si hoy > fechaVencimiento, si no PENDIENTE. + * Decisión de negocio: un pago PARCIAL cuya fecha ya venció sigue siendo + * PARCIAL (tiene abono aplicado); el atraso se señala vía `diasAtraso`. + */ +export function statusPago(pago: Pago, hoy: Date = new Date()): StatusPago { + if (gte(pago.montoPagado, pago.monto)) return 'PAGADO' + if (pago.montoPagado > EPS) return 'PARCIAL' + return compararFechas(hoy, parseFechaLocal(pago.fechaVencimiento)) > 0 ? 'VENCIDO' : 'PENDIENTE' +} + +/** Saldo pendiente del plan: suma(monto - montoPagado) */ +export function saldoPlan(plan: PlanPago): number { + return round2(plan.pagos.reduce((acc, p) => acc + (p.monto - p.montoPagado), 0)) +} + +/** Total cobrado (suma de lo aplicado a todos los pagos) */ +export function pagadoTotal(plan: PlanPago): number { + return round2(plan.pagos.reduce((acc, p) => acc + p.montoPagado, 0)) +} + +/** Fracción de avance 0..1 (pagado / engancheTotal) */ +export function avancePlan(plan: PlanPago): number { + return plan.engancheTotal > 0 ? pagadoTotal(plan) / plan.engancheTotal : 0 +} + +const restante = (p: Pago): number => round2(p.monto - p.montoPagado) +const estaCubierto = (p: Pago): boolean => gte(p.montoPagado, p.monto) + +/** + * Primer pago no cubierto con fecha >= hoy; si no hay, el vencido más antiguo. + * Devuelve null si el plan está cubierto por completo. + */ +export function proximoVencimiento(plan: PlanPago, hoy: Date = new Date()): Pago | null { + const noCubiertos = plan.pagos.filter((p) => !estaCubierto(p)) + const futuro = noCubiertos.find((p) => compararFechas(parseFechaLocal(p.fechaVencimiento), hoy) >= 0) + if (futuro) return futuro + const vencidos = noCubiertos.sort( + (a, b) => parseFechaLocal(a.fechaVencimiento).getTime() - parseFechaLocal(b.fechaVencimiento).getTime(), + ) + return vencidos[0] ?? null +} + +/** Días naturales hasta el próximo vencimiento; null si el plan está cubierto */ +export function diasParaProximoVencimiento(plan: PlanPago, hoy: Date = new Date()): number | null { + const prox = proximoVencimiento(plan, hoy) + if (!prox) return null + return Math.round(compararFechas(parseFechaLocal(prox.fechaVencimiento), hoy) / 86400000) +} + +/** Días de atraso del pago vencido más antiguo (0 si no hay vencidos sin abono) */ +export function diasAtraso(plan: PlanPago, hoy: Date = new Date()): number { + const vencidos = plan.pagos + .filter((p) => statusPago(p, hoy) === 'VENCIDO') + .sort( + (a, b) => + parseFechaLocal(a.fechaVencimiento).getTime() - parseFechaLocal(b.fechaVencimiento).getTime(), + ) + const primero = vencidos[0] + if (!primero) return 0 + return Math.round(compararFechas(hoy, parseFechaLocal(primero.fechaVencimiento)) / 86400000) +} + +/** Status derivado: CANCELADO solo manual; LIQUIDADO si saldo == 0; si no, ACTIVO */ +export function statusPlan(plan: PlanPago): StatusPlan { + if (plan.status === 'CANCELADO') return 'CANCELADO' + return saldoPlan(plan) <= EPS ? 'LIQUIDADO' : 'ACTIVO' +} + +/** Al corriente = sin pagos VENCIDOS */ +export function alCorriente(plan: PlanPago, hoy: Date = new Date()): boolean { + return !plan.pagos.some((p) => statusPago(p, hoy) === 'VENCIDO') +} + +/** + * Mora simple de un pago: restante × (tasa% / 100) / 30 × días vencidos. + * Solo aplica a pagos vencidos con saldo; un PARCIAL vencido también genera + * mora sobre su restante. Tasa 0 (o negativa) = sin mora. + */ +export function moraDePago(pago: Pago, tasaMoraMensual: number, hoy: Date = new Date()): number { + if (tasaMoraMensual <= 0) return 0 + const falta = restante(pago) + if (falta <= EPS) return 0 + const diasVencido = Math.round(compararFechas(hoy, parseFechaLocal(pago.fechaVencimiento)) / 86400000) + if (diasVencido <= 0) return 0 + return round2((falta * (tasaMoraMensual / 100) / 30) * diasVencido) +} + +/** Mora acumulada del plan a la fecha `hoy` (usa plan.tasaMoraMensual ?? 0) */ +export function totalMora(plan: PlanPago, hoy: Date = new Date()): number { + const tasa = plan.tasaMoraMensual ?? 0 + return round2(plan.pagos.reduce((acc, p) => acc + moraDePago(p, tasa, hoy), 0)) +} + +export interface CobranzaItem { + plan: PlanPago + pago: Pago + /** Días de atraso (0 si aún no vence) */ + atraso: number +} + +/** + * Lista plana de "cobranza del día" a través de todos los planes ACTIVOS: + * pagos vencidos (incluye PARCIAL vencido) más los que vencen en los próximos + * `diasAnticipacion` días. Orden de urgencia: mayor atraso primero; a igual + * atraso, el vencimiento más próximo primero. + */ +export function cobranzaDelDia( + planes: PlanPago[], + hoy: Date = new Date(), + diasAnticipacion = 7, +): CobranzaItem[] { + const limite = new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate() + diasAnticipacion) + const items: CobranzaItem[] = [] + for (const plan of planes) { + if (statusPlan(plan) !== 'ACTIVO') continue + for (const pago of plan.pagos) { + if (estaCubierto(pago)) continue + if (compararFechas(parseFechaLocal(pago.fechaVencimiento), limite) > 0) continue + const atraso = Math.max( + 0, + Math.round(compararFechas(hoy, parseFechaLocal(pago.fechaVencimiento)) / 86400000), + ) + items.push({ plan, pago, atraso }) + } + } + return items.sort((a, b) => { + if (a.atraso !== b.atraso) return b.atraso - a.atraso + return compararFechas( + parseFechaLocal(a.pago.fechaVencimiento), + parseFechaLocal(b.pago.fechaVencimiento), + ) + }) +} + +/** + * Agrega una nota a la bitácora del plan (copia inmutable). + * El texto es requerido (mín. 3 caracteres). + */ +export function agregarNotaAPlan(plan: PlanPago, texto: string): PlanPago { + const limpio = texto.trim() + if (limpio.length < 3) { + throw new Error('La nota debe tener al menos 3 caracteres') + } + const nota: NotaPlan = { id: uuid(), fecha: new Date().toISOString(), texto: limpio } + return { ...plan, notas: [...(plan.notas ?? []), nota] } +} + +export interface AbonoInput { + pagoId: string + fechaPago: string // ISO + monto: number + referencia: string +} + +/** + * Aplica un abono al pago indicado y devuelve una copia del plan actualizada. + * Rechaza montos <= 0 y abonos que excedan el restante del pago. + */ +export function registrarAbono(plan: PlanPago, abono: AbonoInput): PlanPago { + if (round2(abono.monto) <= 0) { + throw new Error('El abono debe ser mayor a 0') + } + const pago = plan.pagos.find((p) => p.id === abono.pagoId) + if (!pago) throw new Error('Pago no encontrado en el plan') + const falta = restante(pago) + // La validación es estricta al centavo: un abono nunca puede exceder el restante + if (round2(abono.monto - falta) > 0) { + throw new Error(`El abono excede el restante del pago (${falta.toFixed(2)})`) + } + const aplicacion: AplicacionPago = { + id: uuid(), + fechaPago: abono.fechaPago, + monto: round2(abono.monto), + referencia: abono.referencia, + } + const pagos = plan.pagos.map((p) => + p.id === abono.pagoId + ? { ...p, montoPagado: round2(p.montoPagado + abono.monto), aplicaciones: [...p.aplicaciones, aplicacion] } + : p, + ) + return { ...plan, pagos } +} diff --git a/apps/cobranza/src/lib/formatters.ts b/apps/cobranza/src/lib/formatters.ts new file mode 100644 index 0000000..07c725a --- /dev/null +++ b/apps/cobranza/src/lib/formatters.ts @@ -0,0 +1,36 @@ +const formatoMXN = new Intl.NumberFormat('es-MX', { + style: 'currency', + currency: 'MXN', +}) + +/** Formato monetario MXN, p. ej. $4,579,000.00 */ +export function formatoMoneda(valor: number): string { + return formatoMXN.format(valor) +} + +/** Fracción (0.75) a texto porcentaje (75%) */ +export function formatoPorcentaje(fraccion: number): string { + return `${(fraccion * 100).toLocaleString('es-MX', { maximumFractionDigits: 2 })}%` +} + +/** Fecha en formato largo es-MX: "20 de agosto de 2026" */ +export function formatoFechaLarga(fecha: Date): string { + return fecha.toLocaleDateString('es-MX', { day: 'numeric', month: 'long', year: 'numeric' }) +} + +/** ISO 'YYYY-MM-DD' a fecha legible corta: "08/10/2027" */ +export function formatoFechaCorta(iso: string): string { + return new Date(`${iso}T00:00:00`).toLocaleDateString('es-MX', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }) +} + +/** Fecha ISO del día (para nombre de archivo): 'YYYY-MM-DD' */ +export function fechaISOHoy(fecha: Date = new Date()): string { + const y = fecha.getFullYear() + const m = String(fecha.getMonth() + 1).padStart(2, '0') + const d = String(fecha.getDate()).padStart(2, '0') + return `${y}-${m}-${d}` +} diff --git a/apps/cobranza/src/lib/pdf/estadoCuenta.test.ts b/apps/cobranza/src/lib/pdf/estadoCuenta.test.ts new file mode 100644 index 0000000..6c4532b --- /dev/null +++ b/apps/cobranza/src/lib/pdf/estadoCuenta.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import type { PlanPago } from '../../types' +import { generarCalendario, registrarAbono } from '../cobranza' +import { crearDocumento } from './estadoCuenta' + +function planFixture(): PlanPago { + const { mensualidad, pagos } = generarCalendario('2026-01-15', 100000, 3) + let plan: PlanPago = { + id: 'plan-fixture', + ventaRef: 'VTA-2026-001', + clienteNombre: 'Cliente Fixture', + unidadCodigo: 'U-101', + proyecto: 'SENDA', + fechaInicio: '2026-01-15', + engancheTotal: 100000, + numPagos: 3, + mensualidad, + status: 'ACTIVO', + pagos, + notas: [], + createdAt: '2026-01-01T00:00:00.000Z', + } + plan = registrarAbono(plan, { + pagoId: plan.pagos[0].id, + fechaPago: '2026-01-15', + monto: plan.pagos[0].monto, + referencia: 'SPEI-001', + }) + return plan +} + +describe('estadoCuenta PDF', () => { + it('genera el documento con nombre de archivo correcto', () => { + const hoy = new Date('2026-02-20T12:00:00') + const { doc, nombreArchivo } = crearDocumento(planFixture(), hoy) + expect(nombreArchivo).toBe('estado_cuenta_U-101_2026-02-20.pdf') + expect(doc.getNumberOfPages()).toBeGreaterThanOrEqual(1) + // smoke: el contenido interno es un PDF válido + expect(doc.output('datauristring')).toContain('application/pdf') + }) + + it('smoke: genera el documento con fila de mora cuando hay tasa y pagos vencidos', () => { + const hoy = new Date('2026-04-20T12:00:00') // pagos 2 y 3 vencidos + const plan: PlanPago = { ...planFixture(), tasaMoraMensual: 2 } + const { doc } = crearDocumento(plan, hoy) + expect(doc.getNumberOfPages()).toBeGreaterThanOrEqual(1) + expect(doc.output('datauristring')).toContain('application/pdf') + }) +}) diff --git a/apps/cobranza/src/lib/pdf/estadoCuenta.ts b/apps/cobranza/src/lib/pdf/estadoCuenta.ts new file mode 100644 index 0000000..497044e --- /dev/null +++ b/apps/cobranza/src/lib/pdf/estadoCuenta.ts @@ -0,0 +1,112 @@ +import { jsPDF } from 'jspdf' +import autoTable from 'jspdf-autotable' +import type { PlanPago } from '../../types' +import { avancePlan, pagadoTotal, round2, saldoPlan, statusPago, totalMora } from '../cobranza' +import { fechaISOHoy, formatoFechaCorta, formatoFechaLarga, formatoMoneda, formatoPorcentaje } from '../formatters' + +const MARGEN = 14 + +// jspdf-autotable expone finalY en doc.lastAutoTable en runtime; sus tipos no lo declaran +type DocConTabla = jsPDF & { lastAutoTable: { finalY: number } } +const finalY = (doc: jsPDF): number => (doc as DocConTabla).lastAutoTable.finalY + +export interface DocumentoPdf { + doc: jsPDF + nombreArchivo: string +} + +/** Construye el estado de cuenta del plan de enganche (sin descargarlo) */ +export function crearDocumento(plan: PlanPago, hoy: Date = new Date()): DocumentoPdf { + const doc = new jsPDF() + let y = MARGEN + + // 1. Encabezado + doc.setFontSize(14) + doc.setFont('helvetica', 'bold') + doc.text('FRASA DESARROLLOS', 105, y, { align: 'center' }) + y += 7 + doc.setFontSize(11) + doc.text('ESTADO DE CUENTA — PLAN DE ENGANCHE', 105, y, { align: 'center' }) + y += 6 + doc.setFontSize(9) + doc.setFont('helvetica', 'normal') + doc.text(`Fecha de emisión: ${formatoFechaLarga(hoy)}`, 105, y, { align: 'center' }) + y += 8 + + // 2. Datos del plan + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 8 }, + head: [['CLIENTE', 'UNIDAD', 'PROYECTO', 'VENTA', 'ENGANCHE', 'MENSUALIDAD', 'PAGOS']], + body: [[ + plan.clienteNombre, + plan.unidadCodigo, + plan.proyecto, + plan.ventaRef || '—', + formatoMoneda(plan.engancheTotal), + formatoMoneda(plan.mensualidad), + String(plan.numPagos), + ]], + headStyles: { fillColor: [4, 120, 87] }, + }) + y = finalY(doc) + 6 + + // 3. Tabla de pagos + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 8 }, + head: [['#', 'VENCIMIENTO', 'MONTO', 'PAGADO', 'RESTANTE', 'STATUS', 'REFERENCIAS']], + body: plan.pagos.map((p) => [ + String(p.numPago), + formatoFechaCorta(p.fechaVencimiento), + formatoMoneda(p.monto), + formatoMoneda(p.montoPagado), + formatoMoneda(round2(p.monto - p.montoPagado)), + statusPago(p, hoy), + p.aplicaciones + .map((a) => `${a.referencia} (${formatoFechaCorta(a.fechaPago)})`) + .join('\n') || '—', + ]), + headStyles: { fillColor: [4, 120, 87] }, + }) + y = finalY(doc) + 6 + + // 4. Totales + const pagado = pagadoTotal(plan) + const saldo = saldoPlan(plan) + const mora = totalMora(plan, hoy) + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 9 }, + body: [ + ['TOTAL PAGADO', formatoMoneda(pagado)], + ['SALDO PENDIENTE', formatoMoneda(saldo)], + ...(mora > 0 + ? [ + ['MORA ACUMULADA', formatoMoneda(mora)], + ['TOTAL A PAGAR', formatoMoneda(round2(saldo + mora))], + ] + : []), + ['% AVANCE', formatoPorcentaje(avancePlan(plan))], + ], + theme: 'plain', + columnStyles: { 0: { fontStyle: 'bold', cellWidth: 60 }, 1: { halign: 'left' } }, + }) + y = finalY(doc) + 10 + + // 5. Pie + doc.setFontSize(8) + doc.setFont('helvetica', 'normal') + doc.text('Documento informativo. Para aclaraciones contacta a tu asesor de ventas.', MARGEN, y) + + return { doc, nombreArchivo: `estado_cuenta_${plan.unidadCodigo}_${fechaISOHoy(hoy)}.pdf` } +} + +/** Genera y descarga el estado de cuenta en PDF */ +export function descargar(plan: PlanPago): void { + const { doc, nombreArchivo } = crearDocumento(plan) + doc.save(nombreArchivo) +} diff --git a/apps/cobranza/src/lib/pdf/reciboAbono.test.ts b/apps/cobranza/src/lib/pdf/reciboAbono.test.ts new file mode 100644 index 0000000..2403d19 --- /dev/null +++ b/apps/cobranza/src/lib/pdf/reciboAbono.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import type { PlanPago } from '../../types' +import { generarCalendario, registrarAbono } from '../cobranza' +import { crearDocumento, folioRecibo } from './reciboAbono' + +function planConAbono(): PlanPago { + const { mensualidad, pagos } = generarCalendario('2026-01-15', 100000, 3) + const plan: PlanPago = { + id: 'plan-fixture', + ventaRef: 'VTA-2026-001', + clienteNombre: 'Cliente Fixture', + clienteTelefono: '55 0000 0000', + unidadCodigo: 'U-101', + proyecto: 'SENDA', + fechaInicio: '2026-01-15', + engancheTotal: 100000, + numPagos: 3, + mensualidad, + status: 'ACTIVO', + pagos, + notas: [], + createdAt: '2026-01-01T00:00:00.000Z', + } + return registrarAbono(plan, { + pagoId: plan.pagos[0].id, + fechaPago: '2026-01-15', + monto: 5000, + referencia: 'SPEI-001', + }) +} + +describe('reciboAbono PDF', () => { + it('el folio son los primeros 8 chars del id de la aplicación en mayúsculas', () => { + const plan = planConAbono() + const aplicacion = plan.pagos[0].aplicaciones[0] + expect(folioRecibo(aplicacion)).toBe(aplicacion.id.slice(0, 8).toUpperCase()) + expect(folioRecibo({ ...aplicacion, id: 'abcdef12-3456-7890' })).toBe('ABCDEF12') + }) + + it('genera el documento con nombre de archivo correcto', () => { + const plan = planConAbono() + const pago = plan.pagos[0] + const aplicacion = pago.aplicaciones[0] + const { doc, nombreArchivo } = crearDocumento(plan, pago, aplicacion, new Date('2026-02-20T12:00:00')) + expect(nombreArchivo).toBe(`recibo_U-101_${folioRecibo(aplicacion)}.pdf`) + expect(doc.getNumberOfPages()).toBeGreaterThanOrEqual(1) + // smoke: el contenido interno es un PDF válido + expect(doc.output('datauristring')).toContain('application/pdf') + }) +}) diff --git a/apps/cobranza/src/lib/pdf/reciboAbono.ts b/apps/cobranza/src/lib/pdf/reciboAbono.ts new file mode 100644 index 0000000..1e20e48 --- /dev/null +++ b/apps/cobranza/src/lib/pdf/reciboAbono.ts @@ -0,0 +1,106 @@ +import { jsPDF } from 'jspdf' +import autoTable from 'jspdf-autotable' +import type { AplicacionPago, Pago, PlanPago } from '../../types' +import { round2 } from '../cobranza' +import { formatoFechaCorta, formatoFechaLarga, formatoMoneda } from '../formatters' +import type { DocumentoPdf } from './estadoCuenta' + +const MARGEN = 14 + +/** Folio corto del recibo: primeros 8 caracteres del id de la aplicación, en mayúsculas */ +export function folioRecibo(aplicacion: AplicacionPago): string { + return aplicacion.id.slice(0, 8).toUpperCase() +} + +/** + * Construye el recibo de pago de una aplicación (abono) ya registrada. + * El restante del pago y el saldo del plan se calculan acumulando las + * aplicaciones registradas hasta esa aplicación (inclusive), de modo que el + * recibo refleja los montos vigentes justo tras el abono. + */ +export function crearDocumento( + plan: PlanPago, + pago: Pago, + aplicacion: AplicacionPago, + hoy: Date = new Date(), +): DocumentoPdf { + const folio = folioRecibo(aplicacion) + const doc = new jsPDF() + let y = MARGEN + + // Aplicado acumulado (plan y pago) hasta la aplicación indicada, en orden de registro + let aplicadoPlan = 0 + let aplicadoPago = 0 + let encontrada = false + for (const p of plan.pagos) { + for (const a of p.aplicaciones) { + aplicadoPlan += a.monto + if (p.id === pago.id) aplicadoPago += a.monto + if (a.id === aplicacion.id) { + encontrada = true + break + } + } + if (encontrada) break + } + const saldoTrasAbono = round2(plan.engancheTotal - aplicadoPlan) + const restanteTrasAbono = round2(pago.monto - aplicadoPago) + + // 1. Encabezado + doc.setFontSize(14) + doc.setFont('helvetica', 'bold') + doc.text('FRASA DESARROLLOS', 105, y, { align: 'center' }) + y += 7 + doc.setFontSize(11) + doc.text('RECIBO DE PAGO', 105, y, { align: 'center' }) + y += 6 + doc.setFontSize(9) + doc.setFont('helvetica', 'normal') + doc.text(`Folio: ${folio} · Fecha de emisión: ${formatoFechaLarga(hoy)}`, 105, y, { align: 'center' }) + y += 8 + + // 2. Datos del recibo + autoTable(doc, { + startY: y, + margin: { left: MARGEN, right: MARGEN }, + styles: { fontSize: 9 }, + body: [ + ['CLIENTE', plan.clienteNombre], + ['UNIDAD', plan.unidadCodigo], + ['PROYECTO', plan.proyecto], + ['PAGO', `#${pago.numPago} (vencimiento ${formatoFechaCorta(pago.fechaVencimiento)})`], + ['FECHA DE PAGO', formatoFechaCorta(aplicacion.fechaPago)], + ['MONTO DEL ABONO', formatoMoneda(aplicacion.monto)], + ['REFERENCIA', aplicacion.referencia || '—'], + ['RESTANTE DEL PAGO', formatoMoneda(restanteTrasAbono)], + ['SALDO DEL PLAN TRAS EL ABONO', formatoMoneda(saldoTrasAbono)], + ], + theme: 'plain', + columnStyles: { 0: { fontStyle: 'bold', cellWidth: 70 }, 1: { halign: 'left' } }, + }) + y = (doc as jsPDF & { lastAutoTable: { finalY: number } }).lastAutoTable.finalY + 10 + + // 3. Leyenda informativa + doc.setFontSize(8) + doc.text( + 'Recibo informativo del abono aplicado al plan de enganche. No sustituye la factura ni el comprobante fiscal correspondiente.', + MARGEN, + y, + { maxWidth: 180 }, + ) + y += 24 + + // 4. Línea de firma + doc.setFontSize(9) + doc.text('_______________________________', 105, y, { align: 'center' }) + y += 5 + doc.text(`Recibió — ${plan.clienteNombre}`, 105, y, { align: 'center' }) + + return { doc, nombreArchivo: `recibo_${plan.unidadCodigo}_${folio}.pdf` } +} + +/** Genera y descarga el recibo de pago en PDF */ +export function descargarRecibo(plan: PlanPago, pago: Pago, aplicacion: AplicacionPago): void { + const { doc, nombreArchivo } = crearDocumento(plan, pago, aplicacion) + doc.save(nombreArchivo) +} diff --git a/apps/cobranza/src/lib/uuid.ts b/apps/cobranza/src/lib/uuid.ts new file mode 100644 index 0000000..ce14dad --- /dev/null +++ b/apps/cobranza/src/lib/uuid.ts @@ -0,0 +1,14 @@ +/** + * Genera un UUID v4. crypto.randomUUID solo existe en contextos seguros + * (HTTPS o localhost); al servir por IP en HTTP hay que usar el fallback. + */ +export function uuid(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} diff --git a/apps/cobranza/src/main.tsx b/apps/cobranza/src/main.tsx new file mode 100644 index 0000000..5e038e5 --- /dev/null +++ b/apps/cobranza/src/main.tsx @@ -0,0 +1,29 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import '@fontsource/inter/300.css' +import '@fontsource/inter/400.css' +import '@fontsource/inter/500.css' +import '@fontsource/inter/700.css' +import './index.css' +import Layout from './components/Layout' +import DashboardPage from './pages/DashboardPage' +import HoyPage from './pages/HoyPage' +import PlanDetallePage from './pages/PlanDetallePage' +import NuevoPlanPage from './pages/NuevoPlanPage' + +createRoot(document.getElementById('root')!).render( + + + + + } /> + } /> + } /> + } /> + } /> + + + + , +) diff --git a/apps/cobranza/src/pages/DashboardPage.tsx b/apps/cobranza/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..2292f0f --- /dev/null +++ b/apps/cobranza/src/pages/DashboardPage.tsx @@ -0,0 +1,359 @@ +import { useMemo, useState } from 'react' +import { Link, useSearchParams } from 'react-router-dom' +import { + Bar, + BarChart, + CartesianGrid, + Cell, + Legend, + Pie, + PieChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' +import type { PlanPago, StatusPlan } from '../types' +import { cargarPlanes } from '../services/repository' +import { + alCorriente, + avancePlan, + diasAtraso, + parseFechaLocal, + proximoVencimiento, + round2, + saldoPlan, + statusPlan, + sumarMeses, +} from '../lib/cobranza' +import { formatoFechaCorta, formatoMoneda, formatoPorcentaje } from '../lib/formatters' +import KpiCard from '../components/KpiCard' +import SemaforoDot, { semaforoPlan } from '../components/Semaforo' + +const MESES_CORTOS = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'] +const COLORES_PIE = ['#1B4332', '#d4d4d4'] + +type ColumnaOrden = 'saldo' | 'vencimiento' | 'atraso' | 'avance' +const COLUMNAS_ORDEN: ColumnaOrden[] = ['saldo', 'vencimiento', 'atraso', 'avance'] + +function esColumnaOrden(valor: string | null): valor is ColumnaOrden { + return valor !== null && (COLUMNAS_ORDEN as string[]).includes(valor) +} + +function claveMes(fecha: Date): string { + return `${fecha.getFullYear()}-${String(fecha.getMonth() + 1).padStart(2, '0')}` +} + +export default function DashboardPage() { + const [planes] = useState(() => cargarPlanes()) + // Filtros y ordenamiento persistidos en la URL para que sean compartibles + const [searchParams, setSearchParams] = useSearchParams() + const busqueda = searchParams.get('q') ?? '' + const filtroStatus = (searchParams.get('status') ?? 'TODOS') as StatusPlan | 'TODOS' + const filtroProyecto = searchParams.get('proyecto') ?? 'TODOS' + const paramOrden = searchParams.get('sort') + const orden: ColumnaOrden | null = esColumnaOrden(paramOrden) ? paramOrden : null + const dir: 'asc' | 'desc' = searchParams.get('dir') === 'desc' ? 'desc' : 'asc' + const hoy = useMemo(() => new Date(), []) + + const actualizarParams = (cambios: Record) => { + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev) + for (const [clave, valor] of Object.entries(cambios)) { + if (valor === null || valor === '') next.delete(clave) + else next.set(clave, valor) + } + return next + }, + { replace: true }, + ) + } + + const alternarOrden = (columna: ColumnaOrden) => { + // Click sobre la columna activa alterna asc/desc; sobre otra, empieza en asc + if (orden === columna) { + actualizarParams({ sort: columna, dir: dir === 'asc' ? 'desc' : 'asc' }) + } else { + actualizarParams({ sort: columna, dir: 'asc' }) + } + } + + const proyectos = useMemo( + () => [...new Set(planes.map((p) => p.proyecto))].sort((a, b) => a.localeCompare(b)), + [planes], + ) + + const derivados = useMemo( + () => + planes.map((plan) => ({ + plan, + status: statusPlan(plan), + saldo: saldoPlan(plan), + avance: avancePlan(plan), + corriente: alCorriente(plan, hoy), + proximo: proximoVencimiento(plan, hoy), + atraso: diasAtraso(plan, hoy), + semaforo: semaforoPlan(plan, hoy), + })), + [planes, hoy], + ) + + const activos = derivados.filter((d) => d.status === 'ACTIVO') + const alCorrienteCount = activos.filter((d) => d.corriente).length + const conAtraso = activos.length - alCorrienteCount + const saldoTotal = round2( + derivados.filter((d) => d.status !== 'CANCELADO').reduce((acc, d) => acc + d.saldo, 0), + ) + + const mesActual = claveMes(hoy) + const cobradoEsteMes = round2( + planes + .flatMap((p) => p.pagos.flatMap((pg) => pg.aplicaciones)) + .filter((a) => a.fechaPago.startsWith(mesActual)) + .reduce((acc, a) => acc + a.monto, 0), + ) + + const datosPie = [ + { name: 'Al corriente', value: alCorrienteCount }, + { name: 'Con atraso', value: conAtraso }, + ] + + const datosBarras = useMemo(() => { + const aplicaciones = planes.flatMap((p) => p.pagos.flatMap((pg) => pg.aplicaciones)) + const meses: { clave: string; etiqueta: string }[] = [] + for (let i = 5; i >= 0; i--) { + const f = sumarMeses(new Date(hoy.getFullYear(), hoy.getMonth(), 1), -i) + meses.push({ clave: claveMes(f), etiqueta: MESES_CORTOS[f.getMonth()] }) + } + return meses.map(({ clave, etiqueta }) => ({ + mes: etiqueta, + cobrado: round2( + aplicaciones.filter((a) => a.fechaPago.startsWith(clave)).reduce((acc, a) => acc + a.monto, 0), + ), + })) + }, [planes, hoy]) + + const filtrados = derivados.filter((d) => { + if (filtroStatus !== 'TODOS' && d.status !== filtroStatus) return false + if (filtroProyecto !== 'TODOS' && d.plan.proyecto !== filtroProyecto) return false + const texto = busqueda.trim().toLowerCase() + if (!texto) return true + return ( + d.plan.clienteNombre.toLowerCase().includes(texto) || + d.plan.unidadCodigo.toLowerCase().includes(texto) + ) + }) + + const valorOrden = (d: (typeof filtrados)[number]): number => { + switch (orden) { + case 'saldo': + return d.saldo + case 'vencimiento': + // Planes cubiertos (sin próximo vencimiento) siempre al final + return d.proximo ? parseFechaLocal(d.proximo.fechaVencimiento).getTime() : Number.MAX_SAFE_INTEGER + case 'atraso': + return d.atraso + case 'avance': + return d.avance + default: + return 0 + } + } + + const ordenados = (() => { + if (!orden) return filtrados + const factor = dir === 'desc' ? -1 : 1 + return [...filtrados].sort((a, b) => (valorOrden(a) - valorOrden(b)) * factor) + })() + + const encabezadoOrdenable = (columna: ColumnaOrden, titulo: string, alineadoDerecha = false) => ( + + + + ) + + return ( +
+
+

Módulo Cobranza — Planes de enganche

+ + Nuevo plan + +
+ + {/* KPIs */} +
+ + + + +
+ + {/* Gráficas */} +
+
+

+ Al corriente vs vencidos +

+ + + + {datosPie.map((_, i) => ( + + ))} + + + + + +
+
+

+ Cobrado por mes (últimos 6 meses) +

+ + + + + `$${(v / 1000).toLocaleString('es-MX')}k`} + /> + formatoMoneda(Number(v))} cursor={{ fill: '#fafafa' }} /> + + + +
+
+ + {/* Filtros */} +
+ + actualizarParams({ q: e.target.value })} + placeholder="Buscar por cliente o unidad…" + className="w-64 border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none" + /> + + + + +
+ + {/* Tabla de planes */} +
+ + + + + + + {encabezadoOrdenable('avance', '% pagado')} + {encabezadoOrdenable('vencimiento', 'Próx. vencimiento')} + {encabezadoOrdenable('saldo', 'Saldo', true)} + {encabezadoOrdenable('atraso', 'Días atraso', true)} + + + + + {ordenados.map((d) => ( + + + + + + + + + + + ))} + {ordenados.length === 0 && ( + + + + )} + +
ClienteUnidadProyectoEstado
+ + {d.plan.clienteNombre} + + {d.plan.unidadCodigo}{d.plan.proyecto} +
+
+
+
+ {formatoPorcentaje(d.avance)} +
+
+ {d.proximo ? formatoFechaCorta(d.proximo.fechaVencimiento) : '—'} + {formatoMoneda(d.saldo)} + {d.atraso > 0 ? {d.atraso} : '0'} + + +
+ No hay planes que coincidan con el filtro. +
+
+
+ ) +} diff --git a/apps/cobranza/src/pages/HoyPage.tsx b/apps/cobranza/src/pages/HoyPage.tsx new file mode 100644 index 0000000..d26c5a6 --- /dev/null +++ b/apps/cobranza/src/pages/HoyPage.tsx @@ -0,0 +1,102 @@ +import { useMemo, useState } from 'react' +import { Link } from 'react-router-dom' +import type { PlanPago } from '../types' +import { cargarPlanes } from '../services/repository' +import { cobranzaDelDia, round2, statusPago } from '../lib/cobranza' +import { formatoFechaCorta, formatoMoneda } from '../lib/formatters' +import KpiCard from '../components/KpiCard' + +/** Vista "Cobranza del día": vencidos + por vencer en 7 días, a través de todos los planes activos */ +export default function HoyPage() { + const [planes] = useState(() => cargarPlanes()) + const hoy = useMemo(() => new Date(), []) + const items = useMemo(() => cobranzaDelDia(planes, hoy), [planes, hoy]) + + const vencidos = items.filter((i) => i.atraso > 0) + const porVencer = items.length - vencidos.length + const totalPorCobrar = round2( + items.reduce((acc, i) => acc + (i.pago.monto - i.pago.montoPagado), 0), + ) + + return ( +
+
+

Cobranza del día

+

+ Pagos vencidos y vencimientos de los próximos 7 días, en todos los planes activos. +

+
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + {items.map(({ plan, pago, atraso }) => ( + + + + + + + + + + + + ))} + {items.length === 0 && ( + + + + )} + +
ClienteTeléfonoUnidad# pagoVencimientoMontoRestanteDías atrasoPlan
{plan.clienteNombre} + {plan.clienteTelefono ? ( + + {plan.clienteTelefono} + + ) : ( + — + )} + {plan.unidadCodigo}{pago.numPago}{formatoFechaCorta(pago.fechaVencimiento)}{formatoMoneda(pago.monto)} + {formatoMoneda(round2(pago.monto - pago.montoPagado))} + + {atraso > 0 ? ( + {atraso} + ) : ( + + {statusPago(pago, hoy) === 'PARCIAL' ? 'Parcial' : 'Por vencer'} + + )} + + + Ver plan + +
+ No hay pagos vencidos ni vencimientos en los próximos 7 días. +
+
+
+ ) +} diff --git a/apps/cobranza/src/pages/NuevoPlanPage.tsx b/apps/cobranza/src/pages/NuevoPlanPage.tsx new file mode 100644 index 0000000..99e85bc --- /dev/null +++ b/apps/cobranza/src/pages/NuevoPlanPage.tsx @@ -0,0 +1,213 @@ +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { useNavigate } from 'react-router-dom' +import { crearPlan } from '../services/repository' +import { generarCalendario } from '../lib/cobranza' +import { fechaISOHoy, formatoFechaCorta, formatoMoneda } from '../lib/formatters' + +const PROYECTOS = ['RESIDENCIAL MONTEBELLO', 'SENDA', 'DIAGONAL'] as const + +const esquema = z.object({ + clienteNombre: z.string().trim().min(3, 'Nombre del cliente requerido (mín. 3 caracteres)'), + clienteTelefono: z.string().trim().optional(), + unidadCodigo: z.string().trim().min(1, 'Código de unidad requerido'), + proyecto: z.enum(PROYECTOS), + ventaRef: z.string().trim(), + fechaInicio: z.string().min(1, 'Fecha de inicio requerida'), + engancheTotal: z + .number({ invalid_type_error: 'Captura un monto válido' }) + .positive('El enganche debe ser mayor a 0'), + numPagos: z + .number({ invalid_type_error: 'Captura un número válido' }) + .int('Debe ser un número entero') + .min(1, 'Mínimo 1 pago') + .max(36, 'Máximo 36 pagos'), + tasaMoraMensual: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : Number(v)), + z + .number({ invalid_type_error: 'Captura una tasa válida' }) + .min(0, 'La tasa no puede ser negativa') + .max(30, 'Tasa máxima 30%') + .optional(), + ), +}) + +type NuevoPlanForm = z.infer + +const claseInput = + 'mt-1 w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none' +const claseError = 'mt-1 text-xs text-red-700' +const claseLabel = 'block text-xs font-bold uppercase tracking-widest text-neutral-500' + +export default function NuevoPlanPage() { + const navigate = useNavigate() + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(esquema), + defaultValues: { + clienteNombre: '', + unidadCodigo: '', + proyecto: 'SENDA', + ventaRef: '', + fechaInicio: fechaISOHoy(), + numPagos: 6, + }, + }) + + const [fechaInicio, engancheTotal, numPagos] = watch(['fechaInicio', 'engancheTotal', 'numPagos']) + + // Vista previa en vivo del calendario; solo si los valores son válidos + let vistaPrevia: ReturnType | null = null + try { + if (fechaInicio && engancheTotal > 0 && Number.isInteger(numPagos) && numPagos >= 1 && numPagos <= 36) { + vistaPrevia = generarCalendario(fechaInicio, engancheTotal, numPagos) + } + } catch { + vistaPrevia = null + } + + const guardar = (datos: NuevoPlanForm) => { + const plan = crearPlan(datos) + void navigate(`/planes/${plan.id}`) + } + + return ( +
+

Nuevo plan de enganche

+ +
{ + void handleSubmit(guardar)(e) + }} + > +
+
+ + + {errors.clienteNombre &&

{errors.clienteNombre.message}

} +
+
+ + +
+
+ + + {errors.unidadCodigo &&

{errors.unidadCodigo.message}

} +
+
+ + +
+
+ + +
+
+ + + {errors.fechaInicio &&

{errors.fechaInicio.message}

} +
+
+ + + {errors.engancheTotal &&

{errors.engancheTotal.message}

} +
+
+ + + {errors.numPagos &&

{errors.numPagos.message}

} +
+
+ + + {errors.tasaMoraMensual &&

{errors.tasaMoraMensual.message}

} +
+
+ + {/* Vista previa del calendario */} + {vistaPrevia && ( +
+

+ Vista previa — {vistaPrevia.pagos.length} pagos de {formatoMoneda(vistaPrevia.mensualidad)} +

+
+ + + + + + + + + + {vistaPrevia.pagos.map((p) => ( + + + + + + ))} + +
#VencimientoMonto
{p.numPago}{formatoFechaCorta(p.fechaVencimiento)}{formatoMoneda(p.monto)}
+
+
+ )} + +
+ + +
+
+
+ ) +} diff --git a/apps/cobranza/src/pages/PlanDetallePage.tsx b/apps/cobranza/src/pages/PlanDetallePage.tsx new file mode 100644 index 0000000..a0b1063 --- /dev/null +++ b/apps/cobranza/src/pages/PlanDetallePage.tsx @@ -0,0 +1,324 @@ +import { Fragment, useMemo, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' +import type { Pago, PlanPago } from '../types' +import { agregarNota, cancelarPlan, obtenerPlan, registrarAbonoEnPlan } from '../services/repository' +import { + avancePlan, + diasAtraso, + moraDePago, + pagadoTotal, + round2, + saldoPlan, + statusPago, + statusPlan, + totalMora, +} from '../lib/cobranza' +import { formatoFechaCorta, formatoMoneda, formatoPorcentaje } from '../lib/formatters' +import { descargar } from '../lib/pdf/estadoCuenta' +import { descargarRecibo } from '../lib/pdf/reciboAbono' +import BadgeStatus from '../components/BadgeStatus' +import KpiCard from '../components/KpiCard' +import ModalAbono from '../components/ModalAbono' + +export default function PlanDetallePage() { + const { id } = useParams<{ id: string }>() + const navigate = useNavigate() + const [plan, setPlan] = useState(() => (id ? obtenerPlan(id) : undefined)) + const [pagoAbono, setPagoAbono] = useState(null) + const [expandidos, setExpandidos] = useState>(new Set()) + const [error, setError] = useState(null) + const [notaTexto, setNotaTexto] = useState('') + const [errorNota, setErrorNota] = useState(null) + const hoy = useMemo(() => new Date(), []) + + if (!plan) { + return ( +
+

Plan no encontrado.

+ + Volver al dashboard + +
+ ) + } + + const status = statusPlan(plan) + const saldo = saldoPlan(plan) + const atraso = diasAtraso(plan, hoy) + const tasaMora = plan.tasaMoraMensual ?? 0 + const mora = totalMora(plan, hoy) + + const toggleExpandido = (pagoId: string) => { + setExpandidos((prev) => { + const next = new Set(prev) + if (next.has(pagoId)) next.delete(pagoId) + else next.add(pagoId) + return next + }) + } + + const guardarAbono = (datos: { monto: number; fechaPago: string; referencia: string }) => { + if (!pagoAbono) return + try { + const actualizado = registrarAbonoEnPlan(plan.id, datos, pagoAbono.id) + setPlan(actualizado) + setPagoAbono(null) + setError(null) + } catch (e) { + setError(e instanceof Error ? e.message : 'Error al registrar el abono') + } + } + + const cancelar = () => { + if (!window.confirm('¿Cancelar este plan? El saldo pendiente dejará de cobrarse.')) return + setPlan(cancelarPlan(plan.id)) + } + + const guardarNota = () => { + try { + setPlan(agregarNota(plan.id, notaTexto)) + setNotaTexto('') + setErrorNota(null) + } catch (e) { + setErrorNota(e instanceof Error ? e.message : 'Error al guardar la nota') + } + } + + return ( +
+
+
+ + ← Volver al dashboard + +

{plan.clienteNombre}

+

+ {plan.unidadCodigo} · {plan.proyecto} · Venta {plan.ventaRef || '—'} ·{' '} + {status} + {atraso > 0 && · {atraso} días de atraso} +

+

+ Tel. {plan.clienteTelefono || '—'} + {tasaMora > 0 && · Mora {tasaMora}% mensual} +

+
+
+ + {status === 'ACTIVO' && saldo > 0 && ( + + )} +
+
+ + {error && ( +
{error}
+ )} + + {/* KPIs del plan */} +
+ + + + + + {mora > 0 && ( + {formatoMoneda(mora)}} + detalle={`Tasa ${tasaMora}% mensual`} + /> + )} +
+ + {/* Tabla de pagos */} +
+ + + + + + + + + {tasaMora > 0 && } + + + + + + {plan.pagos.map((pago) => { + const st = statusPago(pago, hoy) + const restante = round2(pago.monto - pago.montoPagado) + const moraPago = tasaMora > 0 ? moraDePago(pago, tasaMora, hoy) : 0 + const expandido = expandidos.has(pago.id) + return ( + + + + + + + + {tasaMora > 0 && ( + + )} + + + + + {expandido && ( + + + + )} + + ) + })} + +
#VencimientoMontoPagadoRestanteMoraStatusAbonos +
{pago.numPago}{formatoFechaCorta(pago.fechaVencimiento)}{formatoMoneda(pago.monto)}{formatoMoneda(pago.montoPagado)}{formatoMoneda(restante)} + {moraPago > 0 ? ( + {formatoMoneda(moraPago)} + ) : ( + — + )} + + + + {pago.aplicaciones.length > 0 ? ( + + ) : ( + — + )} + + {st !== 'PAGADO' && status === 'ACTIVO' && ( + + )} +
0 ? 9 : 8} className="px-4 py-4"> + + + + + + + + + + + {pago.aplicaciones.map((a) => ( + + + + + + + ))} + +
FechaMontoReferenciaRecibo
{formatoFechaCorta(a.fechaPago)}{formatoMoneda(a.monto)}{a.referencia} + +
+
+
+ + {/* Bitácora del plan */} +
+

+ Bitácora +

+ {(plan.notas ?? []).length === 0 ? ( +

Sin notas registradas.

+ ) : ( +
    + {[...(plan.notas ?? [])] + .sort((a, b) => b.fecha.localeCompare(a.fecha)) + .map((nota) => ( +
  • +

    + {new Date(nota.fecha).toLocaleString('es-MX', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} +

    +

    {nota.texto}

    +
  • + ))} +
+ )} +
{ + e.preventDefault() + guardarNota() + }} + > + +