diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f63b146 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "edge", + "request": "launch", + "name": "localhost (Edge)", + "url": "http://localhost:64087", + "webRoot": "${workspaceFolder}" + }, + { + "type": "chrome", + "request": "launch", + "name": "localhost (Chrome)", + "url": "http://localhost:64087", + "webRoot": "${workspaceFolder}" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fc64a33 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +This file explains how Visual Studio created the project. + +The following tools were used to generate this project: +- create-vite + +The following steps were used to generate this project: +- Create react project with create-vite: `npm init --yes vite@latest ndf -- --template=react-ts`. +- Create project file (`ndf.esproj`). +- Create `launch.json` to enable debugging. +- Add project to solution. +- Write this file. diff --git a/DockerfileNDF.frontend b/DockerfileNDF.frontend new file mode 100644 index 0000000..58629d2 --- /dev/null +++ b/DockerfileNDF.frontend @@ -0,0 +1,40 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY ndf/package.json ndf/package-lock.json ./ +RUN npm ci --legacy-peer-deps + +COPY ndf/ . + +# ✅ Écrase vite.config.ts avec la bonne config +RUN cat > /app/vite.config.ts << 'EOF' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { '@': path.resolve(__dirname, './src') } + }, + server: { + host: '0.0.0.0', + port: 81, + strictPort: true, + allowedHosts: ['myndf.ensup-adm.net', 'localhost'], + proxy: { + '/api': { + target: 'http://backend:3024', + changeOrigin: true, + secure: false, + ws: true + } + } + } +}); +EOF + +EXPOSE 81 + +CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "81"] diff --git a/NDF.sln b/NDF.sln new file mode 100644 index 0000000..0411230 --- /dev/null +++ b/NDF.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36915.13 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "ndf", "ndf.esproj", "{EB0E6B4D-5936-F460-7264-0B16674255E7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EB0E6B4D-5936-F460-7264-0B16674255E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB0E6B4D-5936-F460-7264-0B16674255E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB0E6B4D-5936-F460-7264-0B16674255E7}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {EB0E6B4D-5936-F460-7264-0B16674255E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB0E6B4D-5936-F460-7264-0B16674255E7}.Release|Any CPU.Build.0 = Release|Any CPU + {EB0E6B4D-5936-F460-7264-0B16674255E7}.Release|Any CPU.Deploy.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7B246EF3-A0BC-4D0A-9047-CE6948E7257F} + EndGlobalSection +EndGlobal diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9635b17 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +services: + backend: + image: ouijdaneim/ndf-backend:latest + build: + context: ./ndf/public/Backend + dockerfile: DockerfileNDF.backend + container_name: ndf-backend + environment: + - DB_SERVER=192.168.0.3 + - DB_NAME=NDF + - DB_USER=ndf_app + - DB_PASSWORD=P@ssw0rd2026! + - DB_PORT=1433 + - PORT=3024 + - AZURE_TENANT_ID=9840a2a0-6ae1-4688-b03d-d2ec291be0f9 + - AZURE_CLIENT_ID=51a2c5b3-4cea-4752-93d0-bc59ea33be29 + - AZURE_CLIENT_SECRET=uPD8Q~CJbl26DDrRWRgKXvvqwVVm0DtRhzkEqcyT + - AZURE_GROUP_ID=c1ea877c-6bca-4f47-bfad-f223640813a0 + - JWT_SECRET=un_secret_aleatoire_securise + - OAUTH_REDIRECT_URI=http://localhost:8024/api/auth/callback # ✅ + - FRONTEND_URL=https://myndf.ensup-adm.net # ✅ + - SHAREPOINT_SITE_ID=ensup.sharepoint.com,d94abc08-28eb-47ce-8e12-fbbd6f16b9ea,a052c325-d33a-40e3-9e7b-7896a2ea7ab7 + - SHAREPOINT_DRIVE_ID=b!CLxK2esozkeOEvu9bxa56iXDUqA60-NAnnt4lqLqerfjKRsFHmtxSbj0s5KCssZK + hostname: backend + ports: + - "8024:3024" + networks: + - ndf-network + restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" + + frontend: + image: ouijdaneim/ndf-frontend:latest + build: + context: . + dockerfile: DockerfileNDF.frontend + container_name: ndf-frontend + hostname: frontend + ports: + - "3025:81" + environment: + - VITE_API_URL=http://backend:3024 + networks: + - ndf-network + depends_on: + - backend + restart: unless-stopped + +networks: + ndf-network: + name: ndf-network + driver: bridge diff --git a/ndf.esproj b/ndf.esproj new file mode 100644 index 0000000..6e48911 --- /dev/null +++ b/ndf.esproj @@ -0,0 +1,11 @@ + + + npm run dev + src\ + Vitest + + false + + $(MSBuildProjectDirectory)\dist + + \ No newline at end of file diff --git a/ndf/.gitignore b/ndf/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/ndf/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/ndf/README.md b/ndf/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/ndf/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/ndf/eslint.config.js b/ndf/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/ndf/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/ndf/index.html b/ndf/index.html new file mode 100644 index 0000000..56827aa --- /dev/null +++ b/ndf/index.html @@ -0,0 +1,14 @@ + + + + + + + Notes de Frais + + + +
+ + + diff --git a/ndf/package-lock.json b/ndf/package-lock.json new file mode 100644 index 0000000..8640527 --- /dev/null +++ b/ndf/package-lock.json @@ -0,0 +1,4469 @@ +{ + "name": "ndf", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ndf", + "version": "0.0.0", + "dependencies": { + "@azure/msal-node": "^5.0.4", + "axios": "^1.13.5", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "mssql": "^12.2.0", + "pdf-lib": "^1.17.1", + "pdfkit": "^0.17.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-qr-code": "^2.0.20", + "react-router-dom": "^6.26.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.1", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.7.0", + "lucide-react": "^0.577.0", + "tailwindcss": "^4.2.1", + "typescript": "^5.5.3", + "vite": "^5.4.2" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.2.tgz", + "integrity": "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/msal-common": { + "version": "15.14.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.14.2.tgz", + "integrity": "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/msal-node": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.7.tgz", + "integrity": "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.14.2", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.28.2.tgz", + "integrity": "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.14.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": { + "version": "15.14.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.14.2.tgz", + "integrity": "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.0.4.tgz", + "integrity": "sha512-0KZ9/wbUyZN65JLAx5bGNfWjkD0kRMUgM99oSpZFg7wEOb3XcKIiHrFnIpgyc8zZ70fHodyh8JKEOel1oN24Gw==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.0.4.tgz", + "integrity": "sha512-WbA77m68noCw4qV+1tMm5nodll34JCDF0KmrSrp9LskS0bGbgHt98ZRxq69BQK5mjMqDD5ThHJOrrGSfzPybxw==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.0.4", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@js-joda/core": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz", + "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", + "license": "BSD-3-Clause" + }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "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.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tediousjs/connection-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.6.0.tgz", + "integrity": "sha512-GxlsW354Vi6QqbUgdPyQVcQjI7cZBdGV5vOYVYuCVDTylx2wl3WHR2HlhcxxHTrMigbelpXsdcZso+66uxPfow==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.3.tgz", + "integrity": "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.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/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "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/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.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": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, + "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/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "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.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "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/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mssql": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.2.0.tgz", + "integrity": "sha512-lwwLHAqcWOz8okjboQpIEp5OghUFGJhuuQZS3+WF1ZXbaEaCEGKOfiQET3w/5Xz0tyZfDNCQVCm9wp5GwXut6g==", + "license": "MIT", + "dependencies": { + "@tediousjs/connection-string": "^0.6.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "tarn": "^3.0.2", + "tedious": "^19.0.0" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/pdfkit": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", + "integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "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/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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-qr-code": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.20.tgz", + "integrity": "sha512-I7hTe6LBRMU35gQ2Ypsk2LIXZN6iq6Ad9axDj2cuHO+mtbtGIMC1gl2mhwDtPYUX/a7zHrGhsAt6Kifil2ujsA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "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": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tedious": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-19.2.1.tgz", + "integrity": "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.7.2", + "@azure/identity": "^4.2.1", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.6.5", + "@types/node": ">=18", + "bl": "^6.1.4", + "iconv-lite": "^0.7.0", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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", + "peer": true, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + } + } +} diff --git a/ndf/package.json b/ndf/package.json new file mode 100644 index 0000000..212ef49 --- /dev/null +++ b/ndf/package.json @@ -0,0 +1,36 @@ +{ + "name": "ndf", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@azure/msal-node": "^5.0.4", + "axios": "^1.13.5", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "mssql": "^12.2.0", + "pdf-lib": "^1.17.1", + "pdfkit": "^0.17.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-qr-code": "^2.0.20", + "react-router-dom": "^6.26.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.1", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.7.0", + "lucide-react": "^0.577.0", + "tailwindcss": "^4.2.1", + "typescript": "^5.5.3", + "vite": "^5.4.2" + } +} diff --git a/ndf/public/backend/.env b/ndf/public/backend/.env new file mode 100644 index 0000000..327518c --- /dev/null +++ b/ndf/public/backend/.env @@ -0,0 +1,33 @@ +AZURE_CLIENT_ID=51a2c5b3-4cea-4752-93d0-bc59ea33be29 +AZURE_CLIENT_SECRET=uPD8Q~CJbl26DDrRWRgKXvvqwVVm0DtRhzkEqcyT +AZURE_TENANT_ID=9840a2a0-6ae1-4688-b03d-d2ec291be0f9 +JWT_SECRET=un_secret_aleatoire_securise +AZURE_GROUP_ID=c1ea877c-6bca-4f47-bfad-f223640813a0 + + +DB_SERVER=192.168.0.3 +DB_USER=ndf_app +DB_PASSWORD=P@ssw0rd2026! +DB_NAME=NDF +DB_PORT=1433 +RESPONSABLE_PAIEMENT_EMAIL=aagromayor@ensup.eu +MAIL_FROM=ndfnoreply@ensup.eu +OAUTH_REDIRECT_URI=https://myndf.ensup-adm.net/api/auth/callback + +PORT=3024 + + + +SHAREPOINT_SITE_ID=ensup.sharepoint.com,d94abc08-28eb-47ce-8e12-fbbd6f16b9ea,a052c325-d33a-40e3-9e7b-7896a2ea7ab7 +SHAREPOINT_DRIVE_ID=b!CLxK2esozkeOEvu9bxa56iXDUqA60-NAnnt4lqLqerfjKRsFHmtxSbj0s5KCssZK + +IBAN_ENCRYPTION_KEY=a3f8c2d1e4b7096f5a2e1d8c3b4f7a90e2d1c8b5f3a6e9d0c7b4a1f8e5d2c9b6 +IBAN_HASH_SALT=b4c7e2a1f9d3086e5c4a2b8f1e7d3c9a + +COMPANY_NAME=ENSUP GROUP +COMPANY_IBAN=FR76XXXXXXXXXXXXXXXXXXXXXXXXX +COMPANY_BIC=BNPAFRPP +COMPANY_ADDRESS=1 RUE DE LA PAIX +COMPANY_CP=75009 +COMPANY_VILLE=PARIS 09 +COMPANY_PAYS=FR \ No newline at end of file diff --git a/ndf/public/backend/DockerfileNDF.backend b/ndf/public/backend/DockerfileNDF.backend new file mode 100644 index 0000000..9ffedc3 --- /dev/null +++ b/ndf/public/backend/DockerfileNDF.backend @@ -0,0 +1,18 @@ +FROM node:18-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies (including pdfkit) +RUN npm ci --only=production + +# Copy application code +COPY . . + +# Expose port +EXPOSE 3024 + +# Start the server +CMD ["node", "server.js"] \ No newline at end of file diff --git a/ndf/public/backend/msalConfig.ts b/ndf/public/backend/msalConfig.ts new file mode 100644 index 0000000..e07644b --- /dev/null +++ b/ndf/public/backend/msalConfig.ts @@ -0,0 +1,27 @@ +require('dotenv').config(); + +const msalConfig = { + auth: { + clientId: process.env.AZURE_CLIENT_ID, + authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`, + clientSecret: process.env.AZURE_CLIENT_SECRET, + }, + system: { + loggerOptions: { + loggerCallback(loglevel, message, containsPii) { + console.log(message); + }, + piiLoggingEnabled: false, + logLevel: 'Info', + }, + }, +}; + +const REDIRECT_URI = process.env.AZURE_REDIRECT_URI; +const POST_LOGOUT_REDIRECT_URI = process.env.FRONTEND_URL; + +module.exports = { + msalConfig, + REDIRECT_URI, + POST_LOGOUT_REDIRECT_URI, +}; \ No newline at end of file diff --git a/ndf/public/backend/ndfPdfGenerator.js b/ndf/public/backend/ndfPdfGenerator.js new file mode 100644 index 0000000..3eb310c --- /dev/null +++ b/ndf/public/backend/ndfPdfGenerator.js @@ -0,0 +1,485 @@ +// ══════════════════════════════════════════════════════════════════════════════ +// ndfPdfGenerator.js — v4 (pdfkit pur, 0% Python) +// Génère la fiche Note de Frais au format exact du modèle ENSUP +// avec signatures électroniques intégrées. +// v4 : Tarif km et Sous-total km intégrés dans le tableau après colonne Km +// +// Prérequis : pdfkit déjà installé (npm install pdfkit) +// Copier dans le même dossier que server.js. +// ══════════════════════════════════════════════════════════════════════════════ + +import PDFDocument from 'pdfkit'; + +// ───────────────────────────────────────────────────────────────────────────── +// CONSTANTES +// ───────────────────────────────────────────────────────────────────────────── + + +const C = { + blue: '#1B4F8A', header: '#2563EB', + totalBg: '#DBEAFE', altRow: '#EFF6FF', + amountBg: '#EEF2FF', greenBg: '#F0FDF4', + greenText: '#15803D', headerRow: '#E2E8F0', + border: '#CBD5E1', dark: '#0F172A', + grey: '#64748B', light: '#94A3B8', + white: '#FFFFFF', + collabBg: '#F0FDF4', collabBorder: '#A7F3D0', collabText: '#059669', + validBg: '#EEF2FF', validBorder: '#C7D2FE', validText: '#4F46E5', + refusBg: '#FEF2F2', refusBorder: '#FCA5A5', refusText: '#DC2626', + waitBg: '#F8FAFC', waitBorder: '#E2E8F0', + kmBg: '#F5F3FF', // fond violet clair pour colonne tarif km + kmBorder: '#DDD6FE', // bordure violet clair + kmText: '#7C3AED', // texte violet + kmTotalBg: '#EDE9FE', // fond sous-total km +}; + +// Colonnes tableau (largeurs en points) +// ── v4 : 'tarifKm' et 'sousKm' insérées après 'km' ── +const COLS = [ + { key: 'num', label: 'N°pièce', w: 34, align: 'center' }, + { key: 'date', label: 'Date', w: 58, align: 'left' }, + { key: 'nature', label: 'Nature', w: 70, align: 'left' }, + { key: 'lib', label: 'Libellé', w: 140, align: 'left' }, + { key: 'km', label: 'Km', w: 36, align: 'right' }, + { key: 'tarifKm', label: 'Tarif €/km', w: 46, align: 'right' }, + { key: 'sousKm', label: 'S/Total Km', w: 50, align: 'right' }, + { key: 'ttc', label: 'TTC', w: 50, align: 'right' }, + { key: 'tva21', label: 'TVA 2,1%', w: 46, align: 'right' }, + { key: 'tva55', label: 'TVA 5,5%', w: 46, align: 'right' }, + { key: 'tva10', label: 'TVA 10%', w: 46, align: 'right' }, + { key: 'tva20', label: 'TVA 20%', w: 46, align: 'right' }, + { key: 'ht', label: 'HT', w: 50, align: 'right' }, +]; + +// Colonnes km (pour coloration spéciale) +const KM_COLS = ['km', 'tarifKm', 'sousKm']; + +const MARGIN = 30; +const ROW_H = 16; +const HEAD_H = 20; +const PAGE_W = 841.89; // A4 largeur +const PAGE_H = 595.28; // A4 hauteur + +// ───────────────────────────────────────────────────────────────────────────── +// HELPERS +// ───────────────────────────────────────────────────────────────────────────── +const f2 = v => (parseFloat(v) || 0).toFixed(2); +const f3 = v => (parseFloat(v) || 0).toFixed(3); + +function fmtDate(s) { + if (!s) return ''; + try { + const d = new Date(s); + if (isNaN(d)) return String(s).slice(0, 10); + return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`; + } catch { return ''; } +} + +function fmtDateTime(s) { + if (!s) return ''; + try { + const d = new Date(s); + if (isNaN(d)) return String(s).slice(0, 16); + return d.toLocaleString('fr-FR', { + timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit', + year: 'numeric', hour: '2-digit', minute: '2-digit', + }); + } catch { return ''; } +} + +function drawRect(doc, x, y, w, h, fill, stroke = null, lw = 0.3) { + doc.save(); + if (stroke) { + doc.lineWidth(lw).rect(x, y, w, h).fillAndStroke(fill, stroke); + } else { + doc.rect(x, y, w, h).fill(fill); + } + doc.restore(); +} + +function drawCellText(doc, text, x, y, w, h, font, size, color, align, padX = 3) { + text = String(text ?? ''); + const maxW = w - padX * 2; + doc.save().font(font).fontSize(size).fillColor(color); + while (text.length > 1 && doc.widthOfString(text) > maxW) text = text.slice(0, -1); + const ty = y + h * 0.28; + if (align === 'right') { + doc.text(text, x + padX, ty, { width: maxW, align: 'right', lineBreak: false }); + } else if (align === 'center') { + doc.text(text, x + padX, ty, { width: maxW, align: 'center', lineBreak: false }); + } else { + doc.text(text, x + padX, ty, { width: maxW, align: 'left', lineBreak: false }); + } + doc.restore(); +} + +function drawVLine(doc, x, y1, y2) { + doc.save().strokeColor(C.border).lineWidth(0.4) + .moveTo(x, y1).lineTo(x, y2).stroke().restore(); +} + +function drawHLine(doc, x1, x2, y) { + doc.save().strokeColor(C.border).lineWidth(0.3) + .moveTo(x1, y).lineTo(x2, y).stroke().restore(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// preparerLignesPDF — convertit lignes formulaire → lignes PDF +// ───────────────────────────────────────────────────────────────────────────── +export function preparerLignesPDF(lignesParsed, tarifKm = TARIF_KM_DEFAULT) { + return (lignesParsed || []).map((l, idx) => { + const isKm = (l.categorie || '').toLowerCase().includes('kilom'); + const km = parseFloat(l.km) || 0; + const ttc = isKm ? 0 : (parseFloat(l.montant) || 0); + const taux = parseFloat(l.tauxTVA) || 0; + let ht = ttc, tva21 = 0, tva55 = 0, tva10 = 0, tva20 = 0; + + if (!isKm && taux > 0 && ttc > 0) { + ht = parseFloat((ttc / (1 + taux / 100)).toFixed(2)); + const tvaM = parseFloat((ttc - ht).toFixed(2)); + if (Math.abs(taux - 2.1) < 0.01) tva21 = tvaM; + else if (Math.abs(taux - 5.5) < 0.01) tva55 = tvaM; + else if (Math.abs(taux - 10) < 0.01) tva10 = tvaM; + else if (Math.abs(taux - 20) < 0.01) tva20 = tvaM; + } + + const indemniteKm = isKm ? parseFloat((km * tarifKm).toFixed(2)) : 0; + + return { + numPiece: idx + 1, + date: l.date, + nature: l.categorie || '', + libelle: l.libelle || '', + km: isKm ? km : 0, + tarifKmVal: isKm ? tarifKm : 0, // ← valeur numérique tarif + montantTTC: isKm ? 0 : ttc, + tva21, tva55, tva10, tva20, + montantHT: isKm ? 0 : ht, + indemniteKm, + }; + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// generateFicheSignee — point d'entrée appelé depuis server.js +// ───────────────────────────────────────────────────────────────────────────── +export async function generateFicheSignee(note, signatures = []) { + const tarifKm = parseFloat(note.tarifKm) || TARIF_KM_DEFAULT; + + let lignesPDF = []; + if (note.lignesJson) { + try { + const parsed = typeof note.lignesJson === 'string' + ? JSON.parse(note.lignesJson) : note.lignesJson; + lignesPDF = preparerLignesPDF(parsed, tarifKm); + } catch (e) { console.error('ndfPdfGenerator — Parse lignesJson:', e.message); } + } else if (note.lignes && Array.isArray(note.lignes)) { + lignesPDF = preparerLignesPDF(note.lignes, tarifKm); + } else { + const isKm = !!(note.km && parseFloat(note.km) > 0); + const km = isKm ? parseFloat(note.km) : 0; + lignesPDF = [{ + numPiece: 1, date: note.date, nature: note.categorie || '', libelle: note.libelle || '', + km: isKm ? km : 0, + tarifKmVal: isKm ? tarifKm : 0, + montantTTC: isKm ? 0 : parseFloat(note.montant || 0), + tva21: 0, tva55: 0, tva10: 0, tva20: 0, + montantHT: isKm ? 0 : parseFloat(note.montantHT || note.montant || 0), + indemniteKm: isKm ? parseFloat((km * tarifKm).toFixed(2)) : 0, + }]; + } + + let mois = note.mois || ''; + if (!mois && note.date) { + const d = new Date(note.date); + const m = d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); + mois = m.charAt(0).toUpperCase() + m.slice(1); + } + + return _buildPDF({ + reference: note.reference || '', + nomPrenom: note.nomPrenom || note.collaborateur || '', + mois, + departement: note.departement || '', + lignes: lignesPDF, + tarifKm, + signatures, + statut: note.statut || 'enattente', + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// _buildPDF — génère le Buffer PDF +// ───────────────────────────────────────────────────────────────────────────── +function _buildPDF({ reference, nomPrenom, mois, departement, lignes, tarifKm, signatures }) { + return new Promise((resolve, reject) => { + const doc = new PDFDocument({ + size: 'A4', + layout: 'landscape', + margin: 0, + info: { + Title: `Note de Frais ${reference}`, + Author: `ENSUP — ${nomPrenom}`, + Subject: `NDF ${reference}`, + Creator: 'NDF ENSUP v4', + }, + }); + + const chunks = []; + doc.on('data', c => chunks.push(c)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', e => reject(e)); + + // ── Positions X colonnes ───────────────────────────────────── + const colX = {}; + let cx = MARGIN; + for (const col of COLS) { colX[col.key] = cx; cx += col.w; } + const tableW = cx - MARGIN; + + // ── 2. BANDEAU ─────────────────────────────────────────────── + const bandY = 44; + drawRect(doc, MARGIN, bandY, tableW, 18, C.header); + doc.font('Helvetica-Bold').fontSize(11).fillColor(C.white) + .text('NOTE DE FRAIS', MARGIN, bandY + 3.5, { width: tableW, align: 'center', lineBreak: false }); + + // ── 3. INFOS COLLAB ────────────────────────────────────────── + const infoY = bandY + 23; + doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark) + .text(`NOM : ${nomPrenom}`, MARGIN, infoY, { lineBreak: false }); + if (departement) + doc.font('Helvetica').fontSize(8).fillColor(C.grey) + .text(`Service : ${departement}`, MARGIN + 200, infoY, { lineBreak: false }); + doc.font('Helvetica').fontSize(8).fillColor(C.grey) + .text('Repas, déplacement, autres', MARGIN + 380, infoY, { lineBreak: false }); + doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark) + .text(`Mois : ${mois}`, MARGIN, infoY + 13, { lineBreak: false }); + + // ── 4. EN-TÊTE COLONNES ────────────────────────────────────── + const tableTop = infoY + 27; + + // Fond de base + drawRect(doc, MARGIN, tableTop, tableW, HEAD_H, C.headerRow, C.border, 0.5); + + // Fond spécial violet pour les 3 colonnes km dans l'en-tête + for (const key of KM_COLS) { + drawRect(doc, colX[key], tableTop, COLS.find(c => c.key === key).w, HEAD_H, C.kmBg); + } + + for (const col of COLS) { + const isKmCol = KM_COLS.includes(col.key); + drawCellText( + doc, col.label, + colX[col.key], tableTop, col.w, HEAD_H, + 'Helvetica-Bold', isKmCol ? 6.5 : 7, + isKmCol ? C.kmText : C.dark, + col.align + ); + drawVLine(doc, colX[col.key], tableTop, tableTop + HEAD_H); + } + drawVLine(doc, MARGIN + tableW, tableTop, tableTop + HEAD_H); + drawHLine(doc, MARGIN, MARGIN + tableW, tableTop); + drawHLine(doc, MARGIN, MARGIN + tableW, tableTop + HEAD_H); + + // ── 5. LIGNES DONNÉES ──────────────────────────────────────── + const MIN_ROWS = 18; + const totalRows = Math.max(MIN_ROWS, lignes.length); + let y = tableTop + HEAD_H; + let totKm = 0, totTTC = 0, totT21 = 0, totT55 = 0, totT10 = 0, totT20 = 0, totHT = 0, totSousKm = 0; + + for (let i = 0; i < totalRows; i++) { + const lig = lignes[i] || null; + // Fond de ligne alterné + drawRect(doc, MARGIN, y, tableW, ROW_H, i % 2 === 1 ? C.altRow : C.white); + + // Fond violet léger sur les 3 colonnes km (toutes lignes) + for (const key of KM_COLS) { + const col = COLS.find(c => c.key === key); + drawRect(doc, colX[key], y, col.w, ROW_H, + i % 2 === 1 ? '#F3F0FF' : '#FAF8FF'); + } + + if (lig) { + const km = parseFloat(lig.km) || 0; + const tarif = parseFloat(lig.tarifKmVal) || 0; + const sousKm = parseFloat(lig.indemniteKm) || 0; + const ttc = parseFloat(lig.montantTTC) || 0; + const t21 = parseFloat(lig.tva21) || 0; + const t55 = parseFloat(lig.tva55) || 0; + const t10 = parseFloat(lig.tva10) || 0; + const t20 = parseFloat(lig.tva20) || 0; + const ht = parseFloat(lig.montantHT) || 0; + + totKm += km; + totTTC += ttc; + totT21 += t21; totT55 += t55; totT10 += t10; totT20 += t20; + totHT += ht; + totSousKm += sousKm; + + const r = { + num: String(lig.numPiece || i + 1), + date: fmtDate(lig.date), + nature: lig.nature || '', + lib: lig.libelle || '', + km: km > 0 ? f2(km) : '', + tarifKm: tarif > 0 ? f3(tarif) : '', // ex: 0.697 + sousKm: sousKm > 0 ? f2(sousKm) : '', + ttc: f2(ttc), + tva21: f2(t21), tva55: f2(t55), + tva10: f2(t10), tva20: f2(t20), + ht: f2(ht), + }; + + for (const col of COLS) { + const isKmCol = KM_COLS.includes(col.key); + drawCellText( + doc, r[col.key], + colX[col.key], y, col.w, ROW_H, + 'Helvetica', 7, + isKmCol ? C.kmText : C.dark, + col.align + ); + } + } else { + // Ligne vide — zéros en gris sur colonnes numériques + for (const col of COLS) { + if (['ttc', 'tva21', 'tva55', 'tva10', 'tva20', 'ht'].includes(col.key)) + drawCellText(doc, '0.00', colX[col.key], y, col.w, ROW_H, 'Helvetica', 7, C.border, 'right'); + } + } + + // Bordures ligne + drawHLine(doc, MARGIN, MARGIN + tableW, y + ROW_H); + for (const col of COLS) drawVLine(doc, colX[col.key], y, y + ROW_H); + drawVLine(doc, MARGIN + tableW, y, y + ROW_H); + y += ROW_H; + } + + // ── 6. LIGNE TOTAL ─────────────────────────────────────────── + const totalY = y; + drawRect(doc, MARGIN, totalY, tableW, ROW_H + 2, C.totalBg, C.border, 0.5); + + // Fond violet sur les colonnes km dans la ligne total + for (const key of KM_COLS) { + const col = COLS.find(c => c.key === key); + drawRect(doc, colX[key], totalY, col.w, ROW_H + 2, C.kmTotalBg); + } + + doc.font('Helvetica-Bold').fontSize(8).fillColor(C.dark) + .text('Total', MARGIN + 3, totalY + 4, { lineBreak: false }); + + const totMap = { + km: totKm > 0 ? f2(totKm) : '', + tarifKm: '', // pas de somme de tarifs + sousKm: totSousKm > 0 ? f2(totSousKm) : '', + ttc: f2(totTTC), + tva21: f2(totT21), tva55: f2(totT55), + tva10: f2(totT10), tva20: f2(totT20), + ht: f2(totHT), + }; + + for (const col of COLS) { + if (!totMap[col.key] && totMap[col.key] !== '0.00') continue; + if (totMap[col.key] === '') continue; + const isKmCol = KM_COLS.includes(col.key); + drawCellText( + doc, totMap[col.key], + colX[col.key], totalY, col.w, ROW_H + 2, + 'Helvetica-Bold', 8, + isKmCol ? C.kmText : C.dark, + 'right' + ); + } + + // ── 7. ZONE BAS (simplifiée — les infos km sont dans le tableau) ── + const footY = totalY + ROW_H + 12; + const montantR = parseFloat((totTTC + totSousKm).toFixed(2)); + const bw = 64; + + // Montant à rembourser (simplifié) + doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark) + .text('Montant total à rembourser', MARGIN, footY + 4, { lineBreak: false }); + drawRect(doc, MARGIN + 180, footY, bw + 10, 18, C.amountBg, C.border, 0.5); + doc.font('Helvetica-Bold').fontSize(11).fillColor(C.blue) + .text(f2(montantR) + ' €', MARGIN + 182, footY + 3.5, { width: bw + 6, align: 'right', lineBreak: false }); + + // Rappel tarif utilisé (petit, discret) + doc.font('Helvetica').fontSize(6.5).fillColor(C.grey) + .text(`Tarif km appliqué : ${tarifKm.toFixed(3)} €/km — Sous-total km : ${f2(totSousKm)} € — TTC hors km : ${f2(totTTC)} €`, + MARGIN, footY + 24, { lineBreak: false }); + + // ── 8. SIGNATURES ───────────────────────────────────────────── + const sigStartX = MARGIN + 310; + const sigW = (tableW - 313) / 2 - 4; + const sigH = 52; + const sigY = footY - 2; + + const sigCollab = signatures.find(s => s.niveau === 'COLLAB'); + const sigManager = signatures.find(s => ['N1', 'N2'].includes(s.niveau)); + + _drawSigBox(doc, sigCollab, sigStartX, sigY, sigW, sigH, 'Date et signature Collaborateur', false); + _drawSigBox(doc, sigManager, sigStartX + sigW + 6, sigY, sigW, sigH, 'Date et signature', true); + + // ── 9. PIED DE PAGE ─────────────────────────────────────────── + doc.font('Helvetica').fontSize(6).fillColor(C.light) + .text( + `Réf. ${reference} — Généré le ${new Date().toLocaleDateString('fr-FR')} — NDF ENSUP Groupe — Document électronique`, + MARGIN, PAGE_H - 13, { width: tableW, align: 'center', lineBreak: false } + ); + + doc.end(); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// _drawSigBox — boîte signature avec ou sans contenu +// ───────────────────────────────────────────────────────────────────────────── +function _drawSigBox(doc, sig, x, y, w, h, label, isManager) { + let bg, border, accent, icon; + + if (sig) { + const a = sig.action || ''; + if (a === 'refuser' || a === 'refuse') { + bg = C.refusBg; border = C.refusBorder; accent = C.refusText; icon = '✗ REFUSÉ'; + } else if (isManager) { + bg = C.validBg; border = C.validBorder; accent = C.validText; icon = '✓ VALIDÉ'; + } else { + bg = C.collabBg; border = C.collabBorder; accent = C.collabText; icon = '✓ SOUMIS'; + } + } else { + bg = C.waitBg; border = C.waitBorder; accent = C.grey; icon = null; + } + + drawRect(doc, x, y, w, h, bg, border, 1); + + // Label haut + doc.font('Helvetica-Bold').fontSize(6.5).fillColor(C.grey) + .text(label, x + 4, y + 4, { width: w - 8, lineBreak: false }); + + if (sig) { + let nom = String(sig.nomPrenom || ''); + const ds = fmtDateTime(sig.date); + const comment = String(sig.commentaire || ''); + + doc.font('Helvetica-Bold').fontSize(8).fillColor(accent) + .text(icon, x + 4, y + 14, { lineBreak: false }); + + doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark); + while (nom.length > 1 && doc.widthOfString(nom) > w - 10) nom = nom.slice(0, -1); + doc.text(nom, x + 4, y + 25, { width: w - 8, lineBreak: false }); + + doc.font('Helvetica').fontSize(7).fillColor(C.grey) + .text(`Le ${ds}`, x + 4, y + 36, { width: w - 8, lineBreak: false }); + + if (comment) + doc.font('Helvetica-Oblique').fontSize(6.5).fillColor(C.grey) + .text(comment, x + 4, y + 45, { width: w - 8, lineBreak: false }); + + doc.save().strokeColor(border).lineWidth(0.5) + .moveTo(x + 3, y + h - 9).lineTo(x + w - 3, y + h - 9).stroke().restore(); + doc.font('Helvetica').fontSize(6).fillColor(C.grey) + .text('Signature — NDF ENSUP', x + 4, y + h - 7, { width: w - 8, align: 'center', lineBreak: false }); + } else { + doc.font('Helvetica').fontSize(8).fillColor(C.grey) + .text('En attente de signature', x + 4, y + h / 2 - 5, { width: w - 8, align: 'center', lineBreak: false }); + } +} \ No newline at end of file diff --git a/ndf/public/backend/package-lock.json b/ndf/public/backend/package-lock.json new file mode 100644 index 0000000..d251c96 --- /dev/null +++ b/ndf/public/backend/package-lock.json @@ -0,0 +1,3209 @@ +{ + "name": "ndf-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ndf-backend", + "version": "1.0.0", + "dependencies": { + "@azure/msal-node": "^2.16.3", + "@microsoft/microsoft-graph-client": "^3.0.7", + "axios": "^1.13.5", + "cors": "^2.8.6", + "dotenv": "^16.6.1", + "express": "^4.22.1", + "isomorphic-fetch": "^3.0.0", + "jsonwebtoken": "^9.0.3", + "mssql": "^11.0.1", + "multer": "^2.0.2", + "pdf-lib": "^1.17.1", + "pdfkit": "^0.17.2" + }, + "devDependencies": { + "@types/node": "^25.2.3", + "cross-env": "^10.1.0", + "ts-node": "^10.9.2", + "tsx": "^4.21.0" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.2.tgz", + "integrity": "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/msal-common": { + "version": "15.14.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.14.2.tgz", + "integrity": "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/msal-node": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.7.tgz", + "integrity": "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.14.2", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.28.2.tgz", + "integrity": "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.14.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": { + "version": "15.14.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.14.2.tgz", + "integrity": "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.16.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.1.tgz", + "integrity": "sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.3.tgz", + "integrity": "sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "14.16.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@js-joda/core": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz", + "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", + "license": "BSD-3-Clause" + }, + "node_modules/@microsoft/microsoft-graph-client": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-client/-/microsoft-graph-client-3.0.7.tgz", + "integrity": "sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependenciesMeta": { + "@azure/identity": { + "optional": true + }, + "@azure/msal-browser": { + "optional": true + }, + "buffer": { + "optional": true + }, + "stream-browserify": { + "optional": true + } + } + }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/standard-fonts/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/@pdf-lib/upng/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tediousjs/connection-string": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.5.0.tgz", + "integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.3.tgz", + "integrity": "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mssql": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-11.0.1.tgz", + "integrity": "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w==", + "license": "MIT", + "dependencies": { + "@tediousjs/connection-string": "^0.5.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "rfdc": "^1.3.0", + "tarn": "^3.0.2", + "tedious": "^18.2.1" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mssql/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mssql/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/pdfkit": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", + "integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tedious": { + "version": "18.6.2", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-18.6.2.tgz", + "integrity": "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.7.2", + "@azure/identity": "^4.2.1", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.6.1", + "@types/node": ">=18", + "bl": "^6.0.11", + "iconv-lite": "^0.6.3", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tedious/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/ndf/public/backend/package.json b/ndf/public/backend/package.json new file mode 100644 index 0000000..0c91e06 --- /dev/null +++ b/ndf/public/backend/package.json @@ -0,0 +1,30 @@ +{ + "name": "ndf-backend", + "version": "1.0.0", + "type": "module", + "main": "server.js", + "scripts": { + "start": "cross-env TZ=Europe/Paris node server.js", + "dev": "cross-env TZ=Europe/Paris nodemon server.js" + }, + "dependencies": { + "@azure/msal-node": "^2.16.3", + "@microsoft/microsoft-graph-client": "^3.0.7", + "axios": "^1.13.5", + "cors": "^2.8.6", + "dotenv": "^16.6.1", + "express": "^4.22.1", + "isomorphic-fetch": "^3.0.0", + "jsonwebtoken": "^9.0.3", + "mssql": "^11.0.1", + "multer": "^2.0.2", + "pdf-lib": "^1.17.1", + "pdfkit": "^0.17.2" + }, + "devDependencies": { + "@types/node": "^25.2.3", + "cross-env": "^10.1.0", + "ts-node": "^10.9.2", + "tsx": "^4.21.0" + } +} diff --git a/ndf/public/backend/server.js b/ndf/public/backend/server.js new file mode 100644 index 0000000..aab2c6c --- /dev/null +++ b/ndf/public/backend/server.js @@ -0,0 +1,4840 @@ +console.log('🚀 1. Démarrage du serveur...'); + +import express from 'express'; +import cors from 'cors'; +import sql from 'mssql'; +import jwt from 'jsonwebtoken'; +import { ConfidentialClientApplication } from '@azure/msal-node'; +import axios from 'axios'; +import dotenv from 'dotenv'; +import crypto from 'crypto'; +import multer from 'multer'; +import { Client } from '@microsoft/microsoft-graph-client'; +import 'isomorphic-fetch'; +import PDFDocument from 'pdfkit'; +import { PDFDocument as PDFLib } from 'pdf-lib'; + +import { generateFicheSignee, preparerLignesPDF } + from './ndfPdfGenerator.js'; + +console.log('✅ 2. Modules de base chargés'); + +dotenv.config(); +console.log('✅ 3. Dotenv chargé'); + +const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } }); + +const proxyCache = new Map(); +const PROXY_TTL = 10 * 60 * 1000; +function getCached(url) { + const entry = proxyCache.get(url); + if (!entry) return null; + if (Date.now() - entry.at > PROXY_TTL) { proxyCache.delete(url); return null; } + return entry; +} +function setCache(url, buffer, contentType) { + if (proxyCache.size >= 50) { + const oldest = [...proxyCache.entries()].sort((a, b) => a[1].at - b[1].at)[0]; + proxyCache.delete(oldest[0]); + } + proxyCache.set(url, { buffer, contentType, at: Date.now() }); +} + +const SHAREPOINT_CONFIG = { + siteId: process.env.SHAREPOINT_SITE_ID, + driveId: process.env.SHAREPOINT_DRIVE_ID, + basePath: 'Notes de Frais', +}; + +process.on('uncaughtException', (error) => { + console.error('\n❌❌❌ ERREUR NON CAPTURÉE ❌❌❌'); + console.error(error); + console.error(error.stack); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('\n❌❌❌ PROMESSE REJETÉE ❌❌❌'); + console.error('Raison:', reason); +}); + +process.on('exit', (code) => { + console.log(`\n⚠️ PROCESSUS EN COURS DE TERMINAISON - CODE: ${code}\n`); +}); + +console.log('✅ 4. Handlers d\'erreurs installés'); + +const app = express(); +console.log('✅ 5. Express initialisé'); + +const PORT = process.env.PORT || 3024; +console.log(`✅ 6. Port configuré: ${PORT}`); + +app.use(cors({ + origin: [ + 'http://myndf.ensup-adm.net', + 'https://myndf.ensup-adm.net', + 'http://localhost:3025', + 'http://localhost:81' + ], + credentials: true, +})); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +console.log('✅ 7. Middlewares installés'); + +const dbConfig = { + server: process.env.DB_SERVER || '192.168.0.3', + user: process.env.DB_USER || 'ndf_app', + password: process.env.DB_PASSWORD || 'P@ssw0rd2026!', + database: process.env.DB_NAME || 'NDF', + port: parseInt(process.env.DB_PORT) || 1433, + options: { + encrypt: true, + trustServerCertificate: true, + enableArithAbort: true, + connectTimeout: 60000, + requestTimeout: 60000, + useUTC: false + }, + pool: { max: 10, min: 0, idleTimeoutMillis: 30000 } +}; + +console.log('🔄 8. Test connexion SQL Server...'); +let pool; + +const AZURE_CONFIG = { + tenantId: process.env.AZURE_TENANT_ID, + clientId: process.env.AZURE_CLIENT_ID, + clientSecret: process.env.AZURE_CLIENT_SECRET, + groupId: process.env.AZURE_GROUP_ID || 'c1ea877c-6bca-4f47-bfad-f223640813a0' +}; + +async function initializeDatabase() { + try { + pool = await sql.connect(dbConfig); + console.log('✅ 9. Connexion SQL Server réussie'); + console.log(' Server:', dbConfig.server); + console.log(' User:', dbConfig.user); + console.log(' Database:', dbConfig.database); + console.log(' Port:', dbConfig.port); + } catch (err) { + console.error('❌ 9. ERREUR CONNEXION SQL SERVER:', err.message); + throw err; + } +} + +initializeDatabase().catch(err => { + console.error('❌ Impossible de démarrer le serveur:', err); + process.exit(1); +}); + +const msalConfig = { + auth: { + clientId: process.env.AZURE_CLIENT_ID, + authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`, + clientSecret: process.env.AZURE_CLIENT_SECRET + }, + system: { + loggerOptions: { + loggerCallback(loglevel, message) { + if (process.env.NODE_ENV === 'development') console.log('[MSAL]', message); + }, + piiLoggingEnabled: false, + logLevel: 'Info', + }, + } +}; + +const BAREME_KM_SERVER = { + 3: { t1: 0.529, t2_a: 0.316, t2_b: 1061, t3: 0.369 }, + 4: { t1: 0.606, t2_a: 0.340, t2_b: 1330, t3: 0.408 }, + 5: { t1: 0.636, t2_a: 0.356, t2_b: 1391, t3: 0.427 }, + 6: { t1: 0.665, t2_a: 0.374, t2_b: 1457, t3: 0.448 }, + 7: { t1: 0.697, t2_a: 0.394, t2_b: 1512, t3: 0.470 }, +}; + +function getIndemniteKmServer(kmTotal, chevaux) { + const cv = Math.min(Math.max(parseInt(chevaux) || 7, 3), 7); + const b = BAREME_KM_SERVER[cv]; + if (!b || kmTotal <= 0) return 0; + if (kmTotal <= 5000) return parseFloat((kmTotal * b.t1).toFixed(2)); + if (kmTotal <= 20000) return parseFloat((kmTotal * b.t2_a + b.t2_b).toFixed(2)); + return parseFloat((kmTotal * b.t3).toFixed(2)); +} + +function normalizeCampus(campus) { + if (!campus) return null; + const c = campus.toUpperCase(); + if (c.includes('SQY') || c.includes('SAINT') || c.includes('SQUY')) return 'SQY'; + if (c.includes('CGY') || c.includes('CERGY')) return 'CGY'; + if (c.includes('MRS') || c.includes('MARSEILLE')) return 'MRS'; + if (c.includes('NTE') || c.includes('NANTES')) return 'NTE'; + return null; +} + +// ── Helper getTarifKm ──────────────────────────────────────────── +async function getTarifKm() { + try { + const annee = new Date().getFullYear(); + const result = await pool.request() + .input('annee', sql.Int, annee) + .query(` + SELECT TOP 1 tarifParKm + FROM ParametresKm + WHERE annee = @annee AND actif = 1 + ORDER BY DateCreation DESC + `); + if (result.recordset[0]?.tarifParKm) { + return parseFloat(result.recordset[0].tarifParKm); + } + return 0.697; // Fallback 7 CV+ + } catch (e) { + console.warn('⚠️ getTarifKm fallback 0.697:', e.message); + return 0.697; + } +} + +async function getConfigDebiteur() { + try { + const result = await pool.request().query(` + SELECT TOP 1 companyName, companyIban, companyBic, + companyAddress, companyCp, companyVille, companyPays + FROM ConfigDebiteurXML + WHERE actif = 1 + ORDER BY DateModification DESC + `); + if (result.recordset.length) return result.recordset[0]; + } catch (e) { + console.warn('⚠️ getConfigDebiteur fallback .env:', e.message); + } + // Fallback .env si table inaccessible + return { + companyName: process.env.COMPANY_NAME || 'ENSUP GROUP', + companyIban: process.env.COMPANY_IBAN || 'FR0000000000000000000000000', + companyBic: process.env.COMPANY_BIC || 'BNPAFRPP', + companyAddress: process.env.COMPANY_ADDRESS || '', + companyCp: process.env.COMPANY_CP || '', + companyVille: process.env.COMPANY_VILLE || '', + companyPays: process.env.COMPANY_PAYS || 'FR', + }; +} +// ── Helpers IBAN ───────────────────────────────────────────────── +// ── Helpers IBAN ───────────────────────────────────────────────── +function getIbanKey() { + const key = process.env.IBAN_ENCRYPTION_KEY; + if (!key) throw new Error('IBAN_ENCRYPTION_KEY manquante'); + const buf = Buffer.from(key, 'hex'); + if (buf.length !== 32) throw new Error(`IBAN_ENCRYPTION_KEY invalide: ${buf.length} octets (attendu: 32)`); + return buf; +} + +function encryptIban(iban) { + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv('aes-256-gcm', getIbanKey(), iv); + const encrypted = Buffer.concat([cipher.update(iban, 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`; +} + +function decryptIban(stored) { + const [ivHex, tagHex, encHex] = stored.split(':'); + const decipher = crypto.createDecipheriv('aes-256-gcm', getIbanKey(), Buffer.from(ivHex, 'hex')); + decipher.setAuthTag(Buffer.from(tagHex, 'hex')); + return decipher.update(Buffer.from(encHex, 'hex')) + decipher.final('utf8'); +} + +function validateIban(iban) { + const lengths = { FR: 27, BE: 16, DE: 22, ES: 24, IT: 27 }; + const country = iban.slice(0, 2); + if (lengths[country] && iban.length !== lengths[country]) return false; + const rearranged = iban.slice(4) + iban.slice(0, 4); + const numeric = rearranged.split('').map(c => isNaN(c) ? (c.charCodeAt(0) - 55).toString() : c).join(''); + let remainder = 0; + for (const chunk of numeric.match(/.{1,9}/g)) { + remainder = parseInt(remainder + chunk) % 97; + } + return remainder === 1; +} + +function maskIban(iban) { + if (!iban || iban.length < 8) return iban; + return iban.slice(0, 4) + ' ' + + iban.slice(4, -4).replace(/./g, '*').match(/.{1,4}/g).join(' ') + + ' ' + iban.slice(-4); +} + +function hashIban(iban) { + return crypto.createHash('sha256').update(iban + process.env.IBAN_HASH_SALT).digest('hex'); +} +// ✅ NOUVEAU — Charge les rôles depuis UtilisateurRoles (remplace CollaborateurAD.role) +async function getRolesForUser(collaborateurId) { + try { + const result = await pool.request() + .input('id', sql.Int, collaborateurId) + .query(` + SELECT role + FROM UtilisateurRoles + WHERE collaborateur_id = @id AND actif = 1 + `); + return result.recordset.map(r => r.role); + // Retourne ex: ['Collaboratrice', 'Finance'] ou ['superUtilisateur'] + } catch (e) { + console.warn('⚠️ getRolesForUser erreur:', e.message); + return []; + } +} + +function formatDateParis(date) { + if (!date) return '—'; + const d = new Date(date); + return d.toLocaleString('fr-FR', { + timeZone: 'Europe/Paris', + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit' + }); +} + +const cca = new ConfidentialClientApplication(msalConfig); +console.log('✅ 10. MSAL configuré'); + +// ================================================ +// 🔑 TOKEN MICROSOFT GRAPH +// ================================================ +async function getGraphToken() { + try { + console.log('🔑 Tentative d\'obtention du token...'); + console.log(' Tenant ID:', AZURE_CONFIG.tenantId ? '✅' : '❌ MANQUANT'); + console.log(' Client ID:', AZURE_CONFIG.clientId ? '✅' : '❌ MANQUANT'); + console.log(' Client Secret:', AZURE_CONFIG.clientSecret ? '✅' : '❌ MANQUANT'); + + if (!AZURE_CONFIG.tenantId || !AZURE_CONFIG.clientId || !AZURE_CONFIG.clientSecret) { + throw new Error('Configuration Azure incomplète - vérifiez votre fichier .env'); + } + + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: AZURE_CONFIG.clientId, + client_secret: AZURE_CONFIG.clientSecret, + scope: 'https://graph.microsoft.com/.default' + }); + + const response = await axios.post( + `https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`, + params.toString(), + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ); + + console.log('✅ Token obtenu avec succès'); + return response.data.access_token; + } catch (error) { + console.error('❌ Erreur obtention token:', error.message); + if (error.response) { + console.error(' Status HTTP:', error.response.status); + console.error(' Erreur détaillée:', JSON.stringify(error.response.data, null, 2)); + } + return null; + } +} + +// ================================================ +// 🔄 SYNCHRONISATION ENTRA ID +// ================================================ +async function syncEntraIdUsers() { + const syncResults = { processed: 0, inserted: 0, updated: 0, deactivated: 0, errors: [] }; + + try { + console.log('\n🔄 === DÉBUT SYNCHRONISATION ENTRA ID ==='); + + const accessToken = await getGraphToken(); + if (!accessToken) { console.error('❌ Impossible d\'obtenir le token'); return syncResults; } + console.log('✅ Token obtenu'); + + const groupResponse = await axios.get( + `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, + { headers: { Authorization: `Bearer ${accessToken}` } } + ); + const groupName = groupResponse.data.displayName; + console.log(`📋 Groupe : ${groupName}`); + + let allAzureMembers = []; + let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle,officeLocation,accountEnabled&$top=999`; + + console.log('📥 Récupération des membres...'); + while (nextLink) { + const membersResponse = await axios.get(nextLink, { headers: { Authorization: `Bearer ${accessToken}` } }); + allAzureMembers = allAzureMembers.concat(membersResponse.data.value); + nextLink = membersResponse.data['@odata.nextLink']; + if (nextLink) console.log(` 📄 ${allAzureMembers.length} membres récupérés...`); + } + + console.log(`✅ ${allAzureMembers.length} membres trouvés`); + + const validMembers = allAzureMembers.filter(m => { + if (!m.mail || m.mail.trim() === '') return false; + if (m.accountEnabled === false) return false; + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(m.mail); + }); + + console.log(`✅ ${validMembers.length} membres valides`); + + const transaction = new sql.Transaction(pool); + await transaction.begin(); + + try { + const azureEmails = new Set(); + validMembers.forEach(m => azureEmails.add(m.mail.toLowerCase().trim())); + + console.log('\n📝 Traitement des utilisateurs...'); + + for (const m of validMembers) { + try { + const emailClean = m.mail.toLowerCase().trim(); + syncResults.processed++; + + const request = new sql.Request(transaction); + request.input('email', sql.NVarChar, emailClean); + const result = await request.query(` + SELECT id, email, entraUserId, Actif FROM CollaborateurAD WHERE LOWER(email) = LOWER(@email) + `); + + if (result.recordset.length > 0) { + const updateRequest = new sql.Request(transaction); + updateRequest.input('entraUserId', sql.NVarChar, m.id); + updateRequest.input('prenom', sql.NVarChar, m.givenName || ''); + updateRequest.input('nom', sql.NVarChar, m.surname || ''); + updateRequest.input('departement', sql.NVarChar, m.department || ''); + updateRequest.input('fonction', sql.NVarChar, m.jobTitle || ''); + updateRequest.input('campus', sql.NVarChar, m.officeLocation || ''); + updateRequest.input('email', sql.NVarChar, emailClean); + await updateRequest.query(` + UPDATE CollaborateurAD SET + entraUserId = @entraUserId, prenom = @prenom, nom = @nom, + departement = @departement, fonction = @fonction, campus = @campus, + Actif = 1, dateMiseAJour = GETDATE(), DateModification = GETDATE() + WHERE LOWER(email) = LOWER(@email) + `); + syncResults.updated++; + console.log(` ✓ Mis à jour : ${emailClean}`); + } else { + const insertRequest = new sql.Request(transaction); + insertRequest.input('entraUserId', sql.NVarChar, m.id); + insertRequest.input('prenom', sql.NVarChar, m.givenName || ''); + insertRequest.input('nom', sql.NVarChar, m.surname || ''); + insertRequest.input('email', sql.NVarChar, emailClean); + insertRequest.input('departement', sql.NVarChar, m.department || ''); + insertRequest.input('fonction', sql.NVarChar, m.jobTitle || ''); + insertRequest.input('campus', sql.NVarChar, m.officeLocation || ''); + await insertRequest.query(` + INSERT INTO CollaborateurAD + (entraUserId, prenom, nom, email, departement, fonction, campus, role, service, Actif, DateCreation, DateModification, dateMiseAJour) + VALUES (@entraUserId, @prenom, @nom, @email, @departement, @fonction, @campus, 'Collaborateur', NULL, 1, GETDATE(), GETDATE(), GETDATE()) + `); + syncResults.inserted++; + console.log(` ✓ Créé : ${emailClean}`); + } + } catch (userError) { + syncResults.errors.push({ email: m.mail, error: userError.message }); + console.error(` ❌ Erreur ${m.mail}:`, userError.message); + } + } + + console.log('\n🔍 Désactivation des comptes obsolètes...'); + if (azureEmails.size > 0) { + const activeEmailsList = Array.from(azureEmails).map(e => `'${e}'`).join(','); + const deactivateRequest = new sql.Request(transaction); + const deactivateResult = await deactivateRequest.query(` + UPDATE CollaborateurAD SET Actif = 0, DateModification = GETDATE() + WHERE email IS NOT NULL AND email != '' + AND LOWER(email) NOT IN (${activeEmailsList}) + AND (Actif = 1 OR Actif IS NULL) + `); + syncResults.deactivated = deactivateResult.rowsAffected[0]; + console.log(` ✓ ${syncResults.deactivated} compte(s) désactivé(s)`); + } + + await transaction.commit(); + + console.log('\n📊 === RÉSUMÉ ==='); + console.log(` Groupe: ${groupName}`); + console.log(` Total Entra: ${allAzureMembers.length}`); + console.log(` Valides: ${validMembers.length}`); + console.log(` Traités: ${syncResults.processed}`); + console.log(` Créés: ${syncResults.inserted}`); + console.log(` Mis à jour: ${syncResults.updated}`); + console.log(` Désactivés: ${syncResults.deactivated}`); + console.log(` Erreurs: ${syncResults.errors.length}`); + + } catch (error) { + await transaction.rollback(); + throw error; + } + + } catch (error) { + console.error('\n❌ ERREUR SYNCHRONISATION:', error.message); + } + + return syncResults; +} + +// ================================================ +// MIDDLEWARE JWT +// ================================================ +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Token manquant' }); + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; + next(); + } catch (err) { + return res.status(403).json({ error: 'Token invalide ou expiré' }); + } +}; + +// requireRole gère les rôles multiples (tableau req.user.roles) +const requireRole = (...rolesRequis) => (req, res, next) => { + if (!req.user) return res.status(401).json({ error: 'Non authentifié' }); + const hasRole = req.user.roles && req.user.roles.some(r => rolesRequis.includes(r)); + if (!hasRole) { + return res.status(403).json({ error: `Accès refusé — rôle(s) requis : ${rolesRequis.join(' ou ')}` }); + } + next(); +}; + +// Helper : vérifie si l'utilisateur possède un rôle parmi une liste +const hasAnyRole = (user, ...roles) => user.roles && user.roles.some(r => roles.includes(r)); + +// ================================================ +// ROUTES DE BASE +// ================================================ +app.get('/', (req, res) => { + res.json({ message: 'API Gestion des Notes de Frais', version: '1.0.0', status: 'OK' }); +}); + +app.get('/users-dev', async (req, res) => { + try { + const result = await pool.request().query(` + SELECT id, email, nom, prenom, role, Actif FROM CollaborateurAD + WHERE Actif = 1 OR Actif IS NULL ORDER BY nom, prenom + `); + res.json(result.recordset); + } catch (error) { + console.error('❌ Erreur /users-dev:', error); + res.status(500).json({ error: error.message }); + } +}); + +// ✅ MODIFIÉ — login-dev avec rôles depuis UtilisateurRoles +app.post('/login-dev', async (req, res) => { + try { + const { accessToken } = req.body; + if (!accessToken) return res.status(400).json({ error: 'Token d\'accès manquant' }); + + let userInfo; + try { + const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + if (!graphResponse.ok) throw new Error('Token invalide'); + userInfo = await graphResponse.json(); + } catch (graphError) { + return res.status(401).json({ error: 'Token d\'accès invalide', details: graphError.message }); + } + + const userEmail = userInfo.mail || userInfo.userPrincipalName; + const result = await pool.request() + .input('email', sql.VarChar, userEmail) + .query(`SELECT * FROM CollaborateurAD WHERE email = @email AND (Actif = 1 OR Actif IS NULL)`); + + if (!result.recordset.length) + return res.status(404).json({ error: 'Utilisateur non trouvé ou compte désactivé' }); + + const user = result.recordset[0]; + + // ✅ Rôles depuis UtilisateurRoles au lieu de CollaborateurAD.role + const userRoles = await getRolesForUser(user.id); + if (!userRoles.length) + return res.status(403).json({ error: 'Aucun rôle assigné dans UtilisateurRoles' }); + + const rolesAutorises = [ + 'Collaborateur', 'Collaboratrice', + 'Validateur', 'Validatrice', + 'Finance', 'VerificateurFinance', 'ValidateurFinance', + 'superUtilisateur' + ]; + const hasAccess = userRoles.some(r => rolesAutorises.includes(r)); + if (!hasAccess) + return res.status(403).json({ error: `Vos rôles n'ont pas accès à cette application` }); + + const token = jwt.sign( + { + id: user.id, + email: user.email, + roles: userRoles, + nom: user.nom, + prenom: user.prenom, + societe: user.societe, + campus: normalizeCampus(user.campus), + }, + process.env.JWT_SECRET, + { expiresIn: '8h' } + ); + + res.json({ + token, + user: { + id: user.id, + nom: user.nom, + prenom: user.prenom, + email: user.email, + roles: userRoles, + societe: user.societe, + campus: user.campus, + } + }); + } catch (error) { + res.status(500).json({ error: 'Erreur serveur inattendue', details: error.message }); + } +}); + +app.get('/auth/verify', authenticateToken, (req, res) => { + res.json({ valid: true, user: req.user }); +}); + +app.post('/auth/logout', authenticateToken, (req, res) => { + res.json({ success: true, message: 'Déconnexion réussie' }); +}); + +const pkceStore = new Map(); + +app.get('/api/auth/microsoft', (req, res) => { + const tenantId = process.env.AZURE_TENANT_ID; + const clientId = process.env.AZURE_CLIENT_ID; + const state = Math.random().toString(36).substring(2, 15); + const codeVerifier = crypto.randomBytes(32).toString('base64url'); + const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url'); + pkceStore.set(state, codeVerifier); + setTimeout(() => pkceStore.delete(state), 10 * 60 * 1000); + + // ✅ Détecte automatiquement l'URL d'origine + const redirectUri = process.env.OAUTH_REDIRECT_URI || 'https://myndf.ensup-adm.net/api/auth/callback'; + + console.log('🔗 Redirect URI:', redirectUri); + + const params = new URLSearchParams({ + client_id: clientId, + response_type: 'code', + redirect_uri: redirectUri, + response_mode: 'query', + scope: 'openid profile email User.Read', + state, + prompt: 'select_account', + code_challenge: codeChallenge, + code_challenge_method: 'S256' + }); + + res.redirect(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`); +}); + + +// ✅ MODIFIÉ — auth/callback avec rôles depuis UtilisateurRoles +app.get('/api/auth/callback', async (req, res) => { + const { code, error, state } = req.query; + if (error) return res.redirect(`/login?error=${error}&desc=${encodeURIComponent(req.query.error_description || '')}`); + if (!code) return res.redirect(`/login?error=no_code`); + + const codeVerifier = pkceStore.get(state); + if (!codeVerifier) return res.redirect(`/login?error=invalid_state`); + pkceStore.delete(state); + + try { + // ✅ Détecte automatiquement l'URL d'origine (même logique que /api/auth/microsoft) + const redirectUri = process.env.OAUTH_REDIRECT_URI || 'https://myndf.ensup-adm.net/api/auth/callback'; + + console.log('🔗 Callback Redirect URI:', redirectUri); + + const tokenResponse = await axios.post( + `https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`, + new URLSearchParams({ + client_id: AZURE_CONFIG.clientId, + client_secret: AZURE_CONFIG.clientSecret, + code, + redirect_uri: redirectUri, // ✅ URI détectée automatiquement + grant_type: 'authorization_code', + code_verifier: codeVerifier + }), + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ); + + const accessToken = tokenResponse.data.access_token; + const userResponse = await axios.get('https://graph.microsoft.com/v1.0/me', { + headers: { Authorization: `Bearer ${accessToken}` } + }); + + const userEmail = userResponse.data.mail || userResponse.data.userPrincipalName; + const result = await pool.request() + .input('email', sql.VarChar, userEmail) + .query(`SELECT * FROM CollaborateurAD WHERE email = @email AND (Actif = 1 OR Actif IS NULL)`); + + if (!result.recordset.length) return res.redirect(`/login?error=user_not_found`); + + const user = result.recordset[0]; + + // ✅ Rôles depuis UtilisateurRoles + const userRoles = await getRolesForUser(user.id); + if (!userRoles.length) + return res.redirect(`/login?error=no_role_assigned`); + + const rolesAutorises = ['Collaborateur', 'Collaboratrice', 'Validateur', 'Validatrice', 'Finance', 'VerificateurFinance', 'ValidateurFinance', , 'superUtilisateur']; + const hasAccess = userRoles.some(r => rolesAutorises.includes(r)); + if (!hasAccess) return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3025'}/login?error=unauthorized_role`); + + + const jwtToken = jwt.sign( + { + id: user.id, + email: user.email, + roles: userRoles, + nom: user.nom, + prenom: user.prenom, + societe: user.societe, + campus: normalizeCampus(user.campus), + }, + process.env.JWT_SECRET, + { expiresIn: '8h' } + ); + + // ✅ Redirection vers le frontend avec le token + res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3025'}/auth/callback?token=${jwtToken}`); + + } catch (error) { + console.error('❌ Erreur callback OAuth:', error.message); + if (error.response?.data) { + console.error('Détails:', JSON.stringify(error.response.data, null, 2)); + } + res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3025'}/login?error=auth_failed&details=${encodeURIComponent(error.message)}`); + + } +}); + +// GET /api/verificateur/notes — notes approuvées à vérifier +app.get('/api/verificateur/notes', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé VerificateurFinance' }); + + try { + const request = pool.request(); + let campusWhere = ''; + if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) { + const campusCode = normalizeCampus(req.user.campus); + if (campusCode) { + request.input('campus', sql.NVarChar, `%${campusCode}%`); + campusWhere = `AND c.campus LIKE @campus`; + } + } + + const result = await request.query(` + SELECT n.*, + c.nom + ' ' + c.prenom AS collaborateur, + c.email AS collaborateurEmail, + c.departement, c.campus, c.societe, + v1.nom + ' ' + v1.prenom AS nomN1, + v2.nom + ' ' + v2.prenom AS nomN2, + vf.nom + ' ' + vf.prenom AS nomVerificateur, + n.dateVerification, n.commentaireVerification + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id + LEFT JOIN CollaborateurAD v2 ON v2.id = n.validateurN2Id + LEFT JOIN CollaborateurAD vf ON vf.id = n.verificateurFinanceId + WHERE n.statut = 'approuve' + ${campusWhere} + ORDER BY n.DateCreation DESC + `); + + const notes = result.recordset; + for (const note of notes) { + const ncResult = await pool.request() + .input('noteId', sql.Int, note.id) + .query(` + SELECT fileName, motif, statut, dateSignalement + FROM JustificatifsNonConformes + WHERE noteDeFraisId = @noteId + ORDER BY dateSignalement DESC + `); + note.nonConformes = ncResult.recordset; + } + + res.json(notes); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// PUT /api/verificateur/notes/:id/verifier +app.put('/api/verificateur/notes/:id/verifier', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé VerificateurFinance' }); + + const { commentaire } = req.body; + const noteId = parseInt(req.params.id); + + try { + // Récupérer la note + const noteResult = await pool.request() + .input('id', sql.Int, noteId) + .query(` + SELECT n.*, c.prenom, c.nom, c.email, c.campus + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE n.id = @id AND n.statut = 'approuve' + `); + + if (!noteResult.recordset.length) + return res.status(404).json({ error: 'Note introuvable ou statut incompatible' }); + + const note = noteResult.recordset[0]; + + // Marquer comme vérifiée + await pool.request() + .input('id', sql.Int, noteId) + .input('verificateurId', sql.Int, req.user.id) + .input('commentaire', sql.NVarChar, commentaire || null) + .query(` + UPDATE NoteDeFrais SET + statut = 'verifie', + verificateurFinanceId = @verificateurId, + dateVerification = GETDATE(), + commentaireVerification = @commentaire, + DateModification = GETDATE() + WHERE id = @id + `); + + // Historique + await pool.request() + .input('noteId', sql.Int, noteId) + .input('validateurId', sql.Int, req.user.id) + .input('action', sql.NVarChar, 'verifier') + .input('commentaire', sql.NVarChar, commentaire || null) + .input('statut', sql.NVarChar, 'verifie') + .query(` + INSERT INTO HistoriqueValidation + (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, NouveauStatut, DateAction) + VALUES + (@noteId, @validateurId, 'VERIF', @action, @commentaire, @statut, GETDATE()) + `); + + // Trouver les ValidateurFinance du même campus pour les notifier + const campusNorm = normalizeCampus(note.campus); + const validRequest = pool.request() + .input('campus', sql.NVarChar, campusNorm ? `%${campusNorm}%` : '%'); + const validateurs = await validRequest.query(` + SELECT c.id, c.email, c.prenom, c.nom + FROM CollaborateurAD c + JOIN UtilisateurRoles r ON r.collaborateur_id = c.id + WHERE r.role = 'ValidateurFinance' AND r.actif = 1 + AND c.campus LIKE @campus AND c.Actif = 1 +`); + + const verificateurNom = `${req.user.prenom} ${req.user.nom}`; + const montantFormate = parseFloat(note.montant).toFixed(2); + const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net'; + + for (const val of validateurs.recordset) { + // Notification BDD + try { + await creerNotification({ + destinataireId: val.id, + destinataireEmail: val.email, + type: 'paiement', + titre: `✅ Note vérifiée à valider — ${note.reference}`, + message: `${verificateurNom} a vérifié la note ${note.reference} (${montantFormate} €) de ${note.prenom} ${note.nom}. En attente de votre validation de paiement.`, + noteId: noteId + }); + } catch (e) { console.error('Notif ValidateurFinance:', e.message); } + + // Email + try { + await sendMailGraph( + val.email, + `✅ Note vérifiée — validation paiement requise : ${note.reference}`, + `
+
+

✅ Note vérifiée — paiement à valider

+
+
+

Bonjour ${val.prenom} ${val.nom},

+

La note ${note.reference} de ${note.prenom} ${note.nom} (${montantFormate} €) + a été vérifiée par ${verificateurNom} et est prête pour le paiement.

+ ${commentaire ? `

+ 💬 Commentaire vérificateur : ${commentaire}

` : ''} +
+ + + + + +
Référence${note.reference}
Collaborateur${note.prenom} ${note.nom}
Montant${montantFormate} €
Campus${note.campus || '—'}
+
+
+ + Valider le paiement → + +
+
+
` + ); + } catch (e) { console.error('Email ValidateurFinance:', e.message); } + } + + // Notifier aussi le collaborateur + try { + await creerNotification({ + destinataireId: note.collaborateurId, + destinataireEmail: note.email, + type: 'paiement', + titre: `Note ${note.reference} en cours de traitement`, + message: `Votre note ${note.reference} (${montantFormate} €) a été vérifiée et est en attente de validation du paiement.`, + noteId: noteId + }); + } catch (e) { console.error('Notif collab vérification:', e.message); } + + res.json({ success: true, statut: 'verifie', notifiesCount: validateurs.recordset.length }); + + } catch (error) { + console.error('Erreur PUT verificateur/notes/:id/verifier:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/verificateur/historique +app.get('/api/verificateur/historique', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès refusé' }); + + try { + const request = pool.request().input('verificateurId', sql.Int, req.user.id); + + let campusWhere = ''; + if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) { + const campusCode = normalizeCampus(req.user.campus); + if (campusCode) { + request.input('campus', sql.NVarChar, `%${campusCode}%`); + campusWhere = 'AND c.campus LIKE @campus'; + } + } + + const result = await request.query(` + SELECT + n.id, n.reference, n.libelle, n.montant, + n.dateVerification, n.commentaireVerification, + n.lignesJson, n.fichiers, + c.nom + ' ' + c.prenom AS collaborateur, + c.campus, c.departement, + (SELECT COUNT(*) FROM JustificatifsNonConformes j + WHERE j.noteDeFraisId = n.id) AS nbNonConformes + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE n.verificateurFinanceId = @verificateurId + AND n.statut IN ('verifie', 'paiementenattente', 'payee') + ${campusWhere} + ORDER BY n.dateVerification DESC + `); + + const notesAvecNC = await Promise.all(result.recordset.map(async row => { + const ncResult = await pool.request() + .input('noteId', sql.Int, row.id) + .query(` + SELECT fileName, motif, statut, dateSignalement + FROM JustificatifsNonConformes + WHERE noteDeFraisId = @noteId + ORDER BY dateSignalement DESC + `); + + const nonConformes = ncResult.recordset; + + let nbJustifs = 0; + try { + const fichiers = JSON.parse(row.fichiers || '[]'); + const SYSTEME = ['soumission', 'resoumission', 'recap-paiement', 'recap', 'signe-approuve', 'signeapprouve', 'signe_approuve']; + nbJustifs = fichiers.filter(f => + !SYSTEME.some(kw => (f.fileName || '').toLowerCase().includes(kw)) + ).length; + } catch { } + + const nbNonConformes = nonConformes.length; + const nbConformes = Math.max(0, nbJustifs - nbNonConformes); + + return { + ...row, + nbJustifs, + nbConformes, + nbNonConformes, + nonConformes, + dateVerification: row.dateVerification, + commentaire: row.commentaireVerification, + }; + })); + + res.json(notesAvecNC); + + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); +// ================================================ +// SYNC ENTRA — réservé Finance +// ================================================ +app.post('/api/sync-entra', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ success: false, message: 'Accès refusé - réservé à la Finance' }); + const results = await syncEntraIdUsers(); + res.json({ success: true, message: 'Synchronisation terminée', stats: results }); +}); + +app.get('/api/sync-status', authenticateToken, async (req, res) => { + try { + const totalDB = await pool.request().query(` + SELECT COUNT(*) as total, + SUM(CASE WHEN Actif = 1 THEN 1 ELSE 0 END) as actifs, + SUM(CASE WHEN Actif = 0 THEN 1 ELSE 0 END) as inactifs + FROM CollaborateurAD + `); + const derniers = await pool.request().query(` + SELECT TOP 10 id, prenom, nom, email, role, Actif, dateCreation, dateMiseAJour + FROM CollaborateurAD ORDER BY dateMiseAJour DESC + `); + let entraStatus = { connected: false }; + try { + const token = await getGraphToken(); + if (token) { + const groupResponse = await axios.get( + `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, + { headers: { Authorization: `Bearer ${token}` } } + ); + entraStatus = { connected: true, groupName: groupResponse.data.displayName, groupId: AZURE_CONFIG.groupId }; + } + } catch (err) { entraStatus.error = err.message; } + + res.json({ + success: true, + database: totalDB.recordset[0], + entraId: entraStatus, + derniers_utilisateurs: derniers.recordset + }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// ── GET tarif km actif ───────────────────────────────────────────── +app.get('/api/parametres/km', authenticateToken, async (req, res) => { + try { + const result = await pool.request().query(` + SELECT TOP 1 tarifParKm + FROM ParametresKm + WHERE actif = 1 + ORDER BY annee DESC + `); + const tarif = result.recordset[0]?.tarifParKm ?? 0.697; + res.json({ tarifKm: tarif }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// ── GET taux TVA actifs ──────────────────────────────────────────── +app.get('/api/parametres/tva', authenticateToken, async (req, res) => { + try { + const result = await pool.request().query(` + SELECT taux, libelle, categorie + FROM ParametresTVA + WHERE actif = 1 + AND dateDebut <= GETDATE() + AND (dateFin IS NULL OR dateFin >= GETDATE()) + ORDER BY taux ASC + `); + res.json(result.recordset); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// ── PUT tarif km — réservé Finance ──────────────────────────────── +app.put('/api/parametres/km', authenticateToken, requireRole('Finance'), async (req, res) => { + const { tarifParKm, annee } = req.body; + if (!tarifParKm || isNaN(tarifParKm)) + return res.status(400).json({ error: 'Tarif invalide' }); + try { + await pool.request().query(`UPDATE ParametresKm SET actif = 0 WHERE actif = 1`); + await pool.request() + .input('tarif', sql.Decimal(10, 4), parseFloat(tarifParKm)) + .input('annee', sql.Int, annee || new Date().getFullYear()) + .query(`INSERT INTO ParametresKm (tarifParKm, annee, actif, DateCreation) VALUES (@tarif, @annee, 1, GETDATE())`); + res.json({ success: true, tarifParKm: parseFloat(tarifParKm) }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +app.get('/api/parametres/bareme-km', authenticateToken, (req, res) => { + res.json({ + baremes: [ + { chevaux: 3, label: '3 CV et moins', t1: 0.529, t2_a: 0.316, t2_b: 1061, t3: 0.369 }, + { chevaux: 4, label: '4 CV', t1: 0.606, t2_a: 0.340, t2_b: 1330, t3: 0.408 }, + { chevaux: 5, label: '5 CV', t1: 0.636, t2_a: 0.356, t2_b: 1391, t3: 0.427 }, + { chevaux: 6, label: '6 CV', t1: 0.665, t2_a: 0.374, t2_b: 1457, t3: 0.448 }, + { chevaux: 7, label: '7 CV et plus', t1: 0.697, t2_a: 0.394, t2_b: 1512, t3: 0.470 }, + ] + }); +}); + +// ✅ MODIFIÉ — profil expose les rôles depuis UtilisateurRoles +app.get('/api/profile', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('id', sql.Int, req.user.id) + .query(` + SELECT id, nom, prenom, email, role, fonction as poste, + TypeContrat, DateEntree, campus, departement, societe, + adresse_rue, adresse_cp, adresse_ville, adresse_pays + FROM CollaborateurAD WHERE id = @id + `); + if (!result.recordset.length) return res.status(404).json({ error: 'Profil non trouvé' }); + const userProfile = result.recordset[0]; + + // ✅ Rôles depuis UtilisateurRoles au lieu de CollaborateurAD.role + const rolesDB = await getRolesForUser(req.user.id); + userProfile.roles = rolesDB.length > 0 ? rolesDB : ['Collaborateur']; + + res.json(userProfile); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.put('/api/profile/adresse', authenticateToken, async (req, res) => { + try { + const { adresse_rue, adresse_cp, adresse_ville, adresse_pays, societe } = req.body; + + if (!adresse_rue || !adresse_cp || !adresse_ville || !adresse_pays) + return res.status(400).json({ error: 'Tous les champs adresse sont obligatoires' }); + + await pool.request() + .input('id', sql.Int, req.user.id) + .input('adresse_rue', sql.NVarChar, adresse_rue.trim()) + .input('adresse_cp', sql.NVarChar, adresse_cp.trim()) + .input('adresse_ville', sql.NVarChar, adresse_ville.trim()) + .input('adresse_pays', sql.NVarChar, adresse_pays.trim()) + .input('societe', sql.NVarChar, societe ? societe.trim() : null) + .query(` + UPDATE CollaborateurAD SET + adresse_rue = @adresse_rue, + adresse_cp = @adresse_cp, + adresse_ville = @adresse_ville, + adresse_pays = @adresse_pays, + societe = COALESCE(@societe, societe), + DateModification = GETDATE() + WHERE id = @id + `); + + res.json({ success: true }); + } catch (error) { + console.error('PUT /api/profile/adresse :', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// ================================================ +// HELPERS +// ================================================ +async function genererReference(campus, nom, prenom) { + const now = new Date(); + const annee = now.getFullYear(); + const mois = String(now.getMonth() + 1).padStart(2, '0'); + + // Normaliser le campus + const campusCode = normalizeCampus(campus) || 'XXX'; + + // Construire la partie nom : NOM.P (première lettre du prénom) + const nomClean = (nom || '').toUpperCase() + .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // supprimer accents + .replace(/[^A-Z]/g, ''); // garder uniquement lettres + const prenomInitiale = (prenom || '').charAt(0).toUpperCase() + .normalize('NFD').replace(/[\u0300-\u036f]/g, '') + .replace(/[^A-Z]/g, ''); + + const nomPart = `${nomClean}.${prenomInitiale}`; + + const tx = new sql.Transaction(pool); + await tx.begin(); + try { + await new sql.Request(tx).query(` + IF NOT EXISTS (SELECT 1 FROM NDFSequence WHERE annee = ${annee}) + INSERT INTO NDFSequence (annee, compteur) VALUES (${annee}, 0) + `); + const result = await new sql.Request(tx).query(` + UPDATE NDFSequence SET compteur = compteur + 1 OUTPUT INSERTED.compteur WHERE annee = ${annee} + `); + await tx.commit(); + const num = String(result.recordset[0].compteur).padStart(3, '0'); + return `NDF-${annee}-${mois}-${campusCode}-${nomPart}`; + } catch (e) { await tx.rollback(); throw e; } +} + +async function uploadToSharePoint(file, ndfReference, collaborateurNomPrenom) { + const accessToken = await getGraphToken(); + if (!accessToken) throw new Error('Token Graph indisponible'); + const safeName = file.originalname.replace(/[^a-zA-Z0-9._\-]/g, '_'); + const fileName = `${ndfReference}_${safeName}`; + const folderPath = `${SHAREPOINT_CONFIG.basePath}/${collaborateurNomPrenom}`; + const uploadPath = `${folderPath}/${fileName}`; + + const res = await axios.put( + `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${uploadPath}:/content`, + file.buffer, + { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': file.mimetype } } + ); + return { fileName, uploadUrl: res.data.webUrl }; +} + +async function downloadFromSharePoint(webUrl) { + const accessToken = await getGraphToken(); + const urlObj = new URL(webUrl); + const fullPath = decodeURIComponent(urlObj.pathname); + const marker = '/Shared Documents/'; + const markerAlt = '/Documents/'; + let relativePath = ''; + if (fullPath.includes(marker)) relativePath = fullPath.split(marker)[1]; + else if (fullPath.includes(markerAlt)) relativePath = fullPath.split(markerAlt)[1]; + else { + const parts = fullPath.split('/sites/')[1]?.split('/'); + relativePath = parts?.slice(2).join('/') || ''; + } + const res = await axios.get( + `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${relativePath}:/content`, + { headers: { Authorization: `Bearer ${accessToken}` }, responseType: 'arraybuffer' } + ); + return Buffer.from(res.data); +} + +async function uploadToSharePointHierarchique(file, noteRef, nomDossier, moisDossier) { + const accessToken = await getGraphToken(); + if (!accessToken) throw new Error('Token Graph indisponible'); + const safeName = (file.originalname || 'fichier').replace(/[^a-zA-Z0-9._\-]/g, '_'); + const fileName = safeName.startsWith(noteRef) ? safeName : `${noteRef}_${safeName}`; + const folderPath = `${SHAREPOINT_CONFIG.basePath}/${nomDossier}/${moisDossier}/${noteRef}`; + const uploadPath = `${folderPath}/${fileName}`; + + const res = await axios.put( + `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${uploadPath}:/content`, + file.buffer, + { + headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': file.mimetype || 'application/octet-stream' }, + maxBodyLength: Infinity, maxContentLength: Infinity + } + ); + return { fileName, uploadUrl: res.data.webUrl, folderPath }; +} + +// ══════════════════════════════════════════════════════ +// 📎 RÉCAP COMPLET (fiche PDF/A + justificatifs fusionnés) +// ══════════════════════════════════════════════════════ +async function generateRecapWithJustifs(noteData, justifFiles, signaturesOpt) { + const signatures = signaturesOpt || [ + { niveau: 'COLLAB', nomPrenom: noteData.nomPrenom || '', date: new Date(), action: 'soumettre', commentaire: null } + ]; + const ficheBuffer = await generateFicheSignee(noteData, signatures); + + const finalPdf = await PDFLib.create(); + const fichePdf = await PDFLib.load(ficheBuffer); + const fichePages = await finalPdf.copyPages(fichePdf, fichePdf.getPageIndices()); + fichePages.forEach(p => finalPdf.addPage(p)); + + for (const file of justifFiles) { + const mimetype = file.mimetype || ''; + if (mimetype === 'application/pdf') { + try { + const justifPdf = await PDFLib.load(file.buffer); + const pages = await finalPdf.copyPages(justifPdf, justifPdf.getPageIndices()); + pages.forEach(p => finalPdf.addPage(p)); + } catch (e) { console.warn(`⚠️ PDF non intégrable: ${file.originalname} — ${e.message}`); } + } else if (mimetype.startsWith('image/')) { + try { + const page = finalPdf.addPage([595, 842]); + const img = mimetype === 'image/png' ? await finalPdf.embedPng(file.buffer) : await finalPdf.embedJpg(file.buffer); + const maxW = 495, maxH = 742; + const ratio = Math.min(maxW / img.width, maxH / img.height); + const w = img.width * ratio, h = img.height * ratio; + page.drawImage(img, { x: (595 - w) / 2, y: (842 - h) / 2, width: w, height: h }); + } catch (e) { console.warn(`⚠️ Image non intégrable: ${file.originalname} — ${e.message}`); } + } + } + + return Buffer.from(await finalPdf.save()); +} + +// ══════════════════════════════════════════════════════ +// POST /api/notes — Créer une note de frais (multi-lignes) +// ══════════════════════════════════════════════════════ +app.post('/api/notes', authenticateToken, upload.any(), async (req, res) => { + try { + const { libelle, date, description, participants, nombreParticipants, lignes, qrNoteRef } = req.body; + + if (!libelle || !date || !lignes) + return res.status(400).json({ error: 'Libellé, date et lignes sont obligatoires' }); + + let lignesParsed; + try { + lignesParsed = typeof lignes === 'string' ? JSON.parse(lignes) : lignes; + } catch { + return res.status(400).json({ error: 'Format des lignes invalide' }); + } + if (!Array.isArray(lignesParsed) || lignesParsed.length === 0) + return res.status(400).json({ error: 'Au moins une ligne est obligatoire' }); + + let tarifKm = await getTarifKm(); + try { + const annee = new Date().getFullYear(); + const kmParam = await pool.request() + .input('annee', sql.Int, annee) + .query(`SELECT TOP 1 tarifParKm FROM ParametresKm WHERE annee = @annee AND actif = 1 ORDER BY DateCreation DESC`); + if (kmParam.recordset[0]?.tarifParKm) tarifKm = parseFloat(kmParam.recordset[0].tarifParKm); + } catch { } + + const lignesPDF = preparerLignesPDF(lignesParsed, tarifKm); + const montantTTC = lignesPDF.reduce((s, l) => s + l.montantTTC, 0); + const indemKm = lignesPDF.reduce((s, l) => s + l.indemniteKm, 0); + const kmTotal = lignesPDF.reduce((s, l) => s + (l.km || 0), 0); + const montantFinal = parseFloat((montantTTC + indemKm).toFixed(2)); + const montantFormate = montantFinal.toFixed(2); + const isKmOnly = lignesPDF.every(l => l.montantTTC === 0 && l.indemniteKm > 0); + console.log('🔍 isKmOnly:', isKmOnly, 'kmTotal:', kmTotal, 'montantTTC:', montantTTC); + + const categorieNote = isKmOnly ? 'Kilométrique pur' : 'Multiple'; + + // POST /api/notes — ligne ~420 + const collabResult = await pool.request() + .input('id', sql.Int, req.user.id) + .query(`SELECT id, email, prenom, nom, departement, campus FROM CollaborateurAD WHERE id = @id`); + if (!collabResult.recordset.length) return res.status(404).json({ error: 'Collaborateur non trouvé' }); + const collaborateur = collabResult.recordset[0]; + const nomPrenom = `${collaborateur.nom.toUpperCase()} ${collaborateur.prenom}`; + + const dateObj = new Date(date); + const moisStr = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); + const moisCapitalized = moisStr.charAt(0).toUpperCase() + moisStr.slice(1); + + const hierarchie = await pool.request() + .input('collabId', sql.Int, req.user.id) + .query(` + SELECT h.SuperieurId, h.[SuperieurIdn+2], + s1.email AS emailN1, s1.prenom AS prenomN1, s1.nom AS nomN1, + s2.email AS emailN2 + FROM HierarchieValidationNDF h + LEFT JOIN CollaborateurAD s1 ON s1.id = h.SuperieurId + LEFT JOIN CollaborateurAD s2 ON s2.id = h.[SuperieurIdn+2] + WHERE h.CollaborateurId = @collabId + `); + const n1Id = hierarchie.recordset[0]?.SuperieurId ?? null; + const n2Id = hierarchie.recordset[0]?.['SuperieurIdn+2'] ?? null; + const emailN1 = hierarchie.recordset[0]?.emailN1 ?? null; + const prenomN1 = hierarchie.recordset[0]?.prenomN1 ?? null; + const nomN1 = hierarchie.recordset[0]?.nomN1 ?? null; + + const reference = await genererReference(collaborateur.campus, collaborateur.nom, collaborateur.prenom); + + const nomDossier = `${collaborateur.prenom}_${collaborateur.nom}`.replace(/\s+/g, '_'); + const now = new Date(); + const moisDossier = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; + + const allFiles = [...(req.files || [])]; + if (qrNoteRef) { + const qrToken = await pool.request() + .input('noteRef', sql.NVarChar, qrNoteRef) + .query(`SELECT TOP 1 fichiers FROM UploadTokens WHERE noteRef = @noteRef AND used = 1 ORDER BY expiresAt DESC`); + if (qrToken.recordset.length && qrToken.recordset[0].fichiers) { + const qrFichiers = JSON.parse(qrToken.recordset[0].fichiers); + for (const f of qrFichiers) { + try { + const buf = await downloadFromSharePoint(f.uploadUrl); + allFiles.push({ buffer: buf, originalname: f.fileName, mimetype: f.fileName?.endsWith('.pdf') ? 'application/pdf' : 'image/jpeg', size: buf.length }); + } catch (e) { console.warn('⚠️ QR global download fail:', e.message); } + } + } + } + + // ✅ QR par ligne — récupère et STOCKE les fichiers dans qrFiles de chaque ligne + for (let i = 0; i < lignesParsed.length; i++) { + const ligneQrRef = lignesParsed[i].qrNoteRef; + if (!ligneQrRef) continue; + try { + const qrLigne = await pool.request() + .input('noteRef', sql.NVarChar, ligneQrRef) + .query(`SELECT TOP 1 fichiers FROM UploadTokens WHERE noteRef = @noteRef AND used = 1 ORDER BY expiresAt DESC`); + if (qrLigne.recordset.length && qrLigne.recordset[0].fichiers) { + const qrFichiers = JSON.parse(qrLigne.recordset[0].fichiers); + + // ✅ Initialiser qrFiles pour cette ligne + if (!lignesParsed[i].qrFiles) lignesParsed[i].qrFiles = []; + + for (const f of qrFichiers) { + try { + const buf = await downloadFromSharePoint(f.uploadUrl); + const fileObj = { + buffer: buf, + originalname: f.fileName, + mimetype: f.fileName?.endsWith('.pdf') ? 'application/pdf' : 'image/jpeg', + size: buf.length + }; + + // Ajouter à allFiles pour le récap PDF global + allFiles.push(fileObj); + + // ✅ Uploader vers SP avec la référence finale et stocker dans qrFiles + try { + const uploaded = await uploadToSharePointHierarchique( + fileObj, reference, nomDossier, moisDossier + ); + // Éviter les doublons + const dejaSauve = lignesParsed[i].qrFiles.some(x => x.fileName === uploaded.fileName); + if (!dejaSauve) { + lignesParsed[i].qrFiles.push({ + fileName: uploaded.fileName, + uploadUrl: uploaded.uploadUrl + }); + } + console.log(`✅ QR ligne ${i} stocké dans qrFiles: ${uploaded.fileName}`); + } catch (uploadErr) { + console.warn(`⚠️ Upload SP ligne ${i}:`, uploadErr.message); + } + } catch (e) { console.warn(`⚠️ QR ligne ${i} download fail:`, e.message); } + } + } else { + console.warn(`⚠️ QR ligne ${i} (${ligneQrRef}) : token introuvable ou non utilisé`); + } + } catch (e) { + console.warn(`⚠️ QR ligne ${i} erreur DB:`, e.message); + } + } + + // ✅ Recalculer lignesJson APRÈS enrichissement des qrFiles + const lignesJsonFinal = JSON.stringify(lignesParsed); + + const fichiersUploades = []; + for (const file of allFiles) { + try { + const r = await uploadToSharePointHierarchique(file, reference, nomDossier, moisDossier); + fichiersUploades.push(r); + } catch (e) { console.error(`❌ Upload justif ${file.originalname}:`, e.message); } + } + + const noteDataPDF = { + reference, + nomPrenom, + mois: moisCapitalized, + date, + categorie: categorieNote, + libelle, + montant: montantFinal, + lignes: lignesParsed, + lignesJson: JSON.stringify(lignesParsed), + tarifKm: await getTarifKm(), + statut: 'enattente', + departement: collaborateur.departement, + participants: participants || null, + }; + + let ficheResult = null; + try { + const fichePDF = await generateFicheSignee( + noteDataPDF, + [{ niveau: 'COLLAB', nomPrenom, date: new Date(), action: 'soumettre', commentaire: null }] + ); + ficheResult = await uploadToSharePointHierarchique( + { buffer: fichePDF, originalname: `${reference}_soumission.pdf`, mimetype: 'application/pdf', size: fichePDF.length }, + reference, nomDossier, moisDossier + ); + fichiersUploades.push(ficheResult); + console.log('✅ Fiche soumission uploadée:', ficheResult.fileName); + } catch (e) { console.error('❌ Génération fiche PDF:', e.message, e.stack); } + + let recapUrl = null; + try { + const recapBuffer = await generateRecapWithJustifs(noteDataPDF, allFiles); + const recapResult = await uploadToSharePointHierarchique( + { buffer: recapBuffer, originalname: `${reference}_recap.pdf`, mimetype: 'application/pdf', size: recapBuffer.length }, + reference, nomDossier, moisDossier + ); + fichiersUploades.push(recapResult); + recapUrl = recapResult.uploadUrl; + console.log('✅ Récap PDF uploadé:', recapResult.fileName); + } catch (e) { console.error('❌ Génération récap PDF:', e.message); } + + const insertResult = await pool.request() + .input('reference', sql.NVarChar, reference) + .input('collaborateurId', sql.Int, req.user.id) + .input('libelle', sql.NVarChar, libelle) + .input('montant', sql.Decimal, montantFinal) + .input('date', sql.Date, new Date(date)) + .input('categorie', sql.NVarChar, categorieNote) + .input('description', sql.NVarChar, description || null) + .input('participants', sql.NVarChar, participants ? String(participants) : null) + .input('nombreParticipants', sql.Int, nombreParticipants ? parseInt(nombreParticipants) : null) + .input('sharepointUrl', sql.NVarChar, recapUrl || ficheResult?.uploadUrl || fichiersUploades[0]?.uploadUrl || null) + .input('fichiers', sql.NVarChar, JSON.stringify(fichiersUploades)) + .input('statut', sql.NVarChar, 'enattente') + .input('validateurN1Id', sql.Int, n1Id) + .input('validateurN2Id', sql.Int, n2Id) + .input('montantHT', sql.Decimal, null) + .input('tauxTVA', sql.Decimal, null) + .input('montantTVA21', sql.Decimal, null) + .input('montantTVA55', sql.Decimal, null) + .input('montantTVA10', sql.Decimal, null) + .input('montantTVA20', sql.Decimal, null) + .input('km', sql.Decimal, kmTotal || null) + .input('indemniteKm', sql.Decimal, indemKm || null) + .input('lignesJson', sql.NVarChar, lignesJsonFinal) + .query(` + INSERT INTO NoteDeFrais + (reference, collaborateurId, libelle, montant, date, categorie, + description, participants, nombreParticipants, sharepointUrl, fichiers, + statut, validateurN1Id, validateurN2Id, + montantHT, tauxTVA, montantTVA21, montantTVA55, montantTVA10, montantTVA20, + km, indemniteKm, lignesJson) + OUTPUT INSERTED.id, INSERTED.reference + VALUES + (@reference, @collaborateurId, @libelle, @montant, @date, @categorie, + @description, @participants, @nombreParticipants, @sharepointUrl, @fichiers, + @statut, @validateurN1Id, @validateurN2Id, + @montantHT, @tauxTVA, @montantTVA21, @montantTVA55, @montantTVA10, @montantTVA20, + @km, @indemniteKm, @lignesJson) + `); + + const noteCreee = insertResult.recordset[0]; + + for (let i = 0; i < lignesParsed.length; i++) { + const l = lignesParsed[i]; + const pdf = lignesPDF[i]; + try { + await pool.request() + .input('noteId', sql.Int, noteCreee.id) + .input('numPiece', sql.Int, i + 1) + .input('date', sql.Date, new Date(l.date)) + .input('nature', sql.NVarChar, l.categorie || '') + .input('libelle', sql.NVarChar, l.libelle || '') + .input('km', sql.Decimal, pdf.km || null) + .input('montantTTC', sql.Decimal, pdf.montantTTC || null) + .input('tva21', sql.Decimal, pdf.tva21 || null) + .input('tva55', sql.Decimal, pdf.tva55 || null) + .input('tva10', sql.Decimal, pdf.tva10 || null) + .input('tva20', sql.Decimal, pdf.tva20 || null) + .input('montantHT', sql.Decimal, pdf.montantHT || null) + .input('tauxTVA', sql.Decimal, parseFloat(l.tauxTVA) || null) + .input('indemniteKm', sql.Decimal, pdf.indemniteKm || null) + .query(` + INSERT INTO LigneNoteDeFrais + (noteDeFraisId, numPiece, date, nature, libelle, + km, montantTTC, tva21, tva55, tva10, tva20, + montantHT, tauxTVA, indemniteKm) + VALUES + (@noteId, @numPiece, @date, @nature, @libelle, + @km, @montantTTC, @tva21, @tva55, @tva10, @tva20, + @montantHT, @tauxTVA, @indemniteKm) + `); + } catch (e) { console.error(`❌ Insertion ligne ${i + 1}:`, e.message); } + } + + console.log(`✅ Note créée: ${reference} (${lignesParsed.length} lignes) par ${collaborateur.email}`); + + const dateFormatee = new Date(date).toLocaleDateString('fr-FR'); + const frontendUrl = process.env.FRONTEND_URL || 'myndf.ensup-adm.net'; + + try { + await creerNotification({ + destinataireId: collaborateur.id, destinataireEmail: null, type: 'soumission', + titre: `✅ Note ${reference} soumise avec succès`, + message: `Votre note ${reference} — ${libelle} (${montantFormate} €) a bien été reçue.`, + noteId: noteCreee.id + }); + } catch (e) { console.error('❌ Notif BDD collab:', e.message); } + + try { + await sendMailGraph( + collaborateur.email, + `✅ Accusé de réception — Note ${reference}`, + `
+
+

✅ Note de frais bien reçue

+
+
+

Bonjour ${collaborateur.prenom} ${collaborateur.nom},

+

Votre note ${reference} — ${libelle} (${montantFormate} €) a bien été enregistrée.

+ ${nomN1 ? `

Validateur : ${prenomN1} ${nomN1}

` : ''} + ${recapUrl ? `

📎 Voir le récapitulatif PDF

` : ''} +
+ Suivre mes notes → +
+
+
` + ); + } catch (e) { console.error('❌ Email accusé collab:', e.message); } + + if (n1Id && emailN1) { + try { + await creerNotification({ + destinataireId: n1Id, destinataireEmail: emailN1, type: 'validation', + titre: `📋 Note à valider — ${reference}`, + message: `${collaborateur.prenom} ${collaborateur.nom} a soumis une note de frais de ${montantFormate} € en attente de votre validation.`, + noteId: noteCreee.id + }); + } catch (e) { console.error('❌ Notif BDD N1:', e.message); } + + try { + await sendMailGraph( + emailN1, + `📋 Note de frais à valider — ${reference}`, + `
+
+

📋 Note de frais à valider

+
+
+

Bonjour ${prenomN1} ${nomN1},

+

${collaborateur.prenom} ${collaborateur.nom} a soumis la note ${reference} — ${libelle} (${montantFormate} €).

+ ${recapUrl ? `

📎 Récapitulatif + justificatifs

` : ''} +
+ Valider sur la plateforme → +
+
+
` + ); + } catch (e) { console.error('❌ Email N1:', e.message); } + } + + res.status(201).json({ + success: true, id: noteCreee.id, reference: noteCreee.reference, + fichiers: fichiersUploades, recapUrl, + }); + + } catch (error) { + console.error('❌ Erreur POST /api/notes:', error.message); + res.status(500).json({ error: error.message }); + } + } +); +app.put('/api/notes/:id', authenticateToken, upload.any(), async (req, res) => { + try { + const noteId = parseInt(req.params.id); + const userId = req.user.id; + + const noteCheck = await pool.request() + .input('id', sql.Int, noteId) + .input('collabId', sql.Int, userId) + .query(` + SELECT * FROM NoteDeFrais + WHERE id = @id AND collaborateurId = @collabId + AND statut IN ('enattente', 'refuse', 'non_conforme_verif') + `); + + if (!noteCheck.recordset.length) + return res.status(403).json({ error: 'Note introuvable ou non modifiable (statut incompatible)' }); + + const noteExist = noteCheck.recordset[0]; + + // ✅ Détecter si correction (refusée ou non-conforme) → nouvelle note + const estCorrection = noteExist.statut === 'refuse' || noteExist.statut === 'non_conforme_verif'; + + const { libelle, date, description, participants, nombreParticipants, lignes } = req.body; + + if (!libelle || !date || !lignes) + return res.status(400).json({ error: 'Libellé, date et lignes sont obligatoires' }); + + let lignesParsed; + try { lignesParsed = typeof lignes === 'string' ? JSON.parse(lignes) : lignes; } + catch { return res.status(400).json({ error: 'Format des lignes invalide' }); } + + // ── Calculs communs ─────────────────────────────────────────────────── + const montantFinal = lignesParsed.reduce((total, l) => { + const isKm = (l.categorie || '').toLowerCase().includes('kilom'); + if (isKm) { + const km = parseFloat(l.km) || 0; + const cv = parseInt(l.chevaux) || 7; + return total + getIndemniteKmServer(km, cv); + } + const items = l.tvaItems || [{ montantTTC: l.montant, taux: l.tauxTVA }]; + return total + items.reduce((s, i) => s + (parseFloat(i.montantTTC) || 0), 0); + }, 0); + + const indemKm = lignesParsed.reduce((s, l) => { + if ((l.categorie || '').toLowerCase().includes('kilom')) + return s + getIndemniteKmServer(parseFloat(l.km) || 0, parseInt(l.chevaux) || 7); + return s; + }, 0); + + const kmTotal = lignesParsed.reduce((s, l) => + s + ((l.categorie || '').toLowerCase().includes('kilom') ? (parseFloat(l.km) || 0) : 0), 0 + ); + + const isKmOnly = lignesParsed.every(l => (l.categorie || '').toLowerCase().includes('kilom')); + const categorieNote = isKmOnly ? 'Kilométrique pur' : 'Multiple'; + + const collabResult = await pool.request() + .input('id', sql.Int, userId) + .query(`SELECT id, email, prenom, nom, departement, campus FROM CollaborateurAD WHERE id = @id`); + const collaborateur = collabResult.recordset[0]; + const nomPrenom = `${collaborateur.nom.toUpperCase()} ${collaborateur.prenom}`; + + const dateObj = new Date(date); + const moisStr = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); + const moisCapitalized = moisStr.charAt(0).toUpperCase() + moisStr.slice(1); + const nomDossier = `${collaborateur.prenom}_${collaborateur.nom}`.replace(/\s+/g, '_'); + const now = new Date(); + const moisDossier = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; + const tarifKmVal = await getTarifKm(); + + // ════════════════════════════════════════════════════════════════════ + // CAS 1 — Note REFUSÉE ou NON_CONFORME_VERIF → créer une NOUVELLE note + // ════════════════════════════════════════════════════════════════════ + if (estCorrection) { + + // 1. Archiver l'ancienne note + const statutArchive = noteExist.statut === 'non_conforme_verif' + ? 'non_conforme_archive' + : 'refuse_archive'; + + await pool.request() + .input('id', sql.Int, noteId) + .input('statutArchive', sql.NVarChar, statutArchive) + .query(` + UPDATE NoteDeFrais SET + statut = @statutArchive, + DateModification = GETDATE() + WHERE id = @id + `); + + // 2. Nouvelle référence + const nouvelleReference = await genererReference(collaborateur.campus, collaborateur.nom, collaborateur.prenom); + + // 3. Upload fichiers joints + const allFiles = [...(req.files || [])]; + const fichiersUploades = []; + for (const file of allFiles) { + try { + const r = await uploadToSharePointHierarchique(file, nouvelleReference, nomDossier, moisDossier); + fichiersUploades.push(r); + } catch (e) { console.error(`❌ Upload justif correction ${file.originalname}:`, e.message); } + } + + // 4. Récupérer fichiers QR par ligne + for (let i = 0; i < lignesParsed.length; i++) { + const ligneQrRef = lignesParsed[i].qrNoteRef; + if (!ligneQrRef) continue; + try { + const qrLigne = await pool.request() + .input('noteRef', sql.NVarChar, ligneQrRef) + .query(`SELECT TOP 1 fichiers FROM UploadTokens WHERE noteRef = @noteRef AND used = 1 ORDER BY expiresAt DESC`); + if (qrLigne.recordset.length && qrLigne.recordset[0].fichiers) { + const qrFichiers = JSON.parse(qrLigne.recordset[0].fichiers); + for (const f of qrFichiers) { + try { + const buf = await downloadFromSharePoint(f.uploadUrl); + const r = await uploadToSharePointHierarchique( + { buffer: buf, originalname: f.fileName, mimetype: f.fileName?.endsWith('.pdf') ? 'application/pdf' : 'image/jpeg', size: buf.length }, + nouvelleReference, nomDossier, moisDossier + ); + fichiersUploades.push(r); + } catch (e) { console.warn(`⚠️ QR ligne ${i} download fail:`, e.message); } + } + } + } catch (e) { console.warn(`⚠️ QR ligne ${i} erreur DB:`, e.message); } + } + + // 5. Générer fiche PDF + const noteDataPDF = { + reference: nouvelleReference, nomPrenom, mois: moisCapitalized, date, + categorie: categorieNote, libelle, + montant: parseFloat(montantFinal.toFixed(2)), + lignes: lignesParsed, lignesJson: JSON.stringify(lignesParsed), + tarifKm: tarifKmVal, statut: 'enattente', + departement: collaborateur.departement, + }; + + try { + const commentairePDF = noteExist.statut === 'non_conforme_verif' + ? `Correction suite à non-conformité signalée sur ${noteExist.reference}` + : `Correction suite au refus de ${noteExist.reference}`; + const fichePDF = await generateFicheSignee(noteDataPDF, [{ + niveau: 'COLLAB', nomPrenom, date: new Date(), + action: 'soumettre', commentaire: commentairePDF + }]); + const ficheResult = await uploadToSharePointHierarchique( + { buffer: fichePDF, originalname: `${nouvelleReference}_soumission.pdf`, mimetype: 'application/pdf', size: fichePDF.length }, + nouvelleReference, nomDossier, moisDossier + ); + fichiersUploades.push(ficheResult); + } catch (e) { console.error('❌ Fiche PDF correction:', e.message); } + + // 6. Hiérarchie + const hierarchie = await pool.request() + .input('collabId', sql.Int, userId) + .query(` + SELECT h.SuperieurId, h.[SuperieurIdn+2], + s1.email AS emailN1, s1.prenom AS prenomN1, s1.nom AS nomN1, + s2.email AS emailN2 + FROM HierarchieValidationNDF h + LEFT JOIN CollaborateurAD s1 ON s1.id = h.SuperieurId + LEFT JOIN CollaborateurAD s2 ON s2.id = h.[SuperieurIdn+2] + WHERE h.CollaborateurId = @collabId + `); + const n1Id = hierarchie.recordset[0]?.SuperieurId ?? null; + const n2Id = hierarchie.recordset[0]?.['SuperieurIdn+2'] ?? null; + const emailN1 = hierarchie.recordset[0]?.emailN1 ?? null; + const prenomN1 = hierarchie.recordset[0]?.prenomN1 ?? null; + const nomN1 = hierarchie.recordset[0]?.nomN1 ?? null; + + // 7. Insérer nouvelle note + const insertResult = await pool.request() + .input('reference', sql.NVarChar, nouvelleReference) + .input('collaborateurId', sql.Int, userId) + .input('libelle', sql.NVarChar, libelle) + .input('montant', sql.Decimal, parseFloat(montantFinal.toFixed(2))) + .input('date', sql.Date, new Date(date)) + .input('categorie', sql.NVarChar, categorieNote) + .input('description', sql.NVarChar, description || null) + .input('participants', sql.NVarChar, participants ? String(participants) : null) + .input('nombreParticipants', sql.Int, nombreParticipants ? parseInt(nombreParticipants) : null) + .input('fichiers', sql.NVarChar, JSON.stringify(fichiersUploades)) + .input('statut', sql.NVarChar, 'enattente') + .input('validateurN1Id', sql.Int, n1Id) + .input('validateurN2Id', sql.Int, n2Id) + .input('km', sql.Decimal, kmTotal || null) + .input('indemniteKm', sql.Decimal, indemKm || null) + .input('lignesJson', sql.NVarChar, JSON.stringify(lignesParsed)) + .input('noteRefuseeId', sql.Int, noteId) + .query(` + INSERT INTO NoteDeFrais + (reference, collaborateurId, libelle, montant, date, categorie, + description, participants, nombreParticipants, + fichiers, statut, validateurN1Id, validateurN2Id, + km, indemniteKm, lignesJson, noteRefuseeId, + DateCreation, DateModification) + OUTPUT INSERTED.id, INSERTED.reference + VALUES + (@reference, @collaborateurId, @libelle, @montant, @date, @categorie, + @description, @participants, @nombreParticipants, + @fichiers, @statut, @validateurN1Id, @validateurN2Id, + @km, @indemniteKm, @lignesJson, @noteRefuseeId, + GETDATE(), GETDATE()) + `); + + const nouvelleNote = insertResult.recordset[0]; + + // 8. Insérer lignes + const lignesPDF = preparerLignesPDF(lignesParsed, tarifKmVal); + for (let i = 0; i < lignesParsed.length; i++) { + const l = lignesParsed[i]; + const pdf = lignesPDF[i] || {}; + const isKmLine = (l.categorie || '').toLowerCase().includes('kilom'); + const kmLine = parseFloat(l.km) || 0; + const cvLine = parseInt(l.chevaux) || 7; + const indemLine = isKmLine ? getIndemniteKmServer(kmLine, cvLine) : 0; + const items = l.tvaItems || [{ montantTTC: l.montant, taux: l.tauxTVA }]; + const ttcLine = isKmLine ? indemLine : items.reduce((s, item) => s + (parseFloat(item.montantTTC) || 0), 0); + try { + await pool.request() + .input('noteId', sql.Int, nouvelleNote.id) + .input('numPiece', sql.Int, i + 1) + .input('date', sql.Date, new Date(l.date)) + .input('nature', sql.NVarChar, l.categorie || '') + .input('libelle', sql.NVarChar, l.libelle || '') + .input('km', sql.Decimal, isKmLine ? kmLine : null) + .input('montantTTC', sql.Decimal, ttcLine || null) + .input('tva21', sql.Decimal, pdf.tva21 || null) + .input('tva55', sql.Decimal, pdf.tva55 || null) + .input('tva10', sql.Decimal, pdf.tva10 || null) + .input('tva20', sql.Decimal, pdf.tva20 || null) + .input('montantHT', sql.Decimal, pdf.montantHT || null) + .input('tauxTVA', sql.Decimal, parseFloat(l.tauxTVA) || null) + .input('indemniteKm', sql.Decimal, indemLine || null) + .query(` + INSERT INTO LigneNoteDeFrais + (noteDeFraisId, numPiece, date, nature, libelle, + km, montantTTC, tva21, tva55, tva10, tva20, + montantHT, tauxTVA, indemniteKm) + VALUES + (@noteId, @numPiece, @date, @nature, @libelle, + @km, @montantTTC, @tva21, @tva55, @tva10, @tva20, + @montantHT, @tauxTVA, @indemniteKm) + `); + } catch (e) { console.error(`❌ Insertion ligne correction ${i + 1}:`, e.message); } + } + + // 9. Notifier N1 + if (n1Id && emailN1) { + try { + const titreNotif = noteExist.statut === 'non_conforme_verif' + ? `📋 Note corrigée à valider — ${nouvelleReference}` + : `📋 Note corrigée à valider — ${nouvelleReference}`; + // Dans le PUT /api/notes/:id — CAS 1 correction, section "Notifier N1" + const msgNotif = `${collaborateur.prenom} ${collaborateur.nom} a resoumis une note corrigée. +Ancienne référence : ${noteExist.reference} (${statutArchive}) +Nouvelle référence : ${nouvelleReference} +Montant : ${parseFloat(montantFinal.toFixed(2))} €`; + + await creerNotification({ + destinataireId: n1Id, destinataireEmail: emailN1, + type: 'validation', titre: titreNotif, message: msgNotif, + noteId: nouvelleNote.id + }); + + const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net'; + const couleurHeader = noteExist.statut === 'non_conforme_verif' + ? 'linear-gradient(135deg,#f97316,#ea580c)' + : 'linear-gradient(135deg,#f59e0b,#d97706)'; + const contexte = noteExist.statut === 'non_conforme_verif' + ? `suite à la non-conformité signalée sur ${noteExist.reference}` + : `suite au refus de ${noteExist.reference}`; + + await sendMailGraph(emailN1, `📋 Note corrigée à valider — ${nouvelleReference}`, + `
+
+

📋 Note corrigée — à valider

+
+
+

Bonjour ${prenomN1} ${nomN1},

+

${collaborateur.prenom} ${collaborateur.nom} a resoumis une note corrigée ${contexte}.

+

Nouvelle référence : ${nouvelleReference}${parseFloat(montantFinal.toFixed(2))} €

+
+ Valider sur la plateforme → +
+
+
` + ); + } catch (e) { console.error('❌ Notif N1 correction:', e.message); } + } + + console.log(`✅ Note corrigée ${nouvelleReference} créée (remplace ${noteExist.reference} → ${statutArchive}) par ${collaborateur.email}`); + return res.json({ + success: true, + id: nouvelleNote.id, + reference: nouvelleReference, + statut: 'enattente', + isCorrection: true, + ancienneReference: noteExist.reference + }); + } + + // ════════════════════════════════════════════════════════════════════ + // CAS 2 — Note EN ATTENTE → modifier sur place (comportement original) + // ════════════════════════════════════════════════════════════════════ + + const reference = noteExist.reference; + const allFiles = [...(req.files || [])]; + let fichiersExistants = []; + try { fichiersExistants = JSON.parse(noteExist.fichiers || '[]'); } catch { } + + for (const file of allFiles) { + try { + const r = await uploadToSharePointHierarchique(file, reference, nomDossier, moisDossier); + fichiersExistants.push(r); + } catch (e) { console.error(`❌ Upload justif modif ${file.originalname}:`, e.message); } + } + + const noteDataPDF = { + reference, nomPrenom, mois: moisCapitalized, date, + categorie: categorieNote, libelle, + montant: parseFloat(montantFinal.toFixed(2)), + lignes: lignesParsed, lignesJson: JSON.stringify(lignesParsed), + tarifKm: tarifKmVal, statut: 'enattente', + departement: collaborateur.departement, + }; + + try { + const fichePDF = await generateFicheSignee(noteDataPDF, [ + { niveau: 'COLLAB', nomPrenom, date: new Date(), action: 'soumettre', commentaire: 'Note modifiée et resoumise' } + ]); + const ficheResult = await uploadToSharePointHierarchique( + { buffer: fichePDF, originalname: `${reference}_resoumission.pdf`, mimetype: 'application/pdf', size: fichePDF.length }, + reference, nomDossier, moisDossier + ); + fichiersExistants.push(ficheResult); + } catch (e) { console.error('❌ Fiche PDF re-soumission:', e.message); } + + await pool.request() + .input('id', sql.Int, noteId) + .input('libelle', sql.NVarChar, libelle) + .input('montant', sql.Decimal, parseFloat(montantFinal.toFixed(2))) + .input('date', sql.Date, new Date(date)) + .input('categorie', sql.NVarChar, categorieNote) + .input('description', sql.NVarChar, description || null) + .input('fichiers', sql.NVarChar, JSON.stringify(fichiersExistants)) + .input('statut', sql.NVarChar, 'enattente') + .input('km', sql.Decimal, kmTotal || null) + .input('indemniteKm', sql.Decimal, indemKm || null) + .input('lignesJson', sql.NVarChar, JSON.stringify(lignesParsed)) + .query(` + UPDATE NoteDeFrais SET + libelle = @libelle, montant = @montant, date = @date, + categorie = @categorie, description = @description, + fichiers = @fichiers, statut = @statut, + km = @km, indemniteKm = @indemniteKm, + lignesJson = @lignesJson, + motifRefus = NULL, commentaireN1 = NULL, commentaireN2 = NULL, + dateValidationN1 = NULL, dateValidationN2 = NULL, + DateModification = GETDATE() + WHERE id = @id + `); + + await pool.request() + .input('noteId', sql.Int, noteId) + .query(`DELETE FROM LigneNoteDeFrais WHERE noteDeFraisId = @noteId`); + + const lignesPDF = preparerLignesPDF(lignesParsed, tarifKmVal); + for (let i = 0; i < lignesParsed.length; i++) { + const l = lignesParsed[i]; + const pdf = lignesPDF[i] || {}; + const isKmLine = (l.categorie || '').toLowerCase().includes('kilom'); + const kmLine = parseFloat(l.km) || 0; + const cvLine = parseInt(l.chevaux) || 7; + const indemLine = isKmLine ? getIndemniteKmServer(kmLine, cvLine) : 0; + const items = l.tvaItems || [{ montantTTC: l.montant, taux: l.tauxTVA }]; + const ttcLine = isKmLine ? indemLine : items.reduce((s, item) => s + (parseFloat(item.montantTTC) || 0), 0); + try { + await pool.request() + .input('noteId', sql.Int, noteId) + .input('numPiece', sql.Int, i + 1) + .input('date', sql.Date, new Date(l.date)) + .input('nature', sql.NVarChar, l.categorie || '') + .input('libelle', sql.NVarChar, l.libelle || '') + .input('km', sql.Decimal, isKmLine ? kmLine : null) + .input('montantTTC', sql.Decimal, ttcLine || null) + .input('tva21', sql.Decimal, pdf.tva21 || null) + .input('tva55', sql.Decimal, pdf.tva55 || null) + .input('tva10', sql.Decimal, pdf.tva10 || null) + .input('tva20', sql.Decimal, pdf.tva20 || null) + .input('montantHT', sql.Decimal, pdf.montantHT || null) + .input('tauxTVA', sql.Decimal, parseFloat(l.tauxTVA) || null) + .input('indemniteKm', sql.Decimal, indemLine || null) + .query(` + INSERT INTO LigneNoteDeFrais + (noteDeFraisId, numPiece, date, nature, libelle, + km, montantTTC, tva21, tva55, tva10, tva20, + montantHT, tauxTVA, indemniteKm) + VALUES + (@noteId, @numPiece, @date, @nature, @libelle, + @km, @montantTTC, @tva21, @tva55, @tva10, @tva20, + @montantHT, @tauxTVA, @indemniteKm) + `); + } catch (e) { console.error(`❌ Insertion ligne modif ${i + 1}:`, e.message); } + } + + const hierarchie = await pool.request() + .input('collabId', sql.Int, userId) + .query(` + SELECT h.SuperieurId, s1.email AS emailN1, s1.prenom AS prenomN1, s1.nom AS nomN1 + FROM HierarchieValidationNDF h + LEFT JOIN CollaborateurAD s1 ON s1.id = h.SuperieurId + WHERE h.CollaborateurId = @collabId + `); + const n1 = hierarchie.recordset[0]; + if (n1?.SuperieurId && n1?.emailN1) { + try { + await creerNotification({ + destinataireId: n1.SuperieurId, destinataireEmail: n1.emailN1, + type: 'validation', + titre: `📋 Note modifiée à valider — ${reference}`, + message: `${collaborateur.prenom} ${collaborateur.nom} a modifié et resoumis la note ${reference} (${parseFloat(montantFinal.toFixed(2))} €).`, + noteId + }); + const frontendUrl = process.env.FRONTEND_URL || 'myndf.ensup-adm.net'; + await sendMailGraph(n1.emailN1, `📋 Note modifiée à valider — ${reference}`, + `
+
+

📋 Note modifiée — à valider

+
+
+

Bonjour ${n1.prenomN1} ${n1.nomN1},

+

${collaborateur.prenom} ${collaborateur.nom} a modifié et resoumis la note ${reference} — ${libelle} (${parseFloat(montantFinal.toFixed(2))} €).

+
+ Valider sur la plateforme → +
+
+
` + ); + } catch (e) { console.error('❌ Notif N1 modif:', e.message); } + } + + console.log(`✅ Note ${reference} modifiée par ${collaborateur.email}`); + res.json({ success: true, id: noteId, reference, statut: 'enattente', isCorrection: false }); + + } catch (error) { + console.error('❌ Erreur PUT /api/notes/:id:', error.message); + res.status(500).json({ error: error.message }); + } +}); +// ================================================ +// GET /api/notes +// ================================================ +app.get('/api/notes', authenticateToken, async (req, res) => { + try { + const { statut, mois, annee } = req.query; + const request = pool.request() + .input('collaborateurId', sql.Int, req.user.id); + + let where = 'WHERE n.collaborateurId = @collaborateurId'; + if (statut) { + request.input('statut', sql.NVarChar, statut); + where += ' AND n.statut = @statut'; + } + if (mois && annee) { + request.input('mois', sql.Int, parseInt(mois)); + request.input('annee', sql.Int, parseInt(annee)); + where += ' AND MONTH(n.date) = @mois AND YEAR(n.date) = @annee'; + } + + const result = await request.query(` + SELECT n.*, + v1.nom + ' ' + v1.prenom as nomValidateurN1, + v2.nom + ' ' + v2.prenom as nomValidateurN2 + FROM NoteDeFrais n + LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id + LEFT JOIN CollaborateurAD v2 ON v2.id = n.validateurN2Id + ${where} + ORDER BY n.DateCreation DESC + `); + + const notes = result.recordset; + for (const note of notes) { + // ✅ Parser fichiers → sharepointFiles pour le frontend + if (note.fichiers) { + try { note.sharepointFiles = JSON.parse(note.fichiers); } catch { note.sharepointFiles = []; } + } else { note.sharepointFiles = []; } + if (note.statut === 'non_conforme_verif') { + try { + const ncResult = await pool.request() + .input('noteId', sql.Int, note.id) + .query(` + SELECT fileName, motif, dateSignalement + FROM JustificatifsNonConformes + WHERE noteDeFraisId = @noteId + ORDER BY dateSignalement DESC + `); + note.nonConformes = ncResult.recordset; + } catch (e) { note.nonConformes = []; } + } + // ✅ Toujours re-parser lignesJson depuis la BDD pour avoir les qrFiles à jour + // Ne reconstruire depuis LigneNoteDeFrais qu'en dernier recours + // ✅ Enrichir chaque ligne avec ses fichiers QR depuis UploadTokens + if (note.lignesJson) { + try { + const lignes = JSON.parse(note.lignesJson); + let enrichi = false; + + const lignesEnrichies = await Promise.all(lignes.map(async (l) => { + if (l.qrFiles && l.qrFiles.length > 0) return l; + + const qrRef = l.qrNoteRef || ''; + if (!qrRef) return l; + + try { + const qrResult = await pool.request() + .input('noteRef', sql.NVarChar, qrRef) + .query(`SELECT TOP 1 fichiers FROM UploadTokens + WHERE noteRef = @noteRef AND used = 1 + ORDER BY expiresAt DESC`); + + if (qrResult.recordset.length && qrResult.recordset[0].fichiers) { + const fichiers = JSON.parse(qrResult.recordset[0].fichiers); + if (fichiers.length > 0) { + enrichi = true; + return { ...l, qrFiles: fichiers }; + } + } + } catch (e) { } + return l; + })); + + if (enrichi) { + note.lignesJson = JSON.stringify(lignesEnrichies); + } + } catch (e) { + console.warn('⚠️ Enrichissement QR fail:', note.id, e.message); + } + } + } + + res.json(notes); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ================================================ +// 🔑 TOKEN SHAREPOINT (scope différent de Graph) +// ================================================ +async function getSharePointToken() { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: AZURE_CONFIG.clientId, + client_secret: AZURE_CONFIG.clientSecret, + scope: 'https://ensup.sharepoint.com/.default' // ← scope SharePoint + }); + + const response = await axios.post( + `https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`, + params.toString(), + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ); + + return response.data.access_token; + } catch (error) { + console.error('❌ Erreur token SharePoint:', error.response?.data || error.message); + return null; + } +} + +// routes/sharepoint.js +// ✅ route proxy-file — force les bons headers +app.get('/api/proxy-pdf', async (req, res) => { + let url = req.query.url; + if (!url) return res.status(400).send('URL manquante'); + try { url = decodeURIComponent(url); } catch { } + if (url.includes('/api/proxy-pdf?url=')) { + url = url.split('/api/proxy-pdf?url=')[1]; + try { url = decodeURIComponent(url); } catch { } + } + if (!url.startsWith('http')) return res.status(400).send('URL invalide : ' + url); + + // ✅ Headers cache navigateur + res.setHeader('Cache-Control', 'private, max-age=600'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Content-Disposition', 'inline'); + + // ✅ Vérifier cache serveur + const cached = getCached(url); + if (cached) { + res.setHeader('Content-Type', cached.contentType); + res.setHeader('X-Cache', 'HIT'); + return res.send(cached.buffer); + } + + try { + const buffer = await downloadFromSharePoint(url); + const urlLower = url.toLowerCase(); + let contentType = 'application/octet-stream'; + if (urlLower.includes('.pdf')) contentType = 'application/pdf'; + else if (urlLower.includes('.jpg') || urlLower.includes('.jpeg')) contentType = 'image/jpeg'; + else if (urlLower.includes('.png')) contentType = 'image/png'; + + setCache(url, buffer, contentType); + res.setHeader('Content-Type', contentType); + res.setHeader('X-Cache', 'MISS'); + res.send(buffer); + } catch (err) { + console.error('❌ proxy-pdf erreur:', err.message); + res.status(500).json({ error: err.message, url }); + } +}); + + +// ================================================ +// GET /api/notes/pending +// ================================================ +app.get('/api/notes/pending', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('userId', sql.Int, req.user.id) + .query(` + SELECT n.id, n.reference, n.libelle, n.montant, n.date, n.categorie, n.statut, + n.description, n.participants, n.nombreParticipants, + n.montantHT, n.tauxTVA, n.km, n.indemniteKm, + n.commentaireN1, n.commentaireN2, n.sharepointUrl, n.fichiers, + n.lignesJson, + n.noteRefuseeId, + ancienne.reference AS ancienneReference, + ancienne.motifRefus AS ancienMotifRefus, + c.prenom + ' ' + c.nom AS collaborateur, c.departement, c.campus + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + LEFT JOIN NoteDeFrais ancienne ON ancienne.id = n.noteRefuseeId + WHERE (n.validateurN1Id = @userId AND n.statut = 'enattente') + OR (n.validateurN2Id = @userId AND n.statut = 'validen1') + ORDER BY n.date DESC + `); + + // Pour chaque note, récupérer les lignes depuis LigneNoteDeFrais + // si lignesJson est vide + const notes = result.recordset; + for (const note of notes) { + // ✅ Parser fichiers → sharepointFiles pour le frontend + if (note.fichiers) { + try { note.sharepointFiles = JSON.parse(note.fichiers); } catch { note.sharepointFiles = []; } + } else { note.sharepointFiles = []; } + + // ✅ Toujours re-parser lignesJson depuis la BDD pour avoir les qrFiles à jour + // Ne reconstruire depuis LigneNoteDeFrais qu'en dernier recours + // ✅ Enrichir chaque ligne avec ses fichiers QR depuis UploadTokens + if (note.lignesJson) { + try { + const lignes = JSON.parse(note.lignesJson); + let enrichi = false; + + const lignesEnrichies = await Promise.all(lignes.map(async (l) => { + if (l.qrFiles && l.qrFiles.length > 0) return l; + + const qrRef = l.qrNoteRef || ''; + if (!qrRef) return l; + + try { + const qrResult = await pool.request() + .input('noteRef', sql.NVarChar, qrRef) + .query(`SELECT TOP 1 fichiers FROM UploadTokens + WHERE noteRef = @noteRef AND used = 1 + ORDER BY expiresAt DESC`); + + if (qrResult.recordset.length && qrResult.recordset[0].fichiers) { + const fichiers = JSON.parse(qrResult.recordset[0].fichiers); + if (fichiers.length > 0) { + enrichi = true; + return { ...l, qrFiles: fichiers }; + } + } + } catch (e) { } + return l; + })); + + if (enrichi) { + note.lignesJson = JSON.stringify(lignesEnrichies); + } + } catch (e) { + console.warn('⚠️ Enrichissement QR fail:', note.id, e.message); + } + } + } + + res.json(notes); + } catch (e) { + console.error('Erreur /api/notes/pending:', e.message); + res.status(500).json({ error: e.message }); + } +}); + +// ================================================ +// GET /api/notes/:id/lignes +// ================================================ +app.get('/api/notes/:id/lignes', authenticateToken, async (req, res) => { + try { + const isValidator = hasAnyRole(req.user, 'Finance', 'Validateur', 'Validatrice', 'superUtilisateur') ? 1 : 0; + const result = await pool.request() + .input('noteId', sql.Int, req.params.id) + .input('userId', sql.Int, req.user.id) + .input('isVal', sql.Int, isValidator) + .query(` + SELECT l.* + FROM LigneNoteDeFrais l + JOIN NoteDeFrais n ON n.id = l.noteDeFraisId + WHERE l.noteDeFraisId = @noteId + AND (n.collaborateurId = @userId OR @isVal = 1) + ORDER BY l.numPiece ASC + `); + res.json(result.recordset); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/notes/:id/detail — récupère une note par ID (pour validateur + collaborateur) +app.get('/api/notes/:id/detail', authenticateToken, async (req, res) => { + try { + const noteId = parseInt(req.params.id); + const userId = req.user.id; + + const result = await pool.request() + .input('id', sql.Int, noteId) + .query(` + SELECT n.*, + c.prenom + ' ' + c.nom AS collaborateur, + c.departement, c.campus, + ancienne.reference AS ancienneReference, + ancienne.motifRefus AS ancienMotifRefus + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + LEFT JOIN NoteDeFrais ancienne ON ancienne.id = n.noteRefuseeId + WHERE n.id = @id + AND ( + n.collaborateurId = ${userId} + OR n.validateurN1Id = ${userId} + OR n.validateurN2Id = ${userId} + OR EXISTS ( + SELECT 1 FROM UtilisateurRoles r + WHERE r.collaborateur_id = ${userId} + AND r.role IN ('Finance','superUtilisateur','VerificateurFinance','ValidateurFinance') + AND r.actif = 1 + ) + ) + `); + + if (!result.recordset.length) + return res.status(404).json({ error: 'Note introuvable ou accès refusé' }); + + const note = result.recordset[0]; + if (note.fichiers) { + try { note.sharepointFiles = JSON.parse(note.fichiers); } catch { note.sharepointFiles = []; } + } else { note.sharepointFiles = []; } + + res.json(note); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ================================================ +// PUT /api/notes/:id/statut — Valider ou refuser +// ================================================ +// PUT /api/notes/:id/statut — Valider ou refuser +app.put('/api/notes/:id/statut', authenticateToken, async (req, res) => { + try { + const { id } = req.params; + const { action, commentaire, motifRefus } = req.body; + const userId = Number(req.user.id); + console.log('Validation demande', id, action, userId); + + const noteResult = await pool.request() + .input('id', sql.Int, id) + .query('SELECT * FROM NoteDeFrais WHERE id = @id'); + if (!noteResult.recordset.length) + return res.status(404).json({ error: 'Note non trouvée' }); + + const note = noteResult.recordset[0]; + const n1Id = Number(note.validateurN1Id); + const n2Id = Number(note.validateurN2Id); + const statutNote = note.statut?.trim(); + + let nouveauStatut = null, niveauValidation = null; + + if (n1Id === userId && statutNote === 'enattente') { + niveauValidation = 'N1'; + nouveauStatut = action === 'valider' + ? (note.validateurN2Id && n2Id !== userId ? 'validen1' : 'approuve') + : 'refuse'; + } else if (n2Id === userId && statutNote === 'validen1') { + niveauValidation = 'N2'; + nouveauStatut = action === 'valider' ? 'approuve' : 'refuse'; + } else { + return res.status(403).json({ error: 'Non autorisé à valider cette note' }); + } + + const dateField = niveauValidation === 'N1' ? 'dateValidationN1' : 'dateValidationN2'; + const commentaireField = niveauValidation === 'N1' ? 'commentaireN1' : 'commentaireN2'; + + await pool.request() + .input('id', sql.Int, id) + .input('statut', sql.NVarChar, nouveauStatut) + .input('commentaire', sql.NVarChar, commentaire ?? null) + .input('motifRefus', sql.NVarChar, motifRefus ?? null) + .query(` + UPDATE NoteDeFrais + SET statut = @statut, + ${dateField} = GETDATE(), + ${commentaireField} = @commentaire, + motifRefus = CASE WHEN @motifRefus IS NOT NULL THEN @motifRefus ELSE motifRefus END, + DateModification = GETDATE() + WHERE id = @id + `); + + await pool.request() + .input('noteId', sql.Int, id) + .input('validateurId', sql.Int, userId) + .input('niveau', sql.NVarChar, niveauValidation) + .input('action', sql.NVarChar, action) + .input('commentaire', sql.NVarChar, commentaire ?? null) + .input('motifRefus', sql.NVarChar, motifRefus ?? null) + .input('statut', sql.NVarChar, nouveauStatut) + .query(` + INSERT INTO HistoriqueValidation + (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, MotifRefus, NouveauStatut, DateAction) + VALUES + (@noteId, @validateurId, @niveau, @action, @commentaire, @motifRefus, @statut, GETDATE()) + `); + + // Génération PDF signé + let signedPdfUrl = null; + try { + const validateurSelfResult = await pool.request() + .input('id', sql.Int, userId) + .query('SELECT prenom, nom FROM CollaborateurAD WHERE id = @id'); + const validateurSelf = validateurSelfResult.recordset[0]; + const nomValidateurActuel = (validateurSelf + ? `${validateurSelf.prenom} ${validateurSelf.nom}` + : `${req.user.prenom} ${req.user.nom}`).trim(); + + const noteComplete = await pool.request() + .input('id', sql.Int, id) + .query(` + SELECT n.reference, n.libelle, n.montant, n.date, n.categorie, + n.montantHT, n.tauxTVA, n.km, n.participants, n.description, + n.fichiers, n.DateCreation, n.collaborateurId, n.lignesJson, + n.commentaireN1, n.commentaireN2, n.dateValidationN1, n.dateValidationN2, + c.prenom + ' ' + c.nom AS nomPrenom, + c.prenom AS collabPrenom, c.nom AS collabNom, c.departement, + v1.prenom + ' ' + v1.nom AS nomValidateurN1, + v2.prenom + ' ' + v2.nom AS nomValidateurN2 + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id + LEFT JOIN CollaborateurAD v2 ON v2.id = n.validateurN2Id + WHERE n.id = @id + `); + + if (noteComplete.recordset.length) { + const nd = noteComplete.recordset[0]; + const signatures = [ + { niveau: 'COLLAB', nomPrenom: nd.nomPrenom, date: nd.DateCreation, action: 'soumettre', commentaire: null } + ]; + if (niveauValidation === 'N1') { + signatures.push({ niveau: 'N1', nomPrenom: nomValidateurActuel, date: new Date(), action, commentaire: commentaire ?? null }); + } else if (niveauValidation === 'N2') { + if (nd.nomValidateurN1 && nd.dateValidationN1) + signatures.push({ niveau: 'N1', nomPrenom: nd.nomValidateurN1, date: nd.dateValidationN1, action: 'valider', commentaire: nd.commentaireN1 ?? null }); + signatures.push({ niveau: 'N2', nomPrenom: nomValidateurActuel, date: new Date(), action, commentaire: commentaire ?? null }); + } + + const moisStr = (() => { + if (!nd.date) return ''; + const d = new Date(nd.date); + const m = d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); + return m.charAt(0).toUpperCase() + m.slice(1); + })(); + + const noteDataPDF = { + reference: nd.reference, nomPrenom: nd.nomPrenom, mois: moisStr, + departement: nd.departement, lignesJson: nd.lignesJson, + tarifKm: await getTarifKm(), statut: nouveauStatut + }; + + const pdfSigne = await generateFicheSignee(noteDataPDF, signatures); + const suffixe = nouveauStatut === 'approuve' ? 'signe-approuve' + : nouveauStatut === 'refuse' ? 'signe-refuse' : `signe-${nouveauStatut}`; + + let fichiersExistants = []; + try { fichiersExistants = JSON.parse(nd.fichiers); } catch { } + + const existingFolder = fichiersExistants[0]?.folderPath; + const nomDossier = existingFolder + ? existingFolder.split('/')[1] + : `${nd.collabPrenom}${nd.collabNom}`.replace(/[^a-zA-Z0-9]/g, ''); + const moisDossier = existingFolder + ? existingFolder.split('/')[2] + : `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + + const signedResult = await uploadToSharePointHierarchique( + { buffer: pdfSigne, originalname: `${nd.reference}-${suffixe}.pdf`, mimetype: 'application/pdf', size: pdfSigne.length }, + nd.reference, nomDossier, moisDossier + ); + fichiersExistants.push(signedResult); + + await pool.request() + .input('id', sql.Int, id) + .input('fichiers', sql.NVarChar, JSON.stringify(fichiersExistants)) + .query('UPDATE NoteDeFrais SET fichiers = @fichiers WHERE id = @id'); + + signedPdfUrl = signedResult.uploadUrl; + console.log('PDF signé uploadé:', signedResult.fileName, suffixe); + } + } catch (pdfError) { + console.error('Erreur génération PDF signé:', pdfError.message); + } + + // Notifications collaborateur + validateur suivant + const frontendUrl = process.env.FRONTEND_URL || 'myndf.ensup-adm.net'; + const montantFormate = parseFloat(note.montant).toFixed(2); + const collabResult = await pool.request().input('id', sql.Int, note.collaborateurId).query('SELECT id, email, prenom, nom FROM CollaborateurAD WHERE id = @id'); + const validateurResult = await pool.request().input('id', sql.Int, userId).query('SELECT id, email, prenom, nom FROM CollaborateurAD WHERE id = @id'); + + if (collabResult.recordset.length) { + const c = collabResult.recordset[0]; + const v = validateurResult.recordset[0]; + const isApprouve = nouveauStatut === 'approuve'; + const isValidn1 = nouveauStatut === 'validen1'; + const isRefus = nouveauStatut === 'refuse'; + const titreCollab = isApprouve ? `Note ${note.reference} approuvée` : isValidn1 ? `Note ${note.reference} validée N1` : `Note ${note.reference} refusée`; + const msgCollab = isApprouve + ? `Votre note ${note.reference} (${montantFormate}€) a été approuvée.` + : isValidn1 + ? `Votre note ${note.reference} a été validée N1 par ${v?.prenom} ${v?.nom}.` + : `Votre note ${note.reference} a été refusée. Motif : ${motifRefus || commentaire || 'Non précisé'}`; + + try { await creerNotification({ destinataireId: c.id, destinataireEmail: c.email, type: isRefus ? 'refus' : 'validation', titre: titreCollab, message: msgCollab, noteId: parseInt(id) }); } catch { } + + // ── Email collaborateur ────────────────────────────────────────── + try { + const motifAffiche = motifRefus || commentaire || 'Non précisé'; + const nomValidateur = `${v?.prenom || ''} ${v?.nom || ''}`.trim(); + + await sendMailGraph( + c.email, + isRefus + ? `❌ Note refusée — action requise : ${note.reference}` + : titreCollab, + isRefus + ? `
+
+

❌ Votre note de frais a été refusée

+

Une action de votre part est nécessaire

+
+
+

Bonjour ${c.prenom} ${c.nom},

+

Votre note ${note.reference} a été refusée par ${nomValidateur}.

+ +
+
Motif du refus
+
${motifAffiche}
+
+ +
+ + + + + + +
Référence${note.reference}
Libellé${note.libelle}
Montant${montantFormate} €
Refusé par${nomValidateur}
Date${new Date().toLocaleDateString('fr-FR', { day: '2-digit', month: 'long', year: 'numeric' })}
+
+ +
+
📝 Que faire maintenant ?
+
    +
  1. Connectez-vous à la plateforme NDF
  2. +
  3. Rendez-vous dans Mes notes
  4. +
  5. Cliquez sur la note ${note.reference}
  6. +
  7. Corrigez les informations demandées
  8. +
  9. Resoumettez la note
  10. +
+
+ +
+ + ✏️ Modifier ma note → + +
+

+ Vous pouvez modifier votre note tant qu'elle est au statut "Refusée". +

+
+
` + : `
+
+

${titreCollab}

+
+
+

Bonjour ${c.prenom} ${c.nom},

+

${msgCollab}

+
+ Voir mes notes +
+
+
` + ); + } catch { } + + // Notifier N2 si validation N1 + if (action === 'valider' && niveauValidation === 'N1' && note.validateurN2Id && n2Id !== userId) { + const n2Result = await pool.request().input('id', sql.Int, note.validateurN2Id).query('SELECT id, email, prenom, nom FROM CollaborateurAD WHERE id = @id'); + if (n2Result.recordset.length) { + const n2 = n2Result.recordset[0]; + try { await creerNotification({ destinataireId: n2.id, destinataireEmail: n2.email, type: 'validation', titre: `Note à valider N2 : ${note.reference}`, message: `La note ${note.reference} (${montantFormate}€) de ${c.prenom} ${c.nom} attend votre validation finale.`, noteId: parseInt(id) }); } catch { } + try { + await sendMailGraph(n2.email, `Note à valider N2 : ${note.reference}`, ` +
+
+

Note à valider — Niveau N2

+
+
+

Bonjour ${n2.prenom} ${n2.nom},

+

La note ${note.reference} de ${c.prenom} ${c.nom} (${montantFormate}€) a été validée N1 et attend votre validation finale.

+
+ Valider sur la plateforme +
+
+
`); + } catch { } + } + } + } + + res.json({ success: true, statut: nouveauStatut, niveau: niveauValidation }); + } catch (error) { + console.error('Erreur validation:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// ================================================ +// GET /api/notes/:id/historique +// ================================================ +app.get('/api/notes/:id/historique', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('noteId', sql.Int, req.params.id) + .query(` + SELECT h.id, h.Niveau, h.Action, h.Commentaire, h.MotifRefus, + h.NouveauStatut, h.DateAction, + v.prenom + ' ' + v.nom AS validateur, v.role AS roleValidateur + FROM HistoriqueValidation h + JOIN CollaborateurAD v ON v.id = h.ValidateurId + WHERE h.NoteDeFraisId = @noteId ORDER BY h.DateAction ASC + `); + res.json(result.recordset); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +// ================================================ +// GET /api/validateur/historique +// ================================================ +app.get('/api/validateur/historique', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('validateurId', sql.Int, req.user.id) + .query(` + SELECT h.id, h.Niveau, h.Action, h.Commentaire, h.MotifRefus, + h.NouveauStatut, h.DateAction, + n.reference, n.libelle, n.montant, n.categorie, + c.prenom + ' ' + c.nom AS collaborateur, c.departement, c.campus + FROM HistoriqueValidation h + JOIN NoteDeFrais n ON n.id = h.NoteDeFraisId + JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE h.ValidateurId = @validateurId ORDER BY h.DateAction DESC + `); + res.json(result.recordset); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +// ================================================ +// GET /api/notes/all — réservé Finance +// ================================================ +app.get('/api/notes/all', authenticateToken, async (req, res) => { + try { + if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ error: 'Accès réservé Finance' }); + const result = await pool.request().query(` + SELECT n.*, c.nom + ' ' + c.prenom as collaborateur, c.departement, c.campus, + v1.nom + ' ' + v1.prenom as nomN1, v2.nom + ' ' + v2.prenom as nomN2 + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id + LEFT JOIN CollaborateurAD v2 ON v2.id = n.validateurN2Id + ORDER BY n.DateCreation DESC + `); + res.json(result.recordset); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +// ================================================ +// GET /api/admin/notes — réservé superUtilisateur +// ================================================ +// GET /api/admin/notes — superUtilisateur, filtré sur ENSUP SOLUTION ET SUPPORT +app.get('/api/admin/notes', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé superUtilisateur' }); + + try { + const { mois, annee, statut } = req.query; + let query = ` + SELECT n.id, n.reference, n.libelle, n.montant, n.date, n.categorie, n.statut, + n.DateCreation, n.montantHT, n.tauxTVA, n.km, n.indemniteKm, + n.sharepointUrl, n.fichiers, n.lignesJson, + c.prenom + ' ' + c.nom AS collaborateur, c.email AS collaborateurEmail, + c.departement, c.campus, c.societe + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE c.societe = 'ENSUP SOLUTION ET SUPPORT' + `; + const req2 = pool.request(); + if (mois && annee) { + query += ` AND MONTH(n.date) = @mois AND YEAR(n.date) = @annee`; + req2.input('mois', sql.Int, parseInt(mois)); + req2.input('annee', sql.Int, parseInt(annee)); + } + if (statut) { + query += ` AND n.statut = @statut`; + req2.input('statut', sql.NVarChar, statut); + } + query += ` ORDER BY c.nom, n.date DESC`; + const result = await req2.query(query); + res.json(result.recordset); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ================================================ +// PAIEMENTS +// ================================================ +app.get('/api/paiements/prochain', authenticateToken, async (req, res) => { + try { + const config = await pool.request().query(`SELECT TOP 1 JourPaiement FROM ConfigPaiement WHERE Actif = 1 ORDER BY Id DESC`); + const jourPaiement = config.recordset[0]?.JourPaiement ?? 20; + const today = new Date(); + const jour = today.getDate(), mois = today.getMonth(), annee = today.getFullYear(); + const datePaiement = jour <= jourPaiement ? new Date(annee, mois, jourPaiement) : new Date(annee, mois + 1, jourPaiement); + res.json({ jourPaiement, datePaiement: datePaiement.toISOString().split('T')[0], libelle: `Paiement le ${datePaiement.toLocaleDateString('fr-FR')}` }); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +app.post('/api/paiements/marquer-payees', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ error: 'Accès réservé Finance' }); + try { + const config = await pool.request().query(`SELECT TOP 1 JourPaiement FROM ConfigPaiement WHERE Actif = 1 ORDER BY Id DESC`); + const jourPaiement = config.recordset[0]?.JourPaiement ?? 20; + const today = new Date(); + const mois = today.getMonth() + 1, annee = today.getFullYear(); + const datePaiementExacte = new Date(annee, today.getMonth(), jourPaiement); + + const notes = await pool.request().query(` + SELECT n.id, n.reference, n.montant, c.nom + ' ' + c.prenom AS collaborateur, c.email + FROM NoteDeFrais n JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE n.statut = 'approuve' AND n.datePaiement IS NULL + `); + + if (!notes.recordset.length) return res.json({ success: true, message: 'Aucune note approuvée à payer', count: 0 }); + + await pool.request() + .input('datePaiement', sql.DateTime, datePaiementExacte) + .input('moisPaiement', sql.Int, mois) + .input('anneePaiement', sql.Int, annee) + .query(` + UPDATE NoteDeFrais SET statut = 'payee', datePaiement = @datePaiement, + moisPaiement = @moisPaiement, anneePaiement = @anneePaiement, DateModification = GETDATE() + WHERE statut = 'approuve' AND datePaiement IS NULL + `); + + res.json({ success: true, count: notes.recordset.length, datePaiement: datePaiementExacte.toISOString().split('T')[0], notes: notes.recordset }); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +app.get('/api/paiements/historique', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'superUtilisateur', 'ValidateurFinance', 'VerificateurFinance')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + + try { + const mois = req.query.mois ? parseInt(req.query.mois) : null; + const annee = req.query.annee ? parseInt(req.query.annee) : null; + + const request = pool.request(); + request.input('mois', sql.Int, mois); + request.input('annee', sql.Int, annee); + + let campusFilter = ''; + if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) { + const campusCode = normalizeCampus(req.user.campus); + if (campusCode) { + request.input('campus', sql.NVarChar, `%${campusCode}%`); + campusFilter = 'AND c.campus LIKE @campus'; + } + } + + const result = await request.query(` + SELECT + n.id, n.reference, n.libelle, n.montant, + n.datePaiement, n.moisPaiement, n.anneePaiement, + n.categorie, n.lignesJson, n.indemniteKm, n.km, + n.statut, + c.nom + ' ' + c.prenom AS collaborateur, + c.campus, + c.societe + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE n.statut = 'payee' + AND (@mois IS NULL OR n.moisPaiement = @mois) + AND (@annee IS NULL OR n.anneePaiement = @annee) + ${campusFilter} + ORDER BY n.datePaiement DESC, c.nom +`); + + const data = result.recordset.map(row => { + let montantRepas = 0, montantHebergement = 0; + let montantKilometrique = row.indemniteKm || 0; + let montantTransport = 0, montantAutres = 0; + + const cat = (row.categorie || '').toLowerCase(); + if (cat !== 'multiple') { + if (cat.includes('repas') || cat.includes('restaurant')) montantRepas = row.montant; + else if (cat.includes('hebergement') || cat.includes('hotel')) montantHebergement = row.montant; + else if (cat.includes('kilom') || cat.includes('km')) montantKilometrique = row.indemniteKm || row.montant; + else if (cat.includes('transport') || cat.includes('avion') || cat.includes('train') || cat.includes('taxi')) montantTransport = row.montant; + else montantAutres = row.montant; + } else { + try { + const lignes = JSON.parse(row.lignesJson || '[]'); + lignes.forEach(ligne => { + const lcat = (ligne.categorie || ligne.nature || '').toLowerCase(); + const montantLigne = parseFloat(ligne.montantTTC || ligne.montant || 0); + if (lcat.includes('repas') || lcat.includes('restaurant')) montantRepas += montantLigne; + else if (lcat.includes('hebergement') || lcat.includes('hotel')) montantHebergement += montantLigne; + else if (lcat.includes('kilom') || lcat.includes('km')) montantKilometrique += parseFloat(ligne.indemniteKm || montantLigne); + else if (lcat.includes('transport') || lcat.includes('avion') || lcat.includes('train') || lcat.includes('taxi')) montantTransport += montantLigne; + else montantAutres += montantLigne; + }); + } catch { montantAutres = row.montant; } + } + + return { + ...row, + montantRepas: Math.round(montantRepas * 100) / 100, + montantHebergement: Math.round(montantHebergement * 100) / 100, + montantKilometrique: Math.round(montantKilometrique * 100) / 100, + montantTransport: Math.round(montantTransport * 100) / 100, + montantAutres: Math.round(montantAutres * 100) / 100, + }; + }); + + res.json(data); + } catch (error) { + console.error('Erreur historique:', error.message); + res.status(500).json({ error: error.message }); + } +}); +app.get('/api/paiements/config', authenticateToken, async (req, res) => { + try { + const result = await pool.request().query(`SELECT TOP 1 Id, JourPaiement, Actif, DateModif FROM ConfigPaiement WHERE Actif = 1 ORDER BY Id DESC`); + res.json(result.recordset[0] ?? { JourPaiement: 20 }); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +app.put('/api/paiements/config', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ error: 'Accès réservé Finance' }); + try { + const { jourPaiement } = req.body; + if (!jourPaiement || jourPaiement < 1 || jourPaiement > 28) return res.status(400).json({ error: 'Jour de paiement invalide (1-28)' }); + await pool.request().query(`UPDATE ConfigPaiement SET Actif = 0 WHERE Actif = 1`); + await pool.request().input('jour', sql.Int, jourPaiement).query(`INSERT INTO ConfigPaiement (JourPaiement, Actif, DateModif) VALUES (@jour, 1, GETDATE())`); + res.json({ success: true, jourPaiement }); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +// ================================================ +// HELPERS EMAIL + NOTIFICATION +// ================================================ +async function sendMailGraph(to, subject, htmlBody) { + try { + const accessToken = await getGraphToken(); + if (!accessToken) throw new Error('Token Graph indisponible'); + const senderEmail = process.env.MAIL_SENDER || process.env.MAIL_FROM; + if (!senderEmail) { console.error('MAIL_SENDER non défini'); return; } + + await axios.post( + `https://graph.microsoft.com/v1.0/users/${senderEmail}/sendMail`, + { message: { subject, body: { contentType: 'HTML', content: htmlBody }, from: { emailAddress: { address: senderEmail } }, toRecipients: [{ emailAddress: { address: to } }] }, saveToSentItems: false }, + { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } } + ); + console.log(`✅ Email envoyé à ${to} via ${senderEmail}`); + } catch (error) { + console.error('Erreur sendMailGraph:', error.response?.data || error.message); + } +} + +async function creerNotification({ destinataireId, destinataireEmail, type, titre, message, noteId }) { + if (!destinataireId) { + console.error('❌ creerNotification annulée : destinataireId manquant', { destinataireEmail, type, titre }); + return; + } + try { + await pool.request() + .input('destinataireId', sql.Int, destinataireId) + .input('type', sql.NVarChar, type) + .input('titre', sql.NVarChar, titre) + .input('message', sql.NVarChar, message) + .input('noteId', sql.Int, noteId || null) + .query(` + INSERT INTO Notifications (CollaborateurId, Type, Titre, Message, NoteDeFraisId, Lu, DateCreation) + VALUES (@destinataireId, @type, @titre, @message, @noteId, 0, GETDATE()) + `); + console.log(`🔔 Notification insérée pour ${destinataireEmail} (id: ${destinataireId})`); + } catch (err) { console.error('❌ Erreur insertion notification:', err.message); } +} + +// ================================================ +// PAIEMENTS — NOTIFIER +// ================================================ +app.post('/api/paiements/notifier', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance')) return res.status(403).json({ error: 'Accès réservé Finance' }); + try { + const notes = await pool.request().query(` + SELECT n.id, n.reference, n.montant, n.libelle, c.nom + ' ' + c.prenom AS collaborateur + FROM NoteDeFrais n JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE n.statut = 'approuve' AND n.datePaiement IS NULL + `); + if (!notes.recordset.length) return res.json({ success: true, message: 'Aucune note en attente de paiement' }); + + const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0); + const alexandre = await pool.request() + .input('email', sql.NVarChar, process.env.RESPONSABLE_PAIEMENT_EMAIL) + .query(`SELECT TOP 1 id, email, prenom, nom FROM CollaborateurAD WHERE email = @email AND Actif = 1`); + + if (!alexandre.recordset.length) return res.status(404).json({ error: 'Responsable paiement introuvable' }); + const responsable = alexandre.recordset[0]; + + const config = await pool.request().query(`SELECT TOP 1 JourPaiement FROM ConfigPaiement WHERE Actif = 1 ORDER BY Id DESC`); + const jourPaiement = config.recordset[0]?.JourPaiement ?? 20; + const titre = `⚠️ ${notes.recordset.length} note(s) à payer — ${total.toFixed(2)} €`; + + await creerNotification({ destinataireId: responsable.id, destinataireEmail: responsable.email, type: 'paiement', titre, message: `${notes.recordset.length} note(s) en attente avant le ${jourPaiement}. Total : ${total.toFixed(2)} €`, noteId: null }); + await sendMailGraph(responsable.email, titre, `

Bonjour ${responsable.prenom},

${notes.recordset.length} note(s) en attente de paiement avant le ${jourPaiement}. Total : ${total.toFixed(2)} €

`); + + res.json({ success: true, count: notes.recordset.length, total: parseFloat(total.toFixed(2)) }); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +// ================================================ +// NOTIFICATIONS +// ================================================ +app.get('/api/notifications', authenticateToken, async (req, res) => { + try { + const result = await pool.request().input('userId', sql.Int, req.user.id).query(` + SELECT TOP 50 id, Type, Titre, Message, NoteDeFraisId, Lu, DateCreation + FROM Notifications WHERE CollaborateurId = @userId ORDER BY DateCreation DESC + `); + res.json(result.recordset); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +app.put('/api/notifications/:id/lu', authenticateToken, async (req, res) => { + try { + await pool.request().input('id', sql.Int, req.params.id).input('userId', sql.Int, req.user.id) + .query(`UPDATE Notifications SET Lu = 1 WHERE id = @id AND CollaborateurId = @userId`); + res.json({ success: true }); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +// ================================================ +// CATÉGORIES / PARAMÈTRES TVA / KM +// ================================================ +app.get('/api/categories', authenticateToken, async (req, res) => { + try { + const result = await pool.request().query(` + SELECT c.*, p.PlafondParPersonne, p.DescriptionPlafond + FROM CategorieNDF c LEFT JOIN PlafondRepas p ON p.CategorieId = c.id AND p.Actif = 1 + WHERE c.Actif = 1 ORDER BY c.Ordre, c.Nom + `); + res.json(result.recordset); + } catch (error) { res.status(500).json({ error: error.message }); } +}); + +app.get('/api/parametres/tva', authenticateToken, async (req, res) => { + try { + const result = await pool.request().query(` + SELECT id, taux, libelle, categorie FROM ParametresTVA + WHERE actif = 1 AND dateDebut <= GETDATE() AND (dateFin IS NULL OR dateFin >= GETDATE()) + ORDER BY taux ASC + `); + res.json(result.recordset); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.get('/api/parametres/km', authenticateToken, async (req, res) => { + try { + const annee = new Date().getFullYear(); + const result = await pool.request().input('annee', sql.Int, annee).query(` + SELECT TOP 1 tarifParKm FROM ParametresKm WHERE annee = @annee AND actif = 1 ORDER BY DateCreation DESC + `); + res.json({ tarifKm: result.recordset[0]?.tarifParKm ?? await getTarifKm() }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// ================================================ +// QR CODE UPLOAD MOBILE +// ================================================ +app.post('/api/upload/generate-link', authenticateToken, async (req, res) => { + try { + const { noteRef } = req.body; + if (!noteRef) return res.status(400).json({ error: 'noteRef obligatoire' }); + + const collabResult = await pool.request().input('id', sql.Int, req.user.id).query('SELECT nom, prenom FROM CollaborateurAD WHERE id = @id'); + if (!collabResult.recordset.length) return res.status(404).json({ error: 'Collaborateur introuvable' }); + + const { nom, prenom } = collabResult.recordset[0]; + const token = crypto.randomBytes(32).toString('hex'); + + await pool.request() + .input('token', sql.VarChar, token) + .input('nomPrenom', sql.NVarChar, `${nom.toUpperCase()}_${prenom}`) + .input('noteRef', sql.NVarChar, noteRef) + .query(`INSERT INTO UploadTokens (token, nomPrenom, noteRef, expiresAt) VALUES (@token, @nomPrenom, @noteRef, DATEADD(MINUTE, 120, GETDATE()))`); + + res.json({ uploadLink: `${process.env.FRONTEND_URL}/upload/${token}`, expiresAt: new Date(Date.now() + 120 * 60 * 1000), token }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +app.get('/api/upload/check/:token', async (req, res) => { + try { + const valid = await pool.request().input('token', sql.VarChar, req.params.token) + .query(`SELECT * FROM UploadTokens WHERE token = @token AND used = 0 AND expiresAt > GETDATE()`); + if (!valid.recordset.length) return res.status(410).json({ error: 'Lien expiré ou déjà utilisé' }); + const t = valid.recordset[0]; + res.json({ valid: true, noteRef: t.noteRef, nomPrenom: t.nomPrenom }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +app.post('/api/upload/submit/:token', upload.array('files', 10), async (req, res) => { + try { + const result = await pool.request().input('token', sql.VarChar, req.params.token) + .query(`SELECT * FROM UploadTokens WHERE token = @token AND used = 0 AND expiresAt > GETDATE()`); + if (!result.recordset.length) return res.status(410).json({ error: 'Lien expiré ou déjà utilisé' }); + + const { nomPrenom, noteRef } = result.recordset[0]; + const files = req.files; + if (!files || !files.length) return res.status(400).json({ error: 'Aucun fichier reçu' }); + + const uploaded = []; + for (const file of files) { + const r = await uploadToSharePoint(file, noteRef, nomPrenom); + uploaded.push(r); + } + + await pool.request() + .input('token', sql.VarChar, req.params.token) + .input('firstUrl', sql.NVarChar, uploaded[0].uploadUrl) + .input('allFiles', sql.NVarChar, JSON.stringify(uploaded)) + .query(`UPDATE UploadTokens SET used = 1, sharepointUrl = @firstUrl, fichiers = @allFiles WHERE token = @token`); + + res.json({ success: true, uploaded }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +app.get('/api/upload/status/:noteRef', async (req, res) => { + try { + const result = await pool.request().input('noteRef', sql.NVarChar, req.params.noteRef) + .query(`SELECT TOP 1 used, fichiers, sharepointUrl FROM UploadTokens WHERE noteRef = @noteRef ORDER BY expiresAt DESC`); + if (!result.recordset.length) return res.json({ uploaded: false }); + const row = result.recordset[0]; + const fichiers = row.fichiers ? JSON.parse(row.fichiers) : []; + res.json({ + uploaded: row.used === true || row.used === 1, + files: fichiers, // ← ajout pour le frontend + fichiers: fichiers, // ← conservé pour rétrocompat + sharepointUrl: row.sharepointUrl + }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + + +// GET /api/notes-all — réservé Finance (filtré par campus) + superUtilisateur (ENSUP SOLUTION ET SUPPORT) + +app.get('/api/notes-all', authenticateToken, async (req, res) => { + try { + if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + + const request = pool.request(); + let campusWhere = ''; + if (req.user.campus) { + const campusCode = normalizeCampus(req.user.campus); + if (campusCode) { + request.input('campus', sql.NVarChar, `%${campusCode}%`); + campusWhere = `AND c.campus LIKE @campus`; + } + } + + const statutFilter = hasAnyRole(req.user, 'ValidateurFinance') && !hasAnyRole(req.user, 'Finance') + ? `AND LOWER(n.statut) IN ('verifie', 'paiementenattente', 'payee')` + : `AND LOWER(REPLACE(n.statut COLLATE Latin1_General_CI_AI, ' ', '')) IN ( + 'approuve', 'approuv', 'verifie', 'paiementenattente', 'paiement_en_attente', 'payee' + )`; + + const result = await request.query(` + SELECT n.*, + + c.nom + ' ' + c.prenom AS collaborateur, + c.departement, c.campus, c.societe, + v1.nom + ' ' + v1.prenom AS nomN1, + v2.nom + ' ' + v2.prenom AS nomN2, + vf.nom + ' ' + vf.prenom AS nomVerificateur, + n.dateVerification, n.commentaireVerification + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id + LEFT JOIN CollaborateurAD v2 ON v2.id = n.validateurN2Id + LEFT JOIN CollaborateurAD vf ON vf.id = n.verificateurFinanceId + WHERE 1=1 ${statutFilter} ${campusWhere} + ORDER BY n.DateCreation DESC + `); + + res.json(result.recordset); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + + +// POST /api/paiements/generer-xml +// Body: { noteIds: number[] } +// POST /api/paiements/generer-xml — Génère le XML PAIN.001 et passe statut à 'paiementenattente' +app.post('/api/paiements/generer-xml', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + + const { noteIds } = req.body; + if (!Array.isArray(noteIds) || noteIds.length === 0) + return res.status(400).json({ error: 'Aucune note sélectionnée' }); + + try { + const idList = noteIds.map(id => parseInt(id)).filter(Boolean).join(','); + + const notes = await pool.request().query(` + SELECT n.id, n.reference, n.montant, n.libelle, n.date, + n.fichiers, n.lignesJson, n.categorie, n.DateCreation, + c.nom, c.prenom, c.iban, c.bic, c.campus, c.societe, + c.adresse_rue, c.adresse_cp, c.adresse_ville, c.adresse_pays, + c.id AS collabId, c.email + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE n.id IN (${idList}) + AND n.statut IN ('approuve', 'approuvé', 'verifie') + `); + + if (!notes.recordset.length) + return res.status(404).json({ error: 'Aucune note approuvée/vérifiée trouvée' }); + + const erreurs = []; + for (const n of notes.recordset) { + const checks = await pool.request() + .input('collabId', sql.Int, n.collabId) + .query(` + SELECT IBAN, BIC, adresse_rue, adresse_cp, adresse_ville, adresse_pays + FROM CollaborateurAD + WHERE id = @collabId + `); + const c = checks.recordset[0]; + if (!c) { erreurs.push(`${n.reference} : collaborateur introuvable`); continue; } + if (!c.IBAN) erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : IBAN manquant`); + if (!c.BIC) erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : BIC manquant`); + if (!c.adresse_rue || !c.adresse_cp || !c.adresse_ville || !c.adresse_pays) + erreurs.push(`${n.reference} (${n.prenom} ${n.nom}) : adresse postale incomplète`); + } + if (erreurs.length > 0) + return res.status(422).json({ + error: 'Données manquantes — XML non généré', + details: erreurs + }); + + const now = new Date(); + const annee = now.getFullYear(); + const mois = String(now.getMonth() + 1).padStart(2, '0'); + const todayISO = now.toISOString().split('T')[0]; + const creDtTm = now.toISOString().slice(0, 19); + const msgId = `NDF-${annee}${mois}-${Date.now().toString().slice(-7)}`; + const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0); + const totalFormate = total.toFixed(2); + + // ── Config débiteur depuis .env ────────────────────────────────── + const cfg = await getConfigDebiteur(); + const dbtrNom = cfg.companyName; + const dbtrIban = cfg.companyIban; + const dbtrBic = cfg.companyBic; + const dbtrAdrLine = cfg.companyAddress; + const dbtrCp = cfg.companyCp; + const dbtrVille = cfg.companyVille; + const dbtrPays = cfg.companyPays; + + // ── Générer les transactions ────────────────────────────────────── + let transactions = ''; + let numTx = 1; + + for (const n of notes.recordset) { + let ibanClair = 'FR0000000000000000000000000'; + try { + if (n.iban && n.iban.includes(':')) ibanClair = decryptIban(n.iban); + else if (n.iban) ibanClair = n.iban; + } catch (e) { + console.warn(`⚠️ Déchiffrement IBAN impossible pour ${n.reference}:`, e.message); + } + + const benefNom = `${n.nom.toUpperCase()} ${n.prenom}`; + const adrLine = (n.adresse_rue || '').toUpperCase(); + const cp = n.adresse_cp || ''; + const ville = (n.adresse_ville || '').toUpperCase(); + const pays = (n.adresse_pays || 'FR').slice(0, 2).toUpperCase(); + + // BIC bénéficiaire : si présent utiliser, sinon NOTPROVIDED + const benefBicBlock = n.bic + ? `${n.bic}` + : `NOTPROVIDED`; + + transactions += ` + + + VIREMENT NUM:${n.reference} + ${n.reference} + + + ${parseFloat(n.montant).toFixed(2)} + + + ${benefBicBlock} + + + ${benefNom}${adrLine || cp || ville ? ` + ${cp ? ` + ${cp}` : ''}${ville ? ` + ${ville}` : ''} + ${pays}${adrLine ? ` + ${adrLine}` : ''} + ` : ''} + ${pays} + + + + ${ibanClair} + + + `; + numTx++; + } + + // ── XML final au format PAIN.001.001.03 ────────────────────────── + const xml = ` + + + + ${msgId} + ${creDtTm} + ${notes.recordset.length} + ${totalFormate} + + ${dbtrNom} + + + + ${msgId} + TRF + true + ${notes.recordset.length} + ${totalFormate} + + + SEPA + + + ${todayISO} + + ${dbtrNom} + + ${dbtrCp} + ${dbtrVille} + ${dbtrPays} + ${dbtrAdrLine} + + + + + ${dbtrIban} + + + + + + NOTPROVIDED + + + + SLEV${transactions} + + +`; + + // ── Upload XML sur SharePoint dans Virements/{annee}/{mois}/ ───── + let xmlSharepointUrl = null; + try { + const xmlFileName = `virements-ndf-${annee}-${mois}-${Date.now().toString().slice(-5)}.xml`; + const xmlFolderPath = `Virements/${annee}/${mois}`; + const xmlUploadPath = `${xmlFolderPath}/${xmlFileName}`; + + const accessToken = await getGraphToken(); + if (accessToken) { + const spRes = await axios.put( + `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${xmlUploadPath}:/content`, + Buffer.from(xml, 'utf-8'), + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/xml' + }, + maxBodyLength: Infinity + } + ); + xmlSharepointUrl = spRes.data.webUrl; + console.log(`✅ XML virement uploadé sur SharePoint : ${xmlUploadPath}`); + } + } catch (spErr) { + console.error('⚠️ Upload XML SharePoint échoué (XML quand même téléchargé) :', spErr.message); + } + + // ── Générer les PDF récap pour chaque note ──────────────────────── + for (const note of notes.recordset) { + try { + let fichiersExistants = []; + try { fichiersExistants = JSON.parse(note.fichiers || '[]'); } catch { } + + const justifFiles = []; + for (const f of fichiersExistants) { + const name = (f.fileName || '').toLowerCase(); + if (name.includes('soumission') || name.includes('resoumission') || name.includes('recap')) continue; + try { + const buf = await downloadFromSharePoint(f.uploadUrl); + const mimetype = name.endsWith('.pdf') ? 'application/pdf' + : name.endsWith('.png') ? 'image/png' : 'image/jpeg'; + justifFiles.push({ buffer: buf, originalname: f.fileName, mimetype, size: buf.length }); + } catch (e) { console.warn(`⚠️ Justif non récupérable: ${f.fileName}`, e.message); } + } + + const dateObj = new Date(note.date); + const moisStr = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); + const moisCapitalized = moisStr.charAt(0).toUpperCase() + moisStr.slice(1); + const nomPrenom = `${note.nom.toUpperCase()} ${note.prenom}`; + + const histResult = await pool.request() + .input('noteId', sql.Int, note.id) + .query(` + SELECT h.Niveau, h.Action, h.Commentaire, h.DateAction, + c.prenom + ' ' + c.nom AS nomPrenom + FROM HistoriqueValidation h + JOIN CollaborateurAD c ON c.id = h.ValidateurId + WHERE h.NoteDeFraisId = @noteId + ORDER BY h.DateAction ASC + `); + + const signatures = [ + { niveau: 'COLLAB', nomPrenom, date: note.DateCreation || new Date(), action: 'soumettre', commentaire: null }, + ...histResult.recordset.map(h => ({ + niveau: h.Niveau, nomPrenom: h.nomPrenom, + date: h.DateAction, action: h.Action, commentaire: h.Commentaire || null + })) + ]; + + const noteDataPDF = { + reference: note.reference, nomPrenom, mois: moisCapitalized, + date: note.date, categorie: note.categorie || 'Multiple', + libelle: note.libelle, montant: parseFloat(note.montant), + lignesJson: note.lignesJson, tarifKm: await getTarifKm(), + statut: note.statut, departement: note.departement, + }; + + const recapBuffer = await generateRecapWithJustifs(noteDataPDF, justifFiles, signatures); + + const existingFolder = fichiersExistants[0]?.folderPath; + const nomDossier = existingFolder + ? existingFolder.split('/')[1] + : `${note.prenom}_${note.nom}`.replace(/[^a-zA-Z0-9]/g, '_'); + const moisDossier = existingFolder + ? existingFolder.split('/')[2] + : `${annee}-${mois}`; + + const recapResult = await uploadToSharePointHierarchique( + { + buffer: recapBuffer, + originalname: `${note.reference}_recap-paiement.pdf`, + mimetype: 'application/pdf', + size: recapBuffer.length + }, + note.reference, nomDossier, moisDossier + ); + + fichiersExistants.push(recapResult); + + await pool.request() + .input('id', sql.Int, note.id) + .input('fichiers', sql.NVarChar, JSON.stringify(fichiersExistants)) + .input('recapUrl', sql.NVarChar, recapResult.uploadUrl) + .query(` + UPDATE NoteDeFrais + SET fichiers = @fichiers, + sharepointUrl = @recapUrl, + DateModification = GETDATE() + WHERE id = @id + `); + + console.log(`✅ PDF récap-paiement généré pour ${note.reference}`); + } catch (pdfErr) { + console.error(`❌ PDF récap ${note.reference}:`, pdfErr.message); + } + } + + // ── Passer en 'paiementenattente' + enregistrer date XML ───────── + await pool.request() + .input('dateXml', sql.DateTime, now) + .input('xmlUrl', sql.NVarChar, xmlSharepointUrl || null) + .query(` + UPDATE NoteDeFrais + SET statut = 'paiementenattente', + dateXml = @dateXml, + DateModification = GETDATE() + WHERE id IN (${idList}) + AND statut IN ('approuve', 'approuvé', 'verifie') + `); + + // ── Notifier chaque collaborateur ───────────────────────────────── + for (const n of notes.recordset) { + try { + await creerNotification({ + destinataireId: n.collabId, + destinataireEmail: n.email, + type: 'paiement', + titre: `Paiement en cours de traitement : ${n.reference}`, + message: `Votre note ${n.reference} de ${parseFloat(n.montant).toFixed(2)} € est en cours de traitement bancaire.`, + noteId: n.id + }); + } catch (e) { console.error('Notif paiementenattente:', e.message); } + } + + // ── Téléchargement du XML côté client ──────────────────────────── + const xmlFileName = `virements-ndf-${annee}-${mois}-${todayISO}.xml`; + res.setHeader('Content-Type', 'application/xml; charset=iso-8859-1'); + res.setHeader('Content-Disposition', `attachment; filename=${xmlFileName}`); + res.send(xml); + + } catch (error) { + console.error('Erreur génération XML:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/paiements/xml-historique — liste les XML générés +app.get('/api/paiements/xml-historique', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + + try { + const { annee, mois } = req.query; + + const request = pool.request(); + let where = `WHERE n.dateXml IS NOT NULL AND n.statut IN ('paiementenattente', 'payee')`; + + if (annee) { + request.input('annee', sql.Int, parseInt(annee)); + where += ` AND YEAR(n.dateXml) = @annee`; + } + if (mois) { + request.input('mois', sql.Int, parseInt(mois)); + where += ` AND MONTH(n.dateXml) = @mois`; + } + + let campusWhere = ''; + if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) { + const campusCode = normalizeCampus(req.user.campus); + if (campusCode) { + request.input('campus', sql.NVarChar, `%${campusCode}%`); + campusWhere = `AND c.campus LIKE @campus`; + } + } + + const result = await request.query(` + SELECT + CAST(n.dateXml AS DATE) AS dateXmlJour, + MIN(n.dateXml) AS dateXmlExacte, + COUNT(*) AS nbNotes, + SUM(n.montant) AS totalMontant, + STRING_AGG(CAST(n.id AS NVARCHAR(20)), ',') AS noteIds, + STRING_AGG(n.reference, ', ') AS listeReferences + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + ${where} ${campusWhere} + GROUP BY CAST(n.dateXml AS DATE) + ORDER BY CAST(n.dateXml AS DATE) DESC + `); + + res.json(result.recordset); + } catch (error) { + console.error('GET /api/paiements/xml-historique:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/paiements/regenerer-xml — régénère le XML pour un batch +app.post('/api/paiements/regenerer-xml', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + + const { noteIds } = req.body; + if (!Array.isArray(noteIds) || noteIds.length === 0) + return res.status(400).json({ error: 'Aucune note sélectionnée' }); + + try { + const idList = noteIds.map(id => parseInt(id)).filter(Boolean).join(','); + + const notes = await pool.request().query(` + SELECT n.id, n.reference, n.montant, n.libelle, n.dateXml, + c.nom, c.prenom, c.iban, c.bic + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE n.id IN (${idList}) + AND n.statut IN ('paiementenattente', 'payee') + `); + + if (!notes.recordset.length) + return res.status(404).json({ error: 'Notes introuvables' }); + + const now = new Date(); + const annee = now.getFullYear(); + const mois = String(now.getMonth() + 1).padStart(2, '0'); + const todayISO = now.toISOString().split('T')[0]; + const creDtTm = now.toISOString().slice(0, 19); + const msgId = `NDF-REGEN-${annee}${mois}-${Date.now().toString().slice(-7)}`; + const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0).toFixed(2); + const cfg = await getConfigDebiteur(); + const dbtrNom = cfg.companyName; + const dbtrIban = cfg.companyIban; + const dbtrBic = cfg.companyBic; + const dbtrAdrLine = cfg.companyAddress; + const dbtrCp = cfg.companyCp; + const dbtrVille = cfg.companyVille; + const dbtrPays = cfg.companyPays; + + let transactions = ''; + for (const n of notes.recordset) { + let ibanClair = 'FR0000000000000000000000000'; + try { + if (n.iban && n.iban.includes(':')) ibanClair = decryptIban(n.iban); + else if (n.iban) ibanClair = n.iban; + } catch { } + + const benefNom = `${n.nom.toUpperCase()} ${n.prenom}`; + const benefBicBlock = n.bic + ? `${n.bic}` + : `NOTPROVIDED`; + + transactions += ` + + + VIREMENT NUM:${n.reference} + ${n.reference} + + + ${parseFloat(n.montant).toFixed(2)} + + ${benefBicBlock} + ${benefNom} + ${ibanClair} + `; + } + + const xml = ` + + + + ${msgId} + ${creDtTm} + ${notes.recordset.length} + ${total} + ${dbtrNom} + + + ${msgId} + TRF + true + ${notes.recordset.length} + ${total} + SEPA + ${todayISO} + + ${dbtrNom} + + ${dbtrCp} + ${dbtrVille} + ${dbtrPays} + ${dbtrAdrLine} + + + ${dbtrIban} + NOTPROVIDED + SLEV${transactions} + + +`; + + const xmlFileName = `virements-ndf-REGEN-${todayISO}-${Date.now().toString().slice(-5)}.xml`; + res.setHeader('Content-Type', 'application/xml; charset=iso-8859-1'); + res.setHeader('Content-Disposition', `attachment; filename=${xmlFileName}`); + res.send(xml); + + } catch (error) { + console.error('Erreur régénération XML:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/paiements/config-debiteur +app.get('/api/paiements/config-debiteur', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + try { + const result = await pool.request().query(` + SELECT TOP 1 id, companyName, companyIban, companyBic, + companyAddress, companyCp, companyVille, companyPays, + DateModification + FROM ConfigDebiteurXML WHERE actif = 1 + ORDER BY DateModification DESC + `); + res.json(result.recordset[0] ?? null); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// PUT /api/paiements/config-debiteur +app.put('/api/paiements/config-debiteur', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + + const { companyName, companyIban, companyBic, companyAddress, companyCp, companyVille, companyPays } = req.body; + + if (!companyName || !companyIban || !companyBic) + return res.status(400).json({ error: 'Nom, IBAN et BIC sont obligatoires' }); + + const ibanClean = companyIban.replace(/\s+/g, '').toUpperCase(); + const bicClean = companyBic.replace(/\s+/g, '').toUpperCase(); + + try { + // Désactiver l'ancienne config et insérer la nouvelle + await pool.request().query(`UPDATE ConfigDebiteurXML SET actif = 0 WHERE actif = 1`); + + await pool.request() + .input('companyName', sql.NVarChar, companyName.trim()) + .input('companyIban', sql.NVarChar, ibanClean) + .input('companyBic', sql.NVarChar, bicClean) + .input('companyAddress', sql.NVarChar, (companyAddress || '').trim()) + .input('companyCp', sql.NVarChar, (companyCp || '').trim()) + .input('companyVille', sql.NVarChar, (companyVille || '').trim()) + .input('companyPays', sql.NVarChar, (companyPays || 'FR').trim().slice(0, 2).toUpperCase()) + .input('modifiePar', sql.Int, req.user.id) + .query(` + INSERT INTO ConfigDebiteurXML + (companyName, companyIban, companyBic, companyAddress, + companyCp, companyVille, companyPays, actif, modifiePar, + DateCreation, DateModification) + VALUES + (@companyName, @companyIban, @companyBic, @companyAddress, + @companyCp, @companyVille, @companyPays, 1, @modifiePar, + GETDATE(), GETDATE()) + `); + + console.log(`✅ Config débiteur XML mise à jour par ${req.user.email}`); + res.json({ success: true, companyName, companyIban: ibanClean, companyBic: bicClean }); + } catch (e) { + console.error('PUT /api/paiements/config-debiteur:', e.message); + res.status(500).json({ error: e.message }); + } +}); +// POST /api/paiements/confirmer-paiement +// Body: { noteIds: number[], datePaiement: string (ISO) } +// POST /api/paiements/confirmer-paiement — Confirme paiement et passe statut à 'payee' +app.post('/api/paiements/confirmer-paiement', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + + const { noteIds, datePaiement } = req.body; + if (!Array.isArray(noteIds) || noteIds.length === 0) + return res.status(400).json({ error: 'Aucune note sélectionnée' }); + if (!datePaiement) + return res.status(400).json({ error: 'Date de paiement obligatoire' }); + + try { + const idList = noteIds.map(id => parseInt(id)).filter(Boolean).join(','); + const dateObj = new Date(datePaiement); + const mois = dateObj.getMonth() + 1; + const annee = dateObj.getFullYear(); + + // Récupérer les notes pour notifications + const notes = await pool.request().query(` + SELECT n.id, n.reference, n.montant, n.libelle, + c.id AS collabId, c.email, c.prenom, c.nom + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + WHERE n.id IN (${idList}) + AND n.statut = 'paiementenattente' + `); + + if (!notes.recordset.length) + return res.status(404).json({ error: 'Aucune note en attente de paiement trouvée' }); + + // Mettre à jour statut → 'payee' + await pool.request() + .input('datePaiement', sql.DateTime, dateObj) + .input('moisPaiement', sql.Int, mois) + .input('anneePaiement', sql.Int, annee) + .query(` + UPDATE NoteDeFrais + SET statut = 'payee', + datePaiement = @datePaiement, + moisPaiement = @moisPaiement, + anneePaiement = @anneePaiement, + DateModification = GETDATE() + WHERE id IN (${idList}) + AND statut = 'paiementenattente' + `); + + // Notifier chaque collaborateur + for (const n of notes.recordset) { + try { + await creerNotification({ + destinataireId: n.collabId, + destinataireEmail: n.email, + type: 'paiement', + titre: `Paiement effectué : ${n.reference}`, + message: `Votre note ${n.reference} de ${parseFloat(n.montant).toFixed(2)}€ a été payée le ${dateObj.toLocaleDateString('fr-FR')}.`, + noteId: n.id + }); + await sendMailGraph(n.email, `Paiement effectué : ${n.reference}`, ` +
+
+

Paiement effectué

+
+
+

Bonjour ${n.prenom} ${n.nom},

+

Votre note ${n.reference} — ${n.libelle} d'un montant de ${parseFloat(n.montant).toFixed(2)}€ a été payée le ${dateObj.toLocaleDateString('fr-FR')}.

+
+ Voir mes notes +
+
+
`); + } catch (e) { console.error('Notif paiement confirmé:', e.message); } + } + + const total = notes.recordset.reduce((s, n) => s + parseFloat(n.montant), 0); + res.json({ + success: true, + count: notes.recordset.length, + total: parseFloat(total.toFixed(2)), + datePaiement + }); + } catch (error) { + console.error('Erreur confirmer-paiement:', error.message); + res.status(500).json({ error: error.message }); + } +}); + + + +// ================================================ +// ROUTES DE TEST +// ================================================ +app.get('/api/test-get-drive', async (req, res) => { + try { + const token = await getGraphToken(); + const siteId = 'ensup.sharepoint.com,d94abc08-28eb-47ce-8e12-fbbd6f16b9ea,a052c325-d33a-40e3-9e7b-7896a2ea7ab7'; + const r = await axios.get(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, { headers: { Authorization: `Bearer ${token}` } }); + res.json({ driveId: r.data.id, name: r.data.name }); + } catch (e) { res.status(500).json({ error: e.message, details: e.response?.data }); } +}); + +app.get('/api/test-upload', async (req, res) => { + try { + const token = await getGraphToken(); + const testContent = Buffer.from('Test upload NDF - ' + new Date().toISOString()); + const path = `Notes de Frais/TEST_Upload/test_${Date.now()}.txt`; + const r = await axios.put( + `https://graph.microsoft.com/v1.0/sites/${process.env.SHAREPOINT_SITE_ID}/drives/${process.env.SHAREPOINT_DRIVE_ID}/root:/${path}:/content`, + testContent, + { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'text/plain' } } + ); + res.json({ ok: true, url: r.data.webUrl }); + } catch (e) { res.status(500).json({ error: e.message, details: e.response?.data }); } +}); + + + + +// ══════════════════════════════════════════════════════════════════ +// BROUILLONS — Sauvegarde serveur (statut = 'brouillon') +// ══════════════════════════════════════════════════════════════════ + +// GET /api/notes/brouillons — récupère les brouillons du collaborateur connecté +app.get('/api/notes/brouillons', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('collaborateurId', sql.Int, req.user.id) + .query(` + SELECT id, libelle, date, description, montant, categorie, + lignesJson, DateCreation, DateModification + FROM NoteDeFrais + WHERE collaborateurId = @collaborateurId + AND statut = 'brouillon' + ORDER BY DateModification DESC + `); + res.json(result.recordset); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/notes/brouillons — crée un nouveau brouillon +app.post('/api/notes/brouillons', authenticateToken, async (req, res) => { + try { + const { libelle, date, description, lignes } = req.body; + + let lignesJson = '[]'; + if (lignes) { + lignesJson = typeof lignes === 'string' ? lignes : JSON.stringify(lignes); + } + + // Calcul du montant total estimé + let montantEstime = 0; + try { + const tarifKm = await getTarifKm(); + const lignesParsed = JSON.parse(lignesJson); + montantEstime = lignesParsed.reduce((acc, l) => { + const isKm = (l.categorie || '').toLowerCase().includes('kilom'); + const km = parseFloat(l.km) || 0; + const ttc = isKm ? parseFloat((km * tarifKm).toFixed(2)) : (parseFloat(l.montant) || 0); + return acc + ttc; + }, 0); + } catch (e) { /* montant reste 0 */ } + + // Référence temporaire pour les brouillons (colonne NOT NULL) + const refBrouillon = `BRO-${req.user.id}-${Date.now().toString().slice(-6)}`; + + const insertResult = await pool.request() + .input('collaborateurId', sql.Int, req.user.id) + .input('reference', sql.NVarChar, refBrouillon) + .input('libelle', sql.NVarChar, libelle || 'Brouillon sans titre') + .input('date', sql.Date, date ? new Date(date) : new Date()) + .input('description', sql.NVarChar, description || null) + .input('montant', sql.Decimal, montantEstime) + .input('categorie', sql.NVarChar, 'Multiple') + .input('lignesJson', sql.NVarChar, lignesJson) + .input('statut', sql.NVarChar, 'brouillon') + .query(` + INSERT INTO NoteDeFrais + (reference, collaborateurId, libelle, date, description, montant, + categorie, lignesJson, statut, DateCreation, DateModification) + OUTPUT INSERTED.id, INSERTED.DateCreation + VALUES + (@reference, @collaborateurId, @libelle, @date, @description, @montant, + @categorie, @lignesJson, @statut, GETDATE(), GETDATE()) + `); + + const created = insertResult.recordset[0]; + res.status(201).json({ success: true, id: created.id, createdAt: created.DateCreation }); + } catch (error) { + console.error('Erreur POST brouillon:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// PUT /api/notes/brouillons/:id — met à jour un brouillon existant +app.put('/api/notes/brouillons/:id', authenticateToken, upload.any(), async (req, res) => { + try { + const { libelle, date, description, lignes } = req.body; + const brouillonId = parseInt(req.params.id); + + // Vérifier que ce brouillon appartient bien à ce collaborateur + const check = await pool.request() + .input('id', sql.Int, brouillonId) + .input('collaborateurId', sql.Int, req.user.id) + .query(`SELECT id FROM NoteDeFrais + WHERE id = @id AND collaborateurId = @collaborateurId AND statut = 'brouillon'`); + + if (!check.recordset.length) { + return res.status(404).json({ error: 'Brouillon introuvable ou accès refusé' }); + } + + // Récupérer infos collaborateur pour le dossier SharePoint + const collabResult = await pool.request() + .input('id', sql.Int, req.user.id) + .query(`SELECT prenom, nom FROM CollaborateurAD WHERE id = @id`); + const collaborateur = collabResult.recordset[0]; + const nomDossier = `${collaborateur.prenom}${collaborateur.nom}`.replace(/[^a-zA-Z0-9]/g, '_'); + const now = new Date(); + const moisDossier = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; + const noteRef = `BRO-${brouillonId}`; + + let lignesParsed = []; + if (lignes) { + try { + lignesParsed = typeof lignes === 'string' ? JSON.parse(lignes) : lignes; + } catch (e) { lignesParsed = []; } + } + + const uploadedFiles = {}; + // ✅ Upload des nouveaux fichiers vers SharePoint et injection dans lignesParsed + const allFiles = req.files || []; + for (const file of allFiles) { + const match = file.fieldname.match(/^files_(.+)$/); + if (!match) continue; + const depId = String(match[1]); + try { + const uploaded = await uploadToSharePointHierarchique(file, noteRef, nomDossier, moisDossier); + const ligne = lignesParsed.find(l => String(l.id) === depId); + if (ligne) { + if (!Array.isArray(ligne.qrFiles)) ligne.qrFiles = []; + const dejaSauve = ligne.qrFiles.some(f => f.fileName === uploaded.fileName); + if (!dejaSauve) { + // ✅ Stocker avec origin='upload' + ligne.qrFiles.push({ + fileName: uploaded.fileName, + uploadUrl: uploaded.uploadUrl, + origin: 'upload' // ✅ AJOUT + }); + // ✅ Tracker pour le retour + if (!uploadedFiles[depId]) uploadedFiles[depId] = []; + uploadedFiles[depId].push({ + fileName: uploaded.fileName, + uploadUrl: uploaded.uploadUrl, + origin: 'upload' + }); + } + } + } catch (e) { + console.error(`Upload brouillon fichier ${file.originalname}:`, e.message); + } + } + + + // Recalcul montant + let montantEstime = 0; + try { + const tarifKm = await getTarifKm(); + montantEstime = lignesParsed.reduce((acc, l) => { + const isKm = (l.categorie || '').toLowerCase().includes('kilom'); + const km = parseFloat(l.km) || 0; + const ttc = isKm + ? parseFloat((km * tarifKm).toFixed(2)) + : (parseFloat(l.montant) || parseFloat(l.tvaItems?.[0]?.montantTTC) || 0); + return acc + ttc; + }, 0); + } catch (e) { /* montant reste 0 */ } + + const lignesJson = JSON.stringify(lignesParsed); + + await pool.request() + .input('id', sql.Int, brouillonId) + .input('libelle', sql.NVarChar, libelle || 'Brouillon sans titre') + .input('date', sql.Date, date ? new Date(date) : new Date()) + .input('description', sql.NVarChar, description || null) + .input('montant', sql.Decimal, montantEstime) + .input('lignesJson', sql.NVarChar, lignesJson) + .query(` + UPDATE NoteDeFrais + SET libelle = @libelle, + date = @date, + description = @description, + montant = @montant, + lignesJson = @lignesJson, + DateModification = GETDATE() + WHERE id = @id + `); + + res.json({ success: true, updatedAt: new Date().toISOString(), uploadedFiles }); + } catch (error) { + console.error('Erreur PUT brouillon:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /api/notes/brouillons/:id — supprime un brouillon +app.delete('/api/notes/brouillons/:id', authenticateToken, async (req, res) => { + try { + const brouillonId = parseInt(req.params.id); + + const check = await pool.request() + .input('id', sql.Int, brouillonId) + .input('collaborateurId', sql.Int, req.user.id) + .query(`SELECT id FROM NoteDeFrais + WHERE id = @id AND collaborateurId = @collaborateurId AND statut = 'brouillon'`); + + if (!check.recordset.length) { + return res.status(404).json({ error: 'Brouillon introuvable ou accès refusé' }); + } + + await pool.request() + .input('id', sql.Int, brouillonId) + .query(`DELETE FROM NoteDeFrais WHERE id = @id`); + + res.json({ success: true }); + } catch (error) { + console.error('Erreur DELETE brouillon:', error.message); + res.status(500).json({ error: error.message }); + } +}); + + +// ══════════════════════════════════════════════════════════════════ +// DOCUMENTS COLLABORATEUR — RIB / Carte grise / Permis +// Coller dans server.js après les routes /api/profil +// ══════════════════════════════════════════════════════════════════ + + + +// ============================================================ +// DOCUMENTS COLLABORATEUR — RIB / Carte grise / Permis +// ============================================================ +const DOCTYPES = ['rib', 'cartegrise', 'permis']; + +// GET /api/profil/documents +app.get('/api/profil/documents', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('collabId', sql.Int, req.user.id) + .query(` + SELECT type, fileName, sharepointUrl, dateUpload, DateModification, statut, commentaire + FROM DocumentsCollaborateur + WHERE collaborateurId = @collabId + AND type != 'rib' -- ← exclure le rib de cette table + `); + + // Vérifier si IBAN saisi directement dans CollaborateurAD + const ibanResult = await pool.request() + .input('collabId', sql.Int, req.user.id) + .query(`SELECT IBAN FROM CollaborateurAD WHERE id = @collabId`); + + const ibanSaisi = !!(ibanResult.recordset[0]?.IBAN); + + const docs = { + rib: ibanSaisi + ? { fileName: 'IBAN_saisi', sharepointUrl: '', updatedAt: new Date().toISOString(), statut: 'valide', commentaire: null } + : null, + carte_grise: null, + permis: null + }; + + for (const row of result.recordset) { + docs[row.type] = { + fileName: row.fileName, + sharepointUrl: row.sharepointUrl, + updatedAt: row.DateModification, + statut: row.statut ?? 'en_attente', + commentaire: row.commentaire ?? null + }; + } + + res.json(docs); + } catch (error) { + console.error('GET /api/profil/documents', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/profil/documents/:type — upload ou remplacement +app.post('/api/profil/documents/:type', authenticateToken, upload.single('file'), async (req, res) => { + try { + const type = req.params.type; + if (!DOCTYPES.includes(type)) + return res.status(400).json({ error: 'Type invalide. Valeurs : rib, cartegrise, permis' }); + if (!req.file) + return res.status(400).json({ error: 'Aucun fichier fourni' }); + + // Infos collaborateur (campus pour notifier la bonne Finance) + const collabResult = await pool.request() + .input('id', sql.Int, req.user.id) + .query(`SELECT prenom, nom, campus FROM CollaborateurAD WHERE id = @id`); + if (!collabResult.recordset.length) + return res.status(404).json({ error: 'Collaborateur introuvable' }); + + const { prenom, nom, campus } = collabResult.recordset[0]; + const nomDossier = `${prenom}${nom}`.replace(/[^a-zA-Z0-9]/g, '_'); + const safeFileName = `${type}_${req.file.originalname.replace(/[^a-zA-Z0-9.\-]/g, '_')}`; + const folderPath = `Documents-Profil/${nomDossier}`; + const uploadPath = `${folderPath}/${safeFileName}`; + + // Upload SharePoint + const accessToken = await getGraphToken(); + if (!accessToken) throw new Error('Token Graph indisponible'); + + const spRes = await axios.put( + `https://graph.microsoft.com/v1.0/sites/${SHAREPOINT_CONFIG.siteId}/drives/${SHAREPOINT_CONFIG.driveId}/root:/${uploadPath}:/content`, + req.file.buffer, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': req.file.mimetype || 'application/octet-stream' + }, + maxBodyLength: Infinity + } + ); + const sharepointUrl = spRes.data.webUrl; + + // UPSERT — remet en attente si document remplacé + await pool.request() + .input('collabId', sql.Int, req.user.id) + .input('type', sql.NVarChar, type) + .input('fileName', sql.NVarChar, safeFileName) + .input('sharepointUrl', sql.NVarChar, sharepointUrl) + .query(` + IF EXISTS (SELECT 1 FROM DocumentsCollaborateur WHERE collaborateurId = @collabId AND type = @type) + UPDATE DocumentsCollaborateur + SET fileName = @fileName, sharepointUrl = @sharepointUrl, + DateModification = GETDATE(), dateUpload = GETDATE(), + statut = 'en_attente', validePar = NULL, dateValidation = NULL, commentaire = NULL + WHERE collaborateurId = @collabId AND type = @type + ELSE + INSERT INTO DocumentsCollaborateur (collaborateurId, type, fileName, sharepointUrl, dateUpload, DateModification, statut) + VALUES (@collabId, @type, @fileName, @sharepointUrl, GETDATE(), GETDATE(), 'en_attente') + `); + + // Notifier les Finance du même campus + const typeLabels = { rib: 'RIB', cartegrise: 'Carte grise', permis: 'Permis de conduire' }; + const campusNorm = normalizeCampus(campus); + const financeResult = await pool.request() + .input('campus', sql.NVarChar, campusNorm ? `%${campusNorm}%` : '%') + .query(` + SELECT c.id, c.email, c.prenom, c.nom + FROM CollaborateurAD c + JOIN UtilisateurRoles r ON r.collaborateur_id = c.id + WHERE r.role = 'Finance' AND r.actif = 1 + AND c.campus LIKE @campus AND c.Actif = 1 + `); + + const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net'; + for (const finance of financeResult.recordset) { + try { + await creerNotification({ + destinataireId: finance.id, + destinataireEmail: finance.email, + type: 'validationdoc', + titre: `Document à valider — ${prenom} ${nom}`, + message: `${prenom} ${nom} (${campus}) a soumis son ${typeLabels[type]} pour validation.`, + noteId: null + }); + } catch (e) { console.error('Notif BDD Finance doc', e.message); } + try { + await sendMailGraph( + finance.email, + `Document à valider — ${typeLabels[type]} de ${prenom} ${nom}`, + `
+
+

Document à valider

+
+
+

Bonjour ${finance.prenom} ${finance.nom},

+

${prenom} ${nom} (${campus}) a soumis son ${typeLabels[type]} en attente de votre validation.

+
+ + Valider les documents + +
+
+
` + ); + } catch (e) { console.error('Email Finance doc', e.message); } + } + + res.json({ success: true, type, fileName: safeFileName, sharepointUrl, statut: 'en_attente', updatedAt: new Date().toISOString() }); + } catch (error) { + console.error(`POST /api/profil/documents/${req.params.type}`, error.message); + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /api/profil/documents/:type +app.delete('/api/profil/documents/:type', authenticateToken, async (req, res) => { + try { + const type = req.params.type; + if (!DOCTYPES.includes(type)) + return res.status(400).json({ error: 'Type invalide' }); + await pool.request() + .input('collabId', sql.Int, req.user.id) + .input('type', sql.NVarChar, type) + .query(`DELETE FROM DocumentsCollaborateur WHERE collaborateurId = @collabId AND type = @type`); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + + +// GET /api/finance/documents-a-valider — Finance voit les docs en_attente de son campus +app.get('/api/finance/documents-a-valider', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + try { + const request = pool.request(); + let campusFilter = ''; + if (req.user.campus && !hasAnyRole(req.user, 'superUtilisateur')) { + const campusCode = normalizeCampus(req.user.campus); + if (campusCode) { + request.input('campus', sql.NVarChar, `%${campusCode}%`); + campusFilter = 'AND c.campus LIKE @campus'; + } + } + const result = await request.query(` + SELECT d.id, d.collaborateurId, d.type, d.fileName, d.sharepointUrl, + d.dateUpload, d.DateModification, d.statut, d.commentaire, + c.prenom, c.nom, c.email, c.campus, c.departement + FROM DocumentsCollaborateur d + JOIN CollaborateurAD c ON c.id = d.collaborateurId + WHERE d.statut = 'en_attente' ${campusFilter} + ORDER BY d.DateModification ASC + `); + res.json(result.recordset); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PUT /api/finance/documents/:id/valider — Finance valide ou refuse un document +// PUT /api/finance/documents/:id/valider +app.put('/api/finance/documents/:id/valider', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'superUtilisateur')) return res.status(403).json({ error: 'Accès réservé Finance' }); + try { + const docId = parseInt(req.params.id); + const { action, commentaire } = req.body; + if (!['valider', 'refuser'].includes(action)) return res.status(400).json({ error: 'Action invalide' }); + + const newStatut = action === 'valider' ? 'valide' : 'refuse'; + + const docResult = await pool.request() + .input('id', sql.Int, docId) + .query(`SELECT d.*, c.prenom, c.nom, c.email, c.campus FROM DocumentsCollaborateur d JOIN CollaborateurAD c ON c.id = d.collaborateurId WHERE d.id = @id`); + if (!docResult.recordset.length) return res.status(404).json({ error: 'Document introuvable' }); + + const doc = docResult.recordset[0]; + + await pool.request() + .input('id', sql.Int, docId) + .input('statut', sql.NVarChar, newStatut) + .input('validePar', sql.Int, req.user.id) + .input('commentaire', sql.NVarChar, commentaire || null) + .query(` + UPDATE DocumentsCollaborateur + SET statut = @statut, validePar = @validePar, + dateValidation = GETDATE(), commentaire = @commentaire, DateModification = GETDATE() + WHERE id = @id + `); + + const typeLabels = { rib: 'RIB', carte_grise: 'Carte grise', cartegrise: 'Carte grise', permis: 'Permis de conduire' }; + const isValide = newStatut === 'valide'; + const titre = isValide ? `${typeLabels[doc.type] || doc.type} validé ✅` : `${typeLabels[doc.type] || doc.type} refusé ❌`; + const message = isValide + ? `Votre ${typeLabels[doc.type] || doc.type} a été validé par la Finance.` + : `Votre ${typeLabels[doc.type] || doc.type} a été refusé.${commentaire ? ' Motif : ' + commentaire : ''}`; + + try { + await creerNotification({ destinataireId: doc.collaborateurId, destinataireEmail: doc.email, type: isValide ? 'doc_valide' : 'doc_refuse', titre, message, noteId: null }); + } catch (e) { console.error('Notif doc validé', e.message); } + + try { + const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net'; + await sendMailGraph(doc.email, titre, + `
+
+

${titre}

+
+
+

Bonjour ${doc.prenom} ${doc.nom},

+

${message}

+ ${!isValide ? '

Veuillez soumettre un nouveau document corrigé depuis votre profil.

' : ''} +
+ Accéder à mon profil +
+
+
` + ); + } catch (e) { console.error('Email doc validé', e.message); } + + res.json({ success: true, statut: newStatut }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/profil/docs-statut — vérifie si le collaborateur peut soumettre (docs validés) +app.get('/api/profil/docs-statut', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('collaborateurId', sql.Int, req.user.id) + .query(`SELECT type, statut FROM DocumentsCollaborateur WHERE collaborateurId = @collaborateurId`); + const map = {}; + for (const row of result.recordset) map[row.type] = row.statut; + res.json({ + rib: map['rib'] || 'absent', + carte_grise: map['carte_grise'] || 'absent', + permis: map['permis'] || 'absent', + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ── PUT /api/profil/iban ───────────────────────────────────────── +app.put('/api/profil/iban', authenticateToken, async (req, res) => { + try { + let { iban, bic } = req.body; + if (!iban) return res.status(400).json({ error: 'IBAN obligatoire' }); + if (!bic) return res.status(400).json({ error: 'BIC obligatoire' }); + + iban = iban.replace(/\s+/g, '').toUpperCase(); + bic = bic.replace(/\s+/g, '').toUpperCase(); + + if (!validateIban(iban)) + return res.status(400).json({ error: 'IBAN invalide (format ou checksum incorrect)' }); + + const ibanChiffre = encryptIban(iban); + const ibanHash = hashIban(iban); + + await pool.request() + .input('id', sql.Int, req.user.id) + .input('iban', sql.NVarChar, ibanChiffre) + .input('ibanHash', sql.NVarChar, ibanHash) + .input('bic', sql.NVarChar, bic) + .query(` + UPDATE CollaborateurAD + SET IBAN = @iban, IBAN_HASH = @ibanHash, BIC = @bic, DateModification = GETDATE() + WHERE id = @id + `); + + // ← Le second bloc DocumentsCollaborateur est supprimé + + res.json({ success: true, iban: maskIban(iban), bic, statut: 'valide' }); + } catch (error) { + console.error('PUT /api/profil/iban :', error.message); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + +// ── GET /api/profil/iban ───────────────────────────────────────── +app.get('/api/profil/iban', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('id', sql.Int, req.user.id) + .query(`SELECT IBAN, BIC FROM CollaborateurAD WHERE id = @id`); + + const row = result.recordset[0]; + if (!row) return res.status(404).json({ error: 'Utilisateur introuvable' }); + + let ibanMasque = null; + if (row.IBAN) { + try { + const ibanClair = decryptIban(row.IBAN); + ibanMasque = maskIban(ibanClair); + } catch { + // IBAN encore en clair en base (avant migration) — on masque directement + ibanMasque = maskIban(row.IBAN); + } + } + + res.json({ + iban: ibanMasque, + ibanSaisi: !!row.IBAN, + bic: row.BIC || null, + }); + } catch (error) { + console.error('GET /api/profil/iban :', error.message); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + +app.post('/api/verificateur/notes/:id/non-conforme', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès refusé' }); + + const { fileName, motif } = req.body; + const noteId = parseInt(req.params.id); + + try { + const noteResult = await pool.request() + .input('id', sql.Int, noteId) + .query(` + SELECT + n.id, n.reference, n.libelle, n.montant, + n.collaborateurId, + c.prenom, c.nom, c.email, + v1.id AS n1Id, + v1.email AS emailN1, + v1.prenom AS prenomN1, + v1.nom AS nomN1 + FROM NoteDeFrais n + JOIN CollaborateurAD c ON c.id = n.collaborateurId + LEFT JOIN CollaborateurAD v1 ON v1.id = n.validateurN1Id + WHERE n.id = @id + `); + + if (!noteResult.recordset.length) + return res.status(404).json({ error: 'Note introuvable' }); + + const note = noteResult.recordset[0]; + + // ✅ NOUVEAU — sauvegarder en BDD + await pool.request() + .input('noteId', sql.Int, noteId) + .input('verificateurId', sql.Int, req.user.id) + .input('fileName', sql.NVarChar, fileName) + .input('motif', sql.NVarChar, motif) + .query(` + INSERT INTO JustificatifsNonConformes + (noteDeFraisId, verificateurId, fileName, motif, statut, dateSignalement) + VALUES + (@noteId, @verificateurId, @fileName, @motif, 'non_conforme', GETDATE()) + `); + await pool.request() + .input('id', sql.Int, noteId) + .query(` + UPDATE NoteDeFrais SET + statut = 'non_conforme_verif', + DateModification = GETDATE() + WHERE id = @id AND statut = 'approuve' + `); + + // ✅ NOUVEAU — tracer dans HistoriqueValidation + await pool.request() + .input('noteId', sql.Int, noteId) + .input('validateurId', sql.Int, req.user.id) + .input('commentaire', sql.NVarChar, `Justificatif non conforme : "${fileName}" — ${motif}`) + .input('statut', sql.NVarChar, 'approuve') + .query(` + INSERT INTO HistoriqueValidation + (NoteDeFraisId, ValidateurId, Niveau, Action, Commentaire, NouveauStatut, DateAction) + VALUES + (@noteId, @validateurId, 'VERIF', 'non_conforme', @commentaire, @statut, GETDATE()) + `); + + const verificateurNom = `${req.user.prenom} ${req.user.nom}`; + const montantFormate = parseFloat(note.montant).toFixed(2); + const frontendUrl = process.env.FRONTEND_URL || 'https://myndf.ensup-adm.net'; + + // Notifications (votre code existant inchangé) + try { + await creerNotification({ + destinataireId: note.collaborateurId, + destinataireEmail: note.email, + type: 'refus', + titre: `⚠️ Justificatif non conforme — ${note.reference}`, + message: `Le justificatif "${fileName}" de votre note ${note.reference} est non conforme. Motif : ${motif}`, + noteId + }); + } catch (e) { console.error('Notif BDD collab non-conforme:', e.message); } + + + try { + await sendMailGraph( + note.email, + `⚠️ Justificatif non conforme — ${note.reference}`, + `
+
+

⚠️ Justificatif non conforme

+

Une correction est nécessaire

+
+
+

Bonjour ${note.prenom} ${note.nom},

+

Le vérificateur Finance ${verificateurNom} a signalé + un justificatif non conforme sur votre note ${note.reference}.

+ +
+
+ Justificatif concerné +
+
+ 📄 ${fileName} +
+
+ Motif +
+
${motif}
+
+ +
+ + + + + + + +
Référence${note.reference}
Libellé${note.libelle}
Montant${montantFormate} €
+
+ +
+
+ 📝 Que faire ? +
+
    +
  1. Retrouvez le justificatif original corrigé
  2. +
  3. Contactez votre responsable ou la Finance
  4. +
  5. Soumettez un nouveau justificatif via la plateforme
  6. +
+
+ +
+ + Accéder à ma note → + +
+
+
` + ); + } catch (e) { console.error('Email collab non-conforme:', e.message); } + + // ── Notifier le validateur N1 ── + if (note.n1Id && note.emailN1) { + try { + await creerNotification({ + destinataireId: note.n1Id, + destinataireEmail: note.emailN1, + type: 'refus', + titre: `⚠️ Justificatif non conforme — ${note.reference}`, + message: `La note ${note.reference} de ${note.prenom} ${note.nom} comporte un justificatif non conforme : "${fileName}". Motif : ${motif}`, + noteId + }); + } catch (e) { console.error('Notif BDD N1 non-conforme:', e.message); } + + try { + await sendMailGraph( + note.emailN1, + `⚠️ Note ${note.reference} — justificatif non conforme`, + `
+
+

⚠️ Justificatif non conforme signalé

+
+
+

Bonjour ${note.prenomN1} ${note.nomN1},

+

Le vérificateur Finance ${verificateurNom} a signalé + un justificatif non conforme sur la note + ${note.reference} de + ${note.prenom} ${note.nom}.

+
+
+ 📄 ${fileName} +
+
+ Motif : ${motif} +
+
+

+ Le collaborateur a été notifié et doit fournir un justificatif corrigé. +

+
+
` + ); + } catch (e) { console.error('Email N1 non-conforme:', e.message); } + } + + res.json({ + success: true, + notifiedCollab: true, + notifiedN1: !!(note.n1Id && note.emailN1) + }); + + } catch (error) { + console.error('Erreur POST non-conforme:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/paiements/filtres-disponibles', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'Finance', 'ValidateurFinance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès réservé Finance' }); + + try { + const result = await pool.request().query(` + SELECT DISTINCT + c.campus, + c.societe + FROM CollaborateurAD c + WHERE c.Actif = 1 + AND c.campus IS NOT NULL + AND c.campus != '' + ORDER BY c.campus, c.societe + `); + + const campusSet = new Set(); + const societeSet = new Set(); + + for (const row of result.recordset) { + if (row.campus) { + const code = (() => { + const c = row.campus.toUpperCase(); + if (c.includes('SQY') || c.includes('SAINT')) return 'SQY'; + if (c.includes('CGY') || c.includes('CERGY')) return 'CGY'; + if (c.includes('MRS') || c.includes('MARSEILLE')) return 'MRS'; + if (c.includes('NTE') || c.includes('NANTES')) return 'NTE'; + return row.campus; + })(); + campusSet.add(code); + } + if (row.societe && row.societe.trim()) { + societeSet.add(row.societe.trim()); + } + } + + res.json({ + campus: [...campusSet].sort(), + societes: [...societeSet].sort() + }); + + } catch (error) { + console.error('GET /api/paiements/filtres-disponibles:', error.message); + res.status(500).json({ error: error.message }); + } +}); +app.get('/api/verificateur/notes/:id/non-conformes', authenticateToken, async (req, res) => { + if (!hasAnyRole(req.user, 'VerificateurFinance', 'Finance', 'superUtilisateur')) + return res.status(403).json({ error: 'Accès refusé' }); + + try { + const result = await pool.request() + .input('noteId', sql.Int, parseInt(req.params.id)) + .query(` + SELECT j.id, j.fileName, j.motif, j.statut, j.dateSignalement, + c.prenom + ' ' + c.nom AS verificateur + FROM JustificatifsNonConformes j + JOIN CollaborateurAD c ON c.id = j.verificateurId + WHERE j.noteDeFraisId = @noteId + ORDER BY j.dateSignalement DESC + `); + res.json(result.recordset); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ========================================================= +// ROUTES À AJOUTER DANS server.js +// Coller après les routes /api/profil/iban existantes +// ========================================================= + +// ── GET /api/profil/vehicule ───────────────────────────── +app.get('/api/profil/vehicule', authenticateToken, async (req, res) => { + try { + const result = await pool.request() + .input('id', sql.Int, req.user.id) + .query(` + SELECT chevauxFiscaux, vehiculeMarque, vehiculeModele, + vehiculeImmat, vehiculeDateMaj + FROM CollaborateurAD + WHERE id = @id + `); + + const row = result.recordset[0]; + if (!row) return res.status(404).json({ error: 'Utilisateur introuvable' }); + + // Retourner null si pas encore configuré + if (!row.chevauxFiscaux) { + return res.json({ configured: false, vehicule: null }); + } + + res.json({ + configured: true, + vehicule: { + chevaux: row.chevauxFiscaux, + marque: row.vehiculeMarque || '', + modele: row.vehiculeModele || '', + immatriculation: row.vehiculeImmat || '', + dateMaj: row.vehiculeDateMaj, + } + }); + } catch (error) { + console.error('GET /api/profil/vehicule :', error.message); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + +// ── PUT /api/profil/vehicule ───────────────────────────── +app.put('/api/profil/vehicule', authenticateToken, async (req, res) => { + try { + const { chevaux, marque, modele, immatriculation } = req.body; + + // Validation + const cv = parseInt(chevaux); + if (!cv || cv < 3 || cv > 7) { + return res.status(400).json({ + error: 'Cheval fiscal invalide (valeurs acceptées : 3, 4, 5, 6, 7)' + }); + } + + const immatClean = (immatriculation || '') + .replace(/\s+/g, '-') + .toUpperCase() + .slice(0, 20); + + await pool.request() + .input('id', sql.Int, req.user.id) + .input('chevaux', sql.Int, cv) + .input('marque', sql.NVarChar, (marque || '').trim().slice(0, 100)) + .input('modele', sql.NVarChar, (modele || '').trim().slice(0, 100)) + .input('immat', sql.NVarChar, immatClean) + .query(` + UPDATE CollaborateurAD SET + chevauxFiscaux = @chevaux, + vehiculeMarque = @marque, + vehiculeModele = @modele, + vehiculeImmat = @immat, + vehiculeDateMaj = GETDATE(), + DateModification = GETDATE() + WHERE id = @id + `); + + console.log(`✅ Profil véhicule mis à jour — user ${req.user.email} : ${cv} CV`); + + res.json({ + success: true, + vehicule: { chevaux: cv, marque: marque || '', modele: modele || '', immatriculation: immatClean } + }); + } catch (error) { + console.error('PUT /api/profil/vehicule :', error.message); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + +// ── DELETE /api/profil/vehicule ────────────────────────── +app.delete('/api/profil/vehicule', authenticateToken, async (req, res) => { + try { + await pool.request() + .input('id', sql.Int, req.user.id) + .query(` + UPDATE CollaborateurAD SET + chevauxFiscaux = NULL, + vehiculeMarque = NULL, + vehiculeModele = NULL, + vehiculeImmat = NULL, + vehiculeDateMaj = NULL, + DateModification = GETDATE() + WHERE id = @id + `); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ================================================ +// GESTION DES ERREURS +// ================================================ +app.use((err, req, res, next) => { + console.error('❌ Erreur middleware:', err.stack); + res.status(500).json({ error: 'Une erreur est survenue', details: process.env.NODE_ENV === 'development' ? err.message : undefined }); +}); + +// ================================================ +// DÉMARRAGE DU SERVEUR +// ================================================ +const server = app.listen(PORT, '0.0.0.0', () => { + console.log('\n================================================'); + console.log(`✅ SERVEUR DÉMARRÉ sur http://0.0.0.0:${PORT}`); + console.log('================================================'); + + setTimeout(async () => { + console.log('\n🚀 Lancement synchronisation automatique Entra ID...'); + await syncEntraIdUsers(); + setInterval(async () => { + console.log('\n🔁 Synchronisation périodique Entra ID...'); + await syncEntraIdUsers(); + }, 6 * 60 * 60 * 1000); + }, 5000); +}); + +server.on('error', (error) => { + console.error('\n❌ ERREUR SERVEUR:', error); + if (error.code === 'EADDRINUSE') console.error(`⚠️ Le port ${PORT} est déjà utilisé`); + process.exit(1); +}); + +setInterval(() => { }, 60000); \ No newline at end of file diff --git a/ndf/public/img/NDF_Image_Logo.png b/ndf/public/img/NDF_Image_Logo.png new file mode 100644 index 0000000..3ba6b46 Binary files /dev/null and b/ndf/public/img/NDF_Image_Logo.png differ diff --git a/ndf/public/vite.svg b/ndf/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/ndf/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ndf/src/App.css b/ndf/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/ndf/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/ndf/src/App.tsx b/ndf/src/App.tsx new file mode 100644 index 0000000..566d69e --- /dev/null +++ b/ndf/src/App.tsx @@ -0,0 +1,52 @@ +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider, useAuth } from './context/AuthContext'; +import { ThemeProvider } from './context/ThemeContext'; +import Dashboard from './pages/Dashboard'; +import AuthCallback from './pages/AuthCallback'; +import PrivateRoute from './components/PrivateRoute'; +import UploadMobile from './pages/UploadMobile'; +import RoleSelector from './components/RoleSelector'; +import Login from './pages/Login'; + +// ── Garde intermédiaire pour la sélection de rôle ───── +const AppRoutes = (): JSX.Element => { + const { user, needsRoleSelection, selectRole, allRoles } = useAuth(); + + if (user && needsRoleSelection) { + return ( + + ); + } + + return ( + + + } /> + } /> + } /> + + + + } /> + } /> + + + ); +}; + +function App(): JSX.Element { + return ( + + + + + + ); +} + +export default App; \ No newline at end of file diff --git a/ndf/src/assets/react.svg b/ndf/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/ndf/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ndf/src/components/Dashboard.css b/ndf/src/components/Dashboard.css new file mode 100644 index 0000000..ff7b30c --- /dev/null +++ b/ndf/src/components/Dashboard.css @@ -0,0 +1,370 @@ +.dashboard-container { + min-height: 100vh; + background: #f5f7fa; +} + +.navbar { + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + padding: 1rem 0; + position: sticky; + top: 0; + z-index: 100; +} + +.navbar-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.navbar h1 { + font-size: 1.5rem; + color: #667eea; +} + +.user-section { + display: flex; + align-items: center; + gap: 1rem; +} + +.user-name { + font-weight: 500; + color: #333; +} + +.logout-btn { + padding: 8px 16px; + background: #667eea; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s; +} + + .logout-btn:hover { + background: #5568d3; + } + +.dashboard-content { + max-width: 1200px; + margin: 0 auto; + padding: 2rem 20px; +} + +.stats-card { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2rem; + margin-bottom: 2rem; +} + +.stat-item { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.stat-label { + font-size: 0.9rem; + color: #666; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: #667eea; +} + +.add-btn { + padding: 12px 24px; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + transition: all 0.3s; +} + + .add-btn:hover { + background: #5568d3; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + } + +.form-card { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 2rem; + animation: slideDown 0.3s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.form-card h2 { + margin-bottom: 1.5rem; + color: #333; +} + +.form-group { + margin-bottom: 1.5rem; +} + + .form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #333; + } + + .form-group input, + .form-group select, + .form-group textarea { + width: 100%; + padding: 12px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.3s; + } + + .form-group input:focus, + .form-group select:focus, + .form-group textarea:focus { + outline: none; + border-color: #667eea; + } + +.form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; +} + +.form-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 2rem; +} + +.cancel-btn { + padding: 12px 24px; + background: #f0f0f0; + color: #333; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s; +} + + .cancel-btn:hover { + background: #e0e0e0; + } + +.submit-btn { + padding: 12px 24px; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + transition: all 0.3s; +} + + .submit-btn:hover { + background: #5568d3; + } + +.expenses-list { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + + .expenses-list h2 { + margin-bottom: 1.5rem; + color: #333; + } + +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: #666; +} + + .empty-state p { + margin-bottom: 1rem; + font-size: 1.1rem; + } + +.expenses-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; +} + +.expense-card { + border: 2px solid #f0f0f0; + border-radius: 12px; + padding: 1.5rem; + transition: all 0.3s; + position: relative; +} + + .expense-card:hover { + border-color: #667eea; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1); + transform: translateY(-4px); + } + +.expense-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.expense-category { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.category-icon { + font-size: 1.5rem; +} + +.category-label { + font-size: 0.9rem; + color: #666; + font-weight: 500; +} + +.status-badge { + padding: 4px 12px; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; +} + +.status-pending { + background: #fff4e6; + color: #d97706; +} + +.status-approved { + background: #d1fae5; + color: #059669; +} + +.status-rejected { + background: #fee2e2; + color: #dc2626; +} + +.expense-card h3 { + font-size: 1.1rem; + margin-bottom: 0.5rem; + color: #333; +} + +.expense-description { + color: #666; + font-size: 0.9rem; + margin-bottom: 1rem; + line-height: 1.5; +} + +.expense-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 1rem; + border-top: 1px solid #f0f0f0; + margin-bottom: 1rem; +} + +.expense-amount { + font-size: 1.3rem; + font-weight: 700; + color: #667eea; +} + +.expense-date { + font-size: 0.85rem; + color: #999; +} + +.delete-btn { + width: 100%; + padding: 8px; + background: #fee2e2; + color: #dc2626; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s; +} + + .delete-btn:hover { + background: #fecaca; + } + +/* Responsive */ +@media (max-width: 768px) { + .navbar-content { + flex-direction: column; + gap: 1rem; + } + + .user-section { + flex-direction: column; + width: 100%; + } + + .logout-btn { + width: 100%; + } + + .stats-card { + grid-template-columns: 1fr; + } + + .form-row { + grid-template-columns: 1fr; + } + + .expenses-grid { + grid-template-columns: 1fr; + } +} diff --git a/ndf/src/components/Login.css b/ndf/src/components/Login.css new file mode 100644 index 0000000..1b79aa6 --- /dev/null +++ b/ndf/src/components/Login.css @@ -0,0 +1,143 @@ +.login-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 20px; +} + +.login-card { + background: white; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 500px; + width: 100%; + overflow: hidden; + animation: slideUp 0.5s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.login-header { + background: linear-gradient(135deg, #f5f5dc 0%, #e8d7c3 100%); + color: white; + padding: 40px 30px; + text-align: center; +} + + .login-header h1 { + font-size: 2rem; + margin-bottom: 10px; + font-weight: 700; + } + + .login-header p { + font-size: 1rem; + opacity: 0.9; + } + +.login-content { + padding: 40px 30px; +} + +.feature-list { + margin-bottom: 30px; +} + +.feature-item { + display: flex; + align-items: center; + gap: 15px; + padding: 12px 0; + font-size: 0.95rem; + color: #333; +} + +.feature-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.microsoft-login-btn { + width: 100%; + padding: 16px 24px; + background: white; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + color: #5e5e5e; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + transition: all 0.3s ease; + margin-bottom: 20px; +} + + .microsoft-login-btn:hover { + border-color: #667eea; + background: #f8f9ff; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2); + } + +.microsoft-icon { + width: 21px; + height: 21px; +} + +.login-info { + text-align: center; + color: #666; + font-size: 0.85rem; + line-height: 1.5; +} + +/* Responsive */ +@media (max-width: 600px) { + .login-card { + margin: 10px; + } + + .login-header { + padding: 30px 20px; + } + + .login-header h1 { + font-size: 1.5rem; + } + + .login-content { + padding: 30px 20px; + } + + .feature-item { + font-size: 0.9rem; + } + + .microsoft-login-btn { + padding: 14px 20px; + font-size: 0.95rem; + } +} +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} diff --git a/ndf/src/components/PrivateRoute.tsx b/ndf/src/components/PrivateRoute.tsx new file mode 100644 index 0000000..4672a84 --- /dev/null +++ b/ndf/src/components/PrivateRoute.tsx @@ -0,0 +1,25 @@ +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { ReactNode } from 'react'; + +interface PrivateRouteProps { + children: ReactNode; + /** Si fourni, accès restreint aux rôles listés */ + roles?: string[]; +} + +const PrivateRoute = ({ children, roles }: PrivateRouteProps): JSX.Element => { + const { user, hasRole } = useAuth(); + + if (!user) { + return ; + } + + if (roles && roles.length > 0 && !hasRole(...roles)) { + return ; + } + + return <>{children}; +}; + +export default PrivateRoute; \ No newline at end of file diff --git a/ndf/src/components/RoleSelector.tsx b/ndf/src/components/RoleSelector.tsx new file mode 100644 index 0000000..d868c85 --- /dev/null +++ b/ndf/src/components/RoleSelector.tsx @@ -0,0 +1,293 @@ +import { useState } from 'react'; + +// ── TYPES ────────────────────────────────────────────── +interface RoleSelectorProps { + roles: string[]; + userName: string; + onSelect: (role: string) => void; +} + +// ── CONFIG DES RÔLES ─────────────────────────────────── +const ROLE_CONFIG: Record = { + Collaborateur: { + label: 'Collaborateur', + sublabel: 'Soumettre & suivre mes notes', + icon: '👤', + gradient: 'linear-gradient(135deg, #6366f1 0%, #4f46e5 100%)', + accent: '#6366f1', + features: ['Créer des notes de frais', 'Suivre mes remboursements', 'Consulter mon historique'], + }, + Collaboratrice: { + label: 'Collaboratrice', + sublabel: 'Soumettre & suivre mes notes', + icon: '👤', + gradient: 'linear-gradient(135deg, #6366f1 0%, #4f46e5 100%)', + accent: '#6366f1', + features: ['Créer des notes de frais', 'Suivre mes remboursements', 'Consulter mon historique'], + }, + Validateur: { + label: 'Validateur', + sublabel: 'Gérer les demandes à valider', + icon: '✅', + gradient: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)', + accent: '#0ea5e9', + features: ['Valider / refuser des notes', 'Consulter les justificatifs', 'Historique de mes décisions'], + }, + Validatrice: { + label: 'Validatrice', + sublabel: 'Gérer les demandes à valider', + icon: '✅', + gradient: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)', + accent: '#0ea5e9', + features: ['Valider / refuser des notes', 'Consulter les justificatifs', 'Historique de mes décisions'], + }, + Finance: { + label: 'Finance', + sublabel: 'Paiements & administration', + icon: '💼', + gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', + accent: '#10b981', + features: ['Gestion des paiements', 'Export XML virements', 'Paramètres TVA & km', 'Synchronisation Entra ID'], + }, + superUtilisateur: { + label: 'Super Utilisateur', + sublabel: 'Supervision globale — lecture seule', + icon: '🛡️', + gradient: 'linear-gradient(135deg, #7c3aed 0%, #4c1d95 100%)', + accent: '#7c3aed', + features: ['Voir toutes les notes de l\'organisation', 'Filtrer par collaborateur, statut, mois', 'Visualiser les montants globaux'], + }, +}; + +// Normalise un rôle vers sa clé de config +const normalizeRole = (role: string): string => { + const map: Record = { + collaborateur: 'Collaborateur', + collaboratrice: 'Collaboratrice', + validateur: 'Validateur', + validatrice: 'Validatrice', + finance: 'Finance', + }; + return map[role.toLowerCase()] ?? role; +}; + +// ── COMPOSANT ────────────────────────────────────────── +const RoleSelector = ({ roles, userName, onSelect }: RoleSelectorProps) => { + const [hovered, setHovered] = useState(null); + const [selected, setSelected] = useState(null); + + const uniqueRoles = [...new Set(roles.map(normalizeRole))]; + const initials = userName.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2); + + const handleSelect = (role: string) => { + setSelected(role); + setTimeout(() => onSelect(role), 280); + }; + + return ( +
+ {/* Fond décoratif */} +
+
+ + {/* Contenu */} +
+ + {/* En-tête */} +
+
📋
+ +
+
{initials}
+ + {userName} + +
+ +

+ Choisir votre mode +

+

+ Votre compte dispose de {uniqueRoles.length} profils — sélectionnez celui à activer +

+
+ + {/* Cartes de rôles */} +
+ {uniqueRoles.map(role => { + const cfg = ROLE_CONFIG[role] ?? { + label: role, sublabel: '', icon: '🔑', + gradient: 'linear-gradient(135deg,#64748b,#475569)', + accent: '#64748b', features: [], + }; + const isHovered = hovered === role; + const isSelected = selected === role; + + return ( + + ); + })} +
+ + {/* Footer */} +

+ Vous pourrez changer de mode à tout moment depuis l'interface +

+
+ + +
+ ); +}; + +export default RoleSelector; +export { ROLE_CONFIG, normalizeRole }; +export type { RoleSelectorProps }; \ No newline at end of file diff --git a/ndf/src/components/RoleSwitcher.tsx b/ndf/src/components/RoleSwitcher.tsx new file mode 100644 index 0000000..ba0a626 --- /dev/null +++ b/ndf/src/components/RoleSwitcher.tsx @@ -0,0 +1,191 @@ +import { useState } from 'react'; +import { useAuth } from '../context/AuthContext'; + +// ── CONFIG ROLES (couleurs & icônes) ────────────────── +const ROLE_STYLE: Record = { + Collaborateur: { icon: '👤', color: '#a5b4fc', bg: 'rgba(99,102,241,0.2)', label: 'Collaborateur' }, + Collaboratrice: { icon: '👤', color: '#a5b4fc', bg: 'rgba(99,102,241,0.2)', label: 'Collaboratrice' }, + Validateur: { icon: '✅', color: '#7dd3fc', bg: 'rgba(14,165,233,0.2)', label: 'Validateur' }, + Validatrice: { icon: '✅', color: '#7dd3fc', bg: 'rgba(14,165,233,0.2)', label: 'Validatrice' }, + Finance: { icon: '💼', color: '#6ee7b7', bg: 'rgba(16,185,129,0.2)', label: 'Finance' }, + superUtilisateur: { icon: '🛡️', color: '#c4b5fd', bg: 'rgba(124,58,237,0.2)', label: 'Super Utilisateur' }, + VerificateurFinance: { icon: '🔍', color: '#c4b5fd', bg: 'rgba(124,58,237,0.2)', label: 'Vérificateur Finance' }, + ValidateurFinance: { icon: '💳', color: '#fca5a5', bg: 'rgba(185,28,28,0.2)', label: 'Validateur Finance' }, +}; + +const getStyle = (role: string) => + ROLE_STYLE[role] ?? { icon: '🔑', color: '#cbd5e1', bg: 'rgba(100,116,139,0.2)', label: role }; + +// ══════════════════════════════════════════════════════ +// VERSION SIDEBAR (intégrée dans la sidebar existante) +// ══════════════════════════════════════════════════════ +export const RoleSwitcherSidebar = () => { + const { activeRole, allRoles, switchRole, isMultiRole } = useAuth(); + const [open, setOpen] = useState(false); + + if (!isMultiRole || !activeRole) return null; + + const current = getStyle(activeRole); + const others = allRoles.filter(r => r !== activeRole); + + return ( +
+ {/* Bouton mode actif */} + + + {/* Dropdown des autres rôles */} + {open && ( +
+
+ Changer de mode +
+ {others.map(role => { + const s = getStyle(role); + return ( + + ); + })} +
+ )} +
+ ); +}; + +// ══════════════════════════════════════════════════════ +// VERSION BADGE (dans le header, compact) +// ══════════════════════════════════════════════════════ +export const RoleSwitcherBadge = () => { + const { activeRole, allRoles, switchRole, isMultiRole } = useAuth(); + const [open, setOpen] = useState(false); + + if (!activeRole) return null; + + const current = getStyle(activeRole); + const others = allRoles.filter(r => r !== activeRole); + + return ( +
+ + + {open && isMultiRole && ( + <> + {/* Overlay pour fermer */} +
setOpen(false)} + /> +
+
+ Changer de mode +
+ {others.map(role => { + const s = getStyle(role); + return ( + + ); + })} +
+ + )} +
+ ); +}; + +// Export default = version sidebar (la plus courante) +export default RoleSwitcherSidebar; \ No newline at end of file diff --git a/ndf/src/components/ndf/FileUploadZone.tsx b/ndf/src/components/ndf/FileUploadZone.tsx new file mode 100644 index 0000000..6e61711 --- /dev/null +++ b/ndf/src/components/ndf/FileUploadZone.tsx @@ -0,0 +1,101 @@ +import React, { useRef, useState } from 'react'; +import { Upload, FileText, Receipt } from 'lucide-react'; + +interface FileUploadZoneProps { + files: File[]; + setFiles: (f: File[]) => void; +} + +const fileSize = (b: number) => + b < 1048576 ? `${(b / 1024).toFixed(1)} Ko` : `${(b / 1048576).toFixed(1)} Mo`; + +export const FileUploadZone = ({ files, setFiles }: FileUploadZoneProps) => { + const inputRef = useRef(null); + const cameraRef = useRef(null); + const [dragOver, setDragOver] = useState(false); + const allowed = ['image/jpeg', 'image/jpg', 'image/png', 'application/pdf']; + + const add = (list: FileList | null) => { + if (!list) return; + const filtered = Array.from(list).filter(f => allowed.includes(f.type)); + setFiles([...files, ...filtered]); + }; + + return ( +
+
{ e.preventDefault(); setDragOver(true); }} + onDragLeave={() => setDragOver(false)} + onDrop={e => { e.preventDefault(); setDragOver(false); add(e.dataTransfer.files); }} + onClick={() => inputRef.current?.click()} + style={{ + border: `2px dashed ${dragOver ? '#6366f1' : '#cbd5e1'}`, + borderRadius: 10, padding: '28px 20px', textAlign: 'center', + cursor: 'pointer', + background: dragOver ? 'rgba(99,102,241,.05)' : '#f8fafc', + transition: 'all 0.2s', + }}> +
+ +
+
+ Glissez vos fichiers ici +
+
+ ou cliquez pour parcourir +
+
+ PDF, JPG, PNG — max 10 Mo +
+ add(e.target.files)} style={{ display: 'none' }} /> +
+ + + add(e.target.files)} style={{ display: 'none' }} /> + + {files.length > 0 && ( +
+ {files.map((f, i) => ( +
+ + {f.type === 'application/pdf' + ? + : } + +
+
{f.name}
+
{fileSize(f.size)}
+
+ {f.type.startsWith('image/') && ( + {f.name} + )} + +
+ ))} +
+ )} +
+ ); +}; diff --git a/ndf/src/components/ndf/JustificatifPopup.tsx b/ndf/src/components/ndf/JustificatifPopup.tsx new file mode 100644 index 0000000..4a9ce71 --- /dev/null +++ b/ndf/src/components/ndf/JustificatifPopup.tsx @@ -0,0 +1,88 @@ +import React from 'react'; + +interface JustificatifPopupProps { + file: File | null; + onClose: () => void; +} + +export const JustificatifPopup = ({ file, onClose }: JustificatifPopupProps) => { + if (!file) return null; + const url = URL.createObjectURL(file); + const isPDF = file.type === 'application/pdf'; + const isImage = file.type.startsWith('image/'); + + return ( +
+
e.stopPropagation()} style={{ + background: '#fff', borderRadius: 16, + boxShadow: '0 24px 80px rgba(0,0,0,.4)', + maxWidth: '90vw', maxHeight: '90vh', + width: isPDF ? 800 : 'auto', + display: 'flex', flexDirection: 'column', overflow: 'hidden', + }}> + {/* Header */} +
+
+ {isPDF ? '📄' : '🖼️'} +
+
{file.name}
+
+ {file.type} — {file.size < 1048576 + ? `${(file.size / 1024).toFixed(1)} Ko` + : `${(file.size / 1048576).toFixed(1)} Mo`} +
+
+
+ +
+ {/* Body */} +
+ {isPDF && ( +