diff --git a/Backend/.env b/Backend/.env
new file mode 100644
index 0000000..8ac3f59
--- /dev/null
+++ b/Backend/.env
@@ -0,0 +1,31 @@
+# Configuration Base de données MySQL
+DB_SERVER=192.168.0.4
+DB_DATABASE=DemandeConge
+DB_PASSWORD=-2b/)ru5/Bi8P[7_
+DB_ENCRYPT=true
+DB_TRUST_SERVER_CERTIFICATE=true
+PORT=3000
+
+COLLABORATEURS_URL=http://localhost:3000
+RH_URL=http://localhost:3001
+WEBHOOK_SECRET=secret-rh-2025
+
+
+
+# Configuration Serveur
+PORT=3001
+NODE_ENV=development
+
+# Configuration Azure AD (Microsoft Graph)
+AZURE_TENANT_ID=9840a2a0-6ae1-4688-b03d-d2ec291be0f9
+AZURE_CLIENT_ID=4bb4cc24-bac3-427c-b02c-5d14fc67b561
+AZURE_CLIENT_SECRET=o2q8Q~nGLCkry6XOriVFBMvKUk.5cXtFutAQVdx9
+AZURE_GROUP_ID=c1ea877c-6bca-4f47-bfad-f223640813a0
+JWT_SECRET=o2q8Q~nGLCkry6XOriVFBMvKUk.5cXtFutAQVdx9
+
+# Email configuration
+EMAIL_FROM=gtanoreply@ensup.eu
+
+# Upload configuration
+UPLOAD_DIR=./uploads
+MAX_FILE_SIZE=5242880
\ No newline at end of file
diff --git a/Backend/DockerFileGTARH.backend b/Backend/DockerFileGTARH.backend
new file mode 100644
index 0000000..3622529
--- /dev/null
+++ b/Backend/DockerFileGTARH.backend
@@ -0,0 +1,8 @@
+FROM node:18-alpine
+RUN apk add --no-cache curl mysql-client python3 make g++
+WORKDIR /app
+COPY package*.json ./
+RUN npm install --production
+COPY . .
+EXPOSE 3000
+CMD ["node", "server.js"]
\ No newline at end of file
diff --git a/Backend/package-lock.json b/Backend/package-lock.json
new file mode 100644
index 0000000..ce2bb20
--- /dev/null
+++ b/Backend/package-lock.json
@@ -0,0 +1,1840 @@
+{
+ "name": "gtarh-backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "gtarh-backend",
+ "version": "1.0.0",
+ "dependencies": {
+ "@azure/msal-node": "^3.8.0",
+ "axios": "^1.12.2",
+ "cors": "^2.8.5",
+ "crypto": "^1.0.1",
+ "dotenv": "^17.2.3",
+ "express": "^5.1.0",
+ "jsonwebtoken": "^9.0.2",
+ "mysql2": "^3.15.1",
+ "node-fetch": "^2.7.0",
+ "pdfkit": "^0.17.2"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.10"
+ }
+ },
+ "node_modules/@azure/msal-common": {
+ "version": "15.13.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz",
+ "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@azure/msal-node": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.0.tgz",
+ "integrity": "sha512-23BXm82Mp5XnRhrcd4mrHa0xuUNRp96ivu3nRatrfdAqjoeWAGyD0eEAafxAOHAEWWmdlyFK4ELFcdziXyw2sA==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/msal-common": "15.13.0",
+ "jsonwebtoken": "^9.0.0",
+ "uuid": "^8.3.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
+ "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/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/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "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/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/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-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/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/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/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/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "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.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.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/crypto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
+ "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
+ "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
+ "license": "ISC"
+ },
+ "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": "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/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/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "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/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.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "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/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/express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.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/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "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": ">= 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.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "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/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/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.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-errors/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-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-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "license": "MIT"
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "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/jwa": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
+ "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
+ "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": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^1.4.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/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/lru.min": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
+ "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mysql2": {
+ "version": "3.15.1",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.1.tgz",
+ "integrity": "sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==",
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.1",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.0",
+ "long": "^5.2.1",
+ "lru.min": "^1.0.0",
+ "named-placeholders": "^1.1.3",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.2"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/mysql2/node_modules/iconv-lite": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+ "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/named-placeholders": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
+ "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
+ "license": "MIT",
+ "dependencies": {
+ "lru-cache": "^7.14.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "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-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/nodemon": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/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-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/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/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/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/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/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
+ "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.7.0",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+ "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/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/restructure": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
+ "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
+ "license": "MIT"
+ },
+ "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/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.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "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/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sqlstring": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+ "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/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/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "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/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/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "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/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/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/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"
+ }
+ }
+}
diff --git a/Backend/package.json b/Backend/package.json
new file mode 100644
index 0000000..e4cc13e
--- /dev/null
+++ b/Backend/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "gtarh-backend",
+ "version": "1.0.0",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "nodemon server.js"
+ },
+ "dependencies": {
+ "@azure/msal-node": "^3.8.0",
+ "axios": "^1.12.2",
+ "cors": "^2.8.5",
+ "crypto": "^1.0.1",
+ "dotenv": "^17.2.3",
+ "express": "^5.1.0",
+ "jsonwebtoken": "^9.0.2",
+ "mysql2": "^3.15.1",
+ "node-fetch": "^2.7.0",
+ "pdfkit": "^0.17.2"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.10"
+ }
+}
diff --git a/Backend/server.js b/Backend/server.js
new file mode 100644
index 0000000..fc29f6d
--- /dev/null
+++ b/Backend/server.js
@@ -0,0 +1,4263 @@
+console.log('🚀 1. Démarrage du script...');
+
+const express = require('express');
+const cors = require('cors');
+const mysql = require('mysql2/promise');
+const jwt = require('jsonwebtoken');
+const PDFDocument = require('pdfkit');
+
+const { ConfidentialClientApplication } = require('@azure/msal-node');
+
+console.log('✅ 2. Modules de base chargés');
+
+require('dotenv').config();
+console.log('✅ 3. Dotenv chargé');
+
+// HANDLERS D'ERREURS
+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);
+ console.error('Promise:', promise);
+});
+
+process.on('exit', (code) => {
+ console.log(`\n⚠️⚠️⚠️ PROCESSUS EN COURS DE TERMINAISON - CODE: ${code} ⚠️⚠️⚠️\n`);
+});
+
+console.log('✅ 4. Handlers d\'erreurs installés');
+
+// ⭐ IMPORTS WEBHOOKS
+// ⭐ IMPORTS WEBHOOKS
+try {
+ console.log('🔄 5. Chargement WebhookManager...');
+ const WebhookManager = require('./webhook-utils.js');
+ console.log(' Type de WebhookManager:', typeof WebhookManager);
+
+ console.log('🔄 6. Chargement webhook-config...');
+ const { WEBHOOKS, EVENTS } = require('./webhook-config');
+ console.log('✅ 7. Webhooks chargés avec succès');
+
+ const app = express();
+ console.log('✅ 8. Express initialisé');
+
+ const PORT = process.env.PORT || 3001;
+ console.log(`✅ 9. Port configuré: ${PORT}`);
+
+ // ⭐ INITIALISER LE WEBHOOK MANAGER
+ console.log('🔄 10. Initialisation WebhookManager...');
+ const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY);
+ console.log('✅ 11. WebhookManager créé');
+
+ // Middleware
+ app.use(cors());
+ app.use(express.json());
+ console.log('✅ 12. Middlewares installés');
+
+ // Configuration MySQL
+ const dbConfig = {
+ host: process.env.DB_SERVER || '192.168.0.4',
+ user: process.env.DB_USER || 'wpuser',
+ password: process.env.DB_PASSWORD,
+ database: process.env.DB_DATABASE || 'DemandeConge',
+ waitForConnections: true,
+ connectionLimit: 10
+ };
+
+ console.log('🔄 13. Test connexion MySQL...');
+ const pool = mysql.createPool(dbConfig);
+
+ // TEST DE CONNEXION IMMÉDIAT
+ pool.getConnection()
+ .then(conn => {
+ console.log('✅ 14. Connexion MySQL réussie');
+ conn.release();
+ })
+ .catch(err => {
+ console.error('❌ 14. ERREUR CONNEXION MYSQL:', err.message);
+ console.error(' Host:', dbConfig.host);
+ console.error(' User:', dbConfig.user);
+ console.error(' Database:', dbConfig.database);
+ });
+
+ console.log('✅ 15. Pool MySQL créé');
+
+ // Configuration Microsoft O365
+ 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
+ }
+ };
+
+ const cca = new ConfidentialClientApplication(msalConfig);
+
+ async function getGraphToken() {
+ try {
+ console.log('🔑 Demande token Graph API...');
+ const params = new URLSearchParams({
+ grant_type: 'client_credentials',
+ client_id: process.env.AZURE_CLIENT_ID,
+ client_secret: process.env.AZURE_CLIENT_SECRET,
+ scope: 'https://graph.microsoft.com/.default'
+ });
+
+ const response = await fetch(
+ `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: params.toString()
+ }
+ );
+
+ console.log('📊 Réponse OAuth - Status:', response.status);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('❌ Erreur OAuth:', errorText);
+ throw new Error(`OAuth Error: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ if (data.access_token) {
+ console.log('✅ Token obtenu');
+ return data.access_token;
+ } else {
+ console.error('❌ Pas de token dans la réponse:', data);
+ return null;
+ }
+ } catch (error) {
+ console.error('❌ Erreur getGraphToken:', error.message);
+ return null;
+ }
+ }
+
+ async function sendMailGraph(accessToken, fromEmail, toEmail, subject, bodyHtml) {
+ try {
+ console.log('📤 Envoi email via Graph API...');
+ console.log(' From:', fromEmail);
+ console.log(' To:', toEmail);
+ console.log(' Subject:', subject);
+
+ const response = await fetch(
+ `https://graph.microsoft.com/v1.0/users/${fromEmail}/sendMail`,
+ {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ message: {
+ subject,
+ body: { contentType: 'HTML', content: bodyHtml },
+ toRecipients: [{ emailAddress: { address: toEmail } }]
+ },
+ saveToSentItems: false
+ })
+ }
+ );
+
+ console.log('📊 Réponse Graph API - Status:', response.status);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('❌ Erreur Graph API:', errorText);
+ throw new Error(`Graph API Error: ${response.status} - ${errorText}`);
+ }
+
+ console.log('✅ Email envoyé avec succès');
+ return true;
+ } catch (error) {
+ console.error('❌ Erreur sendMailGraph:', error.message);
+ return false;
+ }
+ }
+
+ // Middleware d'authentification
+ const authenticateToken = (req, res, next) => {
+ const token = req.headers['authorization']?.split(' ')[1];
+ if (!token) return res.status(401).json({ error: 'Token requis' });
+
+ jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
+ if (err) return res.status(403).json({ error: 'Token invalide' });
+ req.user = user;
+ next();
+ });
+ };
+
+ // ================================================
+ // GESTION DES SERVER-SENT EVENTS (SSE)
+ // ================================================
+
+ // ================================================
+ // GESTION DES SERVER-SENT EVENTS (SSE)
+ // ================================================
+
+ const sseClients = new Set();
+
+ // 🔌 ROUTE SSE POUR LE CALENDRIER
+ // ROUTE SSE POUR LE CALENDRIER RH
+ app.get('/events', (req, res) => {
+ const { token, userid } = req.query;
+
+ let userId = userid;
+
+ // ✅ Si token fourni, extraire l'ID utilisateur
+ if (token && !userId) {
+ try {
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+ userId = decoded.id;
+ console.log('🔓 Token JWT décodé, userId:', userId);
+ } catch (error) {
+ console.error('❌ Token invalide:', error.message);
+ return res.status(401).json({ error: 'Token invalide' });
+ }
+ }
+
+ if (!userId) {
+ console.error('❌ Ni userid ni token fourni');
+ return res.status(400).json({ error: 'userid ou token requis' });
+ }
+
+ console.log('🔗 Nouvelle connexion SSE (RH):', userId);
+
+ // Headers SSE critiques
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('X-Accel-Buffering', 'no');
+
+ // Flush pour établir la connexion
+ res.flushHeaders();
+
+ const sendEvent = (data) => {
+ try {
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
+ } catch (error) {
+ console.error('Erreur envoi SSE:', error);
+ }
+ };
+
+ const client = { id: userId, send: sendEvent };
+ sseClients.add(client);
+ console.log(`📊 Clients SSE connectés: ${sseClients.size}`);
+
+ // Envoyer un heartbeat initial
+ sendEvent({ type: 'ping', message: 'Connexion établie', timestamp: new Date().toISOString() });
+
+ // Heartbeat toutes les 30 secondes
+ const heartbeat = setInterval(() => {
+ try {
+ sendEvent({ type: 'ping', timestamp: new Date().toISOString() });
+ } catch (error) {
+ clearInterval(heartbeat);
+ }
+ }, 30000);
+
+ // Gérer la déconnexion
+ req.on('close', () => {
+ console.log('🔌 Déconnexion SSE (RH):', userId);
+ clearInterval(heartbeat);
+ sseClients.delete(client);
+ console.log(`📊 Clients SSE connectés: ${sseClients.size}`);
+ });
+ });
+
+
+ // 📢 FONCTION POUR NOTIFIER LES CLIENTS
+ const notifyClients = (event, userId = null) => {
+ console.log(`📢 Notification SSE: ${event.type}${userId ? ` pour ${userId}` : ''}`);
+
+ sseClients.forEach(client => {
+ if (userId && client.id !== userId) {
+ return;
+ }
+
+ try {
+ client.send(event);
+ } catch (error) {
+ console.error('❌ Erreur envoi event:', error);
+ }
+ });
+ };
+
+ // ================================================
+ // ROUTE WEBHOOK RECEIVER
+ // ================================================
+
+ app.post('/webhook/receive', async (req, res) => {
+ try {
+ const signature = req.headers['x-webhook-signature'];
+ const payload = req.body;
+
+ console.log('📥 Webhook reçu du serveur RH:', payload.event);
+
+ if (!webhookManager.verifySignature(payload, signature)) {
+ console.error('❌ Signature webhook invalide');
+ return res.status(401).json({ error: 'Signature invalide' });
+ }
+
+ const { event, data } = payload;
+
+ console.log('✅ Signature valide ! Traitement...');
+ console.log(' Event:', event);
+ console.log(' Data:', data);
+
+ switch (event) {
+ case EVENTS.DEMANDE_VALIDATED:
+ console.log(`\n✅ === WEBHOOK DEMANDE_VALIDATED REÇU ===`);
+ console.log(` Demande: ${data.demandeId}`);
+ console.log(` Statut: ${data.statut}`);
+ console.log(` Type: ${data.type || data.typeConge}`);
+ console.log(` Couleur: ${data.couleurHex}`);
+
+ notifyClients({
+ type: 'demande-validated',
+ demandeId: data.demandeId,
+ statut: data.statut,
+ typeConge: data.typeConge || data.type,
+ type: data.type || data.typeConge,
+ couleurHex: data.couleurHex || '#d946ef',
+ date: data.date,
+ periode: data.periode,
+ collaborateurId: data.collaborateurId,
+ timestamp: new Date().toISOString()
+ }, data.collaborateurId);
+
+ console.log(` ✅ SSE envoyé au collaborateur ${data.collaborateurId}`);
+ break;
+
+ // 🆕 AJOUTER CE CAS POUR LES ANNULATIONS
+ case EVENTS.DEMANDE_CANCELLED:
+ console.log(`\n🔴 === WEBHOOK DEMANDE_CANCELLED REÇU ===`);
+ console.log(` Demande: ${data.demandeId}`);
+ console.log(` Annulée par: ${data.annulateurRole}`);
+ console.log(` Commentaire: ${data.commentaire || 'Aucun'}`);
+
+ // Notifier les clients SSE
+ notifyClients({
+ type: 'demande-cancelled',
+ demandeId: data.demandeId,
+ statut: 'Annulée',
+ collaborateurId: data.collaborateurId,
+ annulateurRole: data.annulateurRole,
+ commentaire: data.commentaire,
+ timestamp: new Date().toISOString()
+ });
+
+ // Notifier spécifiquement le collaborateur concerné
+ notifyClients({
+ type: 'demande-updated',
+ demandeId: data.demandeId,
+ nouveauStatut: 'Annulée',
+ timestamp: new Date().toISOString()
+ }, data.collaborateurId);
+
+ console.log(` ✅ SSE envoyé - Demande ${data.demandeId} marquée comme annulée`);
+ break;
+
+ case EVENTS.COMPTEUR_UPDATED:
+ console.log(`\n🔄 === WEBHOOK COMPTEUR_UPDATED REÇU ===`);
+ console.log(` Collaborateur: ${data.collaborateurId}`);
+
+ notifyClients({
+ type: 'compteur-updated',
+ collaborateurId: data.collaborateurId,
+ typeConge: data.typeConge,
+ typeUpdate: data.typeUpdate,
+ jours: data.jours,
+ timestamp: new Date().toISOString()
+ }, data.collaborateurId);
+
+ console.log(` ✅ SSE envoyé`);
+ break;
+
+ case EVENTS.DEMANDE_UPDATED:
+ console.log(`✏️ Demande ${data.demandeId} modifiée via RH`);
+
+ notifyClients({
+ type: 'demande-updated-rh',
+ demandeId: data.demandeId,
+ timestamp: new Date().toISOString()
+ }, data.collaborateurId);
+ break;
+
+ case EVENTS.DEMANDE_DELETED:
+ console.log(`🗑️ Demande ${data.demandeId} supprimée via RH`);
+
+ notifyClients({
+ type: 'demande-deleted-rh',
+ demandeId: data.demandeId,
+ timestamp: new Date().toISOString()
+ }, data.collaborateurId);
+ break;
+
+ default:
+ console.warn(`⚠️ Type d'événement webhook inconnu: ${event}`);
+ }
+
+ res.json({ success: true, message: 'Webhook traité' });
+
+ } catch (error) {
+ console.error('❌ Erreur traitement webhook:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ // ================================================
+ // ROUTES D'AUTHENTIFICATION
+ // ================================================
+
+ app.get('/users-dev', async (req, res) => {
+ try {
+ // On récupère juste l'essentiel pour le sélecteur
+ const [users] = await pool.query(`
+ SELECT id, email, nom, prenom, role, service, Actif
+ FROM CollaborateurAD
+ WHERE Actif = 1 OR Actif IS NULL
+ ORDER BY nom, prenom
+ `);
+ res.json(users);
+ } catch (error) {
+ console.error('❌ Erreur users-dev:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+ app.post('/login-dev', async (req, res) => {
+ try {
+ console.log('🔐 Login attempt started');
+ const { accessToken } = req.body;
+
+ if (!accessToken) {
+ console.error('❌ No access token provided');
+ return res.status(400).json({ error: 'Token d\'accès manquant' });
+ }
+
+ console.log('👤 Validating access token...');
+
+ 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();
+ console.log('✅ Token validated, user:', userInfo.mail || userInfo.userPrincipalName);
+ } catch (graphError) {
+ console.error('❌ Graph API Error:', graphError);
+ return res.status(401).json({
+ error: 'Token d\'accès invalide',
+ details: graphError.message
+ });
+ }
+
+ const userEmail = userInfo.mail || userInfo.userPrincipalName;
+
+ // ✅ MODIFICATION: Ajouter le filtre Actif
+ const [users] = await pool.query(
+ `SELECT * FROM CollaborateurAD
+ WHERE email = ?
+ AND (Actif = 1 OR Actif IS NULL)`,
+ [userEmail]
+ );
+
+ if (users.length === 0) {
+ console.warn('⚠️ User not found or inactive:', userEmail);
+ return res.status(404).json({
+ error: 'Utilisateur non trouvé ou compte désactivé',
+ details: `Aucun utilisateur actif avec l'email ${userEmail}`
+ });
+ }
+
+ const user = users[0];
+ console.log('👤 User found:', user.email, 'Role:', user.role);
+
+ if (!['RH', 'Admin', 'Validateur'].includes(user.role)) {
+ console.warn('⚠️ Unauthorized role:', user.role);
+ return res.status(403).json({
+ error: 'Accès non autorisé',
+ details: `Le rôle '${user.role}' n'a pas accès à cette application`
+ });
+ }
+
+ const token = jwt.sign(
+ { id: user.id, email: user.email, role: user.role },
+ process.env.JWT_SECRET,
+ { expiresIn: '8h' }
+ );
+
+ console.log('✅ Login successful for:', user.email);
+ res.json({
+ token,
+ user: {
+ id: user.id,
+ nom: user.nom,
+ prenom: user.prenom,
+ role: user.role
+ }
+ });
+ } catch (error) {
+ console.error('❌ Unexpected error in /api/auth/login:', error);
+ res.status(500).json({
+ error: 'Erreur serveur inattendue',
+ details: error.message
+ });
+ }
+ });
+
+ // ================================================
+ // ROUTES DES DEMANDES DE CONGÉS
+ // ================================================
+
+ app.get('/demandes', authenticateToken, async (req, res) => {
+ try {
+ const { statut, dateDebut, dateFin, service } = req.query;
+ let query = `
+ SELECT dc.*,
+ CONCAT(ca.prenom, ' ', ca.nom) as nomEmploye,
+ ca.email as emailEmploye,
+ ca.Actif as employeActif,
+ s.Nom as service,
+ GROUP_CONCAT(DISTINCT tc.Nom SEPARATOR ', ') as typesConge,
+ CONCAT(v.prenom, ' ', v.nom) as nomValidateur
+ FROM DemandeConge dc
+ JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
+ LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
+ LEFT JOIN CollaborateurAD v ON dc.ValidateurADId = v.id
+ WHERE 1=1
+ `;
+ const params = [];
+
+ if (statut) {
+ query += ' AND dc.Statut = ?';
+ params.push(statut);
+ }
+ if (dateDebut) {
+ query += ' AND dc.DateDebut >= ?';
+ params.push(dateDebut);
+ }
+ if (dateFin) {
+ query += ' AND dc.DateFin <= ?';
+ params.push(dateFin);
+ }
+ if (service) {
+ query += ' AND s.Id = ?';
+ params.push(service);
+ }
+
+ query += ' GROUP BY dc.Id ORDER BY dc.DateDemande DESC';
+
+ const [demandes] = await pool.query(query, params);
+ res.json(demandes);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ app.get('/demandes/:id', authenticateToken, async (req, res) => {
+ try {
+ const [demandes] = await pool.query(`
+ SELECT
+ dc.*,
+ CONCAT(ca.prenom, ' ', ca.nom) AS nomEmploye,
+ ca.email AS emailEmploye,
+ ca.Actif AS employeActif,
+ ca.DateSortie AS employeDateSortie,
+ s.Nom AS service,
+ s.Id AS serviceId,
+ CONCAT(v.prenom, ' ', v.nom) AS nomValidateur,
+ v.email AS emailValidateur
+ FROM DemandeConge dc
+ JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ LEFT JOIN CollaborateurAD v ON dc.ValidateurADId = v.id
+ WHERE dc.Id = ?
+ `, [req.params.id]);
+
+ if (demandes.length === 0) {
+ return res.status(404).json({ error: 'Demande non trouvée' });
+ }
+
+ const [types] = await pool.query(`
+ SELECT
+ dct.TypeCongeId as typeId,
+ dct.NombreJours as nombreJours,
+ tc.Nom AS nom,
+ tc.CouleurHex AS couleur
+ FROM DemandeCongeType dct
+ JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
+ WHERE dct.DemandeCongeId = ?
+ `, [req.params.id]);
+
+ res.json({
+ ...demandes[0],
+ typesConge: types
+ });
+ } catch (error) {
+ console.error('❌ Erreur détails demande:', error);
+ console.error(' Code:', error.code);
+ console.error(' SQL:', error.sqlMessage);
+ res.status(500).json({ error: error.message, details: error.sqlMessage });
+ }
+ });
+
+ app.post('/demandes', authenticateToken, async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ const { collaborateurId, dateDebut, dateFin, typesConge, commentaire } = req.body;
+
+ // ✅ Vérifier que le collaborateur est actif
+ const [collab] = await conn.query(
+ 'SELECT Actif FROM CollaborateurAD WHERE id = ?',
+ [collaborateurId]
+ );
+
+ if (collab.length === 0 || collab[0].Actif === 0) {
+ await conn.rollback();
+ return res.status(403).json({ error: 'Impossible de créer une demande pour un compte désactivé' });
+ }
+
+ const totalJours = typesConge.reduce((sum, type) => sum + parseFloat(type.nombreJours), 0);
+
+ const [result] = await conn.query(
+ `INSERT INTO DemandeConge (CollaborateurADId, DateDebut, DateFin, Statut, Commentaire, DateDemande, NombreJours)
+ VALUES (?, ?, ?, 'En attente', ?, NOW(), ?)`,
+ [collaborateurId, dateDebut, dateFin, commentaire || '', totalJours]
+ );
+
+ const demandeId = result.insertId;
+
+ for (const type of typesConge) {
+ await conn.query(
+ 'INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours) VALUES (?, ?, ?)',
+ [demandeId, type.typeId, type.nombreJours]
+ );
+ }
+
+ await conn.commit();
+
+ notifyClients({
+ type: 'demande-created',
+ demandeId: demandeId,
+ collaborateurId: collaborateurId,
+ timestamp: new Date().toISOString()
+ });
+
+ res.json({ id: demandeId, message: 'Demande créée avec succès' });
+ } catch (error) {
+ await conn.rollback();
+ res.status(500).json({ error: error.message });
+ } finally {
+ conn.release();
+ }
+ });
+
+ app.put('/demandes/:id', authenticateToken, async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ const { id } = req.params;
+ const { dateDebut, dateFin, typesConge, commentaire } = req.body;
+
+ const [demande] = await conn.query(
+ 'SELECT Statut, CollaborateurADId FROM DemandeConge WHERE Id = ?',
+ [id]
+ );
+
+ if (demande.length === 0) {
+ await conn.rollback();
+ return res.status(404).json({ error: 'Demande non trouvée' });
+ }
+
+ const ancienStatut = demande[0].Statut;
+ const collaborateurId = demande[0].CollaborateurADId;
+
+ const rolesAutorises = ['Admin', 'RH', 'Validateur'];
+ const estAuteur = collaborateurId === req.user.id;
+ const estRoleAutorise = rolesAutorises.includes(req.user.role);
+
+ if (!estAuteur && !estRoleAutorise) {
+ await conn.rollback();
+ return res.status(403).json({
+ error: 'Accès non autorisé',
+ details: 'Seuls les Admin, RH, Validateurs ou l\'auteur peuvent modifier cette demande'
+ });
+ }
+
+ if (ancienStatut === 'Validée') {
+ console.log('⚠️ Modification d\'une demande validée - restauration des compteurs');
+
+ const [ancienTypes] = await conn.query(
+ 'SELECT TypeCongeId, NombreJours FROM DemandeCongeType WHERE DemandeCongeId = ?',
+ [id]
+ );
+
+ for (const type of ancienTypes) {
+ await conn.query(
+ `UPDATE CompteurConges
+ SET Solde = Solde + ?
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = YEAR(NOW())`,
+ [type.NombreJours, collaborateurId, type.TypeCongeId]
+ );
+ }
+ }
+
+ const totalJours = typesConge.reduce((sum, type) => sum + parseFloat(type.nombreJours), 0);
+
+ const nouveauStatut = (ancienStatut === 'Validée' || ancienStatut === 'Refusée')
+ ? 'En attente'
+ : ancienStatut;
+
+ await conn.query(
+ `UPDATE DemandeConge
+ SET DateDebut = ?,
+ DateFin = ?,
+ Commentaire = ?,
+ NombreJours = ?,
+ Statut = ?,
+ ValidateurADId = NULL,
+ DateValidation = NULL,
+ CommentaireValidation = NULL
+ WHERE Id = ?`,
+ [dateDebut, dateFin, commentaire || '', totalJours, nouveauStatut, id]
+ );
+
+ await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [id]);
+
+ for (const type of typesConge) {
+ await conn.query(
+ 'INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours) VALUES (?, ?, ?)',
+ [id, type.typeId, type.nombreJours]
+ );
+ }
+
+ const actionDetails = ancienStatut === 'Validée'
+ ? `Modification par ${req.user.role} - demande validée remise en attente`
+ : ancienStatut === 'Refusée'
+ ? `Modification par ${req.user.role} - demande refusée remise en attente`
+ : `Modification par ${req.user.role}`;
+
+ await conn.query(
+ `INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DemandeCongeId)
+ VALUES (?, ?, ?, ?)`,
+ [req.user.id, 'Modification demande', actionDetails, id]
+ );
+
+ await conn.commit();
+
+ notifyClients({
+ type: 'demande-updated',
+ demandeId: parseInt(id),
+ action: 'modification',
+ nouveauStatut: nouveauStatut,
+ ancienStatut: ancienStatut,
+ timestamp: new Date().toISOString()
+ }, collaborateurId);
+
+ notifyClients({
+ type: 'demande-list-updated',
+ action: 'modification',
+ demandeId: parseInt(id),
+ timestamp: new Date().toISOString()
+ });
+
+ try {
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.DEMANDE_UPDATED,
+ {
+ demandeId: parseInt(id),
+ collaborateurId: collaborateurId,
+ nouveauStatut: nouveauStatut,
+ ancienStatut: ancienStatut
+ }
+ );
+
+ if (nouveauStatut !== ancienStatut) {
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.COMPTEUR_UPDATED,
+ { collaborateurId: collaborateurId }
+ );
+ }
+ } catch (webhookError) {
+ console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
+ }
+
+ const message = nouveauStatut !== ancienStatut
+ ? `Demande modifiée avec succès et remise en attente (était: ${ancienStatut})`
+ : 'Demande modifiée avec succès';
+
+ res.json({
+ message,
+ nouveauStatut,
+ ancienStatut
+ });
+
+ console.log(`✅ Demande ${id} modifiée par ${req.user.role} - Ancien: ${ancienStatut}, Nouveau: ${nouveauStatut}`);
+
+ } catch (error) {
+ await conn.rollback();
+ console.error('Erreur modification demande:', error);
+ res.status(500).json({ error: error.message });
+ } finally {
+ conn.release();
+ }
+ });
+
+ app.delete('/demandes/:id', authenticateToken, async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ const { id } = req.params;
+
+ const [demande] = await conn.query(
+ 'SELECT Statut, CollaborateurADId FROM DemandeConge WHERE Id = ?',
+ [id]
+ );
+
+ if (demande.length === 0) {
+ await conn.rollback();
+ return res.status(404).json({ error: 'Demande non trouvée' });
+ }
+
+ const collaborateurId = demande[0].CollaborateurADId;
+
+ const rolesAutorises = ['Admin', 'RH', 'Validateur'];
+ const estAuteur = collaborateurId === req.user.id;
+ const estRoleAutorise = rolesAutorises.includes(req.user.role);
+
+ if (!estAuteur && !estRoleAutorise) {
+ await conn.rollback();
+ return res.status(403).json({
+ error: 'Accès non autorisé',
+ details: 'Seuls les Admin, RH, Validateurs ou l\'auteur peuvent supprimer cette demande'
+ });
+ }
+
+ // ✅ AJOUT: Supprimer d'abord les entrées d'historique
+ await conn.query('DELETE FROM HistoriqueActions WHERE DemandeCongeId = ?', [id]);
+
+ // Supprimer les types de congé
+ await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [id]);
+
+ // Supprimer la demande
+ await conn.query('DELETE FROM DemandeConge WHERE Id = ?', [id]);
+
+ await conn.commit();
+
+ notifyClients({
+ type: 'demande-deleted',
+ demandeId: parseInt(id),
+ collaborateurId: collaborateurId,
+ timestamp: new Date().toISOString()
+ });
+
+ try {
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.DEMANDE_DELETED,
+ {
+ demandeId: parseInt(id),
+ collaborateurId: collaborateurId
+ }
+ );
+ } catch (webhookError) {
+ console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
+ }
+
+ console.log(`✅ Demande ${id} supprimée par ${req.user.role}`);
+ res.json({ message: 'Demande supprimée avec succès' });
+ } catch (error) {
+ await conn.rollback();
+ console.error('❌ Erreur suppression demande:', error);
+ // ✅ AJOUT: Log plus détaillé de l'erreur
+ console.error(' Code erreur:', error.code);
+ console.error(' SQL Message:', error.sqlMessage);
+ res.status(500).json({ error: error.message });
+ } finally {
+ conn.release();
+ }
+ });
+
+ // ⭐ ROUTE VALIDATION AVEC WEBHOOKS
+ app.put('/demandes/:id/valider', authenticateToken, async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ const { id } = req.params;
+ const { statut, commentaire } = req.body;
+
+ console.log('\n🔍 === DÉBUT VALIDATION DEMANDE ===');
+
+ const [demandeInfo] = await conn.query(
+ `SELECT
+ dc.Id,
+ dc.CollaborateurADId,
+ dc.DateDebut,
+ dc.DateFin,
+ dc.NombreJours,
+ ca.prenom,
+ ca.nom,
+ ca.email as collaborateur_email,
+ ca.Actif,
+ GROUP_CONCAT(tc.Nom SEPARATOR ', ') as TypeConge
+ FROM DemandeConge dc
+ JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
+ LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
+ LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
+ WHERE dc.Id = ?
+ GROUP BY dc.Id
+ LIMIT 1`,
+ [id]
+ );
+
+ if (demandeInfo.length === 0) {
+ await conn.rollback();
+ return res.status(404).json({ error: 'Demande non trouvée' });
+ }
+
+ const demande = demandeInfo[0];
+ const collaborateurId = demande.CollaborateurADId;
+
+ await conn.query(
+ `UPDATE DemandeConge
+ SET Statut = ?, CommentaireValidation = ?, ValidateurADId = ?, DateValidation = NOW()
+ WHERE Id = ?`,
+ [statut, commentaire || '', req.user.id, id]
+ );
+
+ if (statut === 'Validée') {
+ const [types] = await conn.query(
+ 'SELECT TypeCongeId, NombreJours FROM DemandeCongeType WHERE DemandeCongeId = ?',
+ [id]
+ );
+
+ for (const type of types) {
+ await conn.query(
+ `UPDATE CompteurConges
+ SET Solde = Solde - ?
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = YEAR(NOW())`,
+ [type.NombreJours, collaborateurId, type.TypeCongeId]
+ );
+ }
+ }
+
+ await conn.query(
+ `INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DemandeCongeId)
+ VALUES (?, ?, ?, ?)`,
+ [req.user.id, statut === 'Validée' ? 'Validation congé' : 'Refus congé', commentaire, id]
+ );
+
+ await conn.commit();
+
+ // ⭐ ENVOI EMAIL
+ const accessToken = await getGraphToken();
+ if (accessToken && demande.collaborateur_email) {
+ const fromEmail = 'noreply@ensup.eu';
+ const collaborateurNom = `${demande.prenom} ${demande.nom}`;
+ const validateurNom = req.user.prenom && req.user.nom
+ ? `${req.user.prenom} ${req.user.nom}`
+ : 'l\'équipe RH';
+
+ const dateDebut = new Date(demande.DateDebut).toLocaleDateString('fr-FR');
+ const dateFin = new Date(demande.DateFin).toLocaleDateString('fr-FR');
+ const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`;
+
+ const subject = statut === 'Validée'
+ ? '✅ Votre demande de congé a été approuvée'
+ : '❌ Votre demande de congé a été refusée';
+
+ const body = statut === 'Validée'
+ ? `
+
+
✅ Demande approuvée
+
+
+
Bonjour ${collaborateurNom},
+
Votre demande de congé a été approuvée par ${validateurNom}.
+
+
Type : ${demande.TypeConge}
+
Période : ${datesPeriode}
+
Durée : ${demande.NombreJours} jour(s)
+ ${commentaire ? `
Commentaire : ${commentaire}
` : ''}
+
+
+
`
+ : `
+
+
❌ Demande refusée
+
+
+
Bonjour ${collaborateurNom},
+
Votre demande de congé a été refusée par ${validateurNom}.
+
+
Type : ${demande.TypeConge}
+
Période : ${datesPeriode}
+
Durée : ${demande.NombreJours} jour(s)
+ ${commentaire ? `
Motif du refus : ${commentaire}
` : ''}
+
+
+
`;
+
+ await sendMailGraph(accessToken, fromEmail, demande.collaborateur_email, subject, body);
+ }
+
+ notifyClients({
+ type: 'demande-validated',
+ demandeId: parseInt(id),
+ statut: statut,
+ collaborateurId: collaborateurId,
+ timestamp: new Date().toISOString()
+ }, collaborateurId);
+
+ notifyClients({
+ type: 'demande-list-updated',
+ action: 'validation',
+ demandeId: parseInt(id),
+ timestamp: new Date().toISOString()
+ });
+
+ try {
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.DEMANDE_VALIDATED,
+ {
+ demandeId: parseInt(id),
+ statut: statut,
+ collaborateurId: collaborateurId,
+ validateurId: req.user.id,
+ commentaire: commentaire
+ }
+ );
+
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.COMPTEUR_UPDATED,
+ { collaborateurId: collaborateurId }
+ );
+ } catch (webhookError) {
+ console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
+ }
+
+ res.json({ message: 'Demande mise à jour' });
+ } catch (error) {
+ await conn.rollback();
+ console.error('\n❌ ERREUR VALIDATION:', error);
+ res.status(500).json({ error: error.message });
+ } finally {
+ conn.release();
+ }
+ });
+
+ // ================================================
+ // ROUTES EMPLOYÉS
+ // ================================================
+ app.get('/employes', authenticateToken, async (req, res) => {
+ try {
+ const includeInactifs = req.query.include_inactifs === 'true';
+ const serviceId = req.query.service_id;
+
+ let query = `
+ SELECT
+ ca.id,
+ ca.nom,
+ ca.prenom,
+ CONCAT(ca.prenom, ' ', ca.nom) AS nomComplet,
+ ca.email,
+ ca.role,
+ ca.Actif,
+ ca.DateSortie,
+ ca.TypeContrat,
+ s.Nom AS service,
+ s.Id AS serviceId,
+ COALESCE(c.Nom, 'Sans campus') AS campus,
+ c.Id AS campusId,
+ cc1.Solde AS soldeCP,
+ cc2.Solde AS soldeRTT
+ FROM CollaborateurAD ca
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ LEFT JOIN Campus c ON ca.CampusId = c.Id
+ LEFT JOIN CompteurConges cc1 ON ca.id = cc1.CollaborateurADId
+ AND cc1.TypeCongeId = 1
+ AND cc1.Annee = YEAR(NOW())
+ LEFT JOIN CompteurConges cc2 ON ca.id = cc2.CollaborateurADId
+ AND cc2.TypeCongeId = 2
+ AND cc2.Annee = YEAR(NOW())
+ WHERE 1=1
+ `;
+
+ const params = [];
+
+ if (!includeInactifs) {
+ query += ' AND (ca.Actif = 1 OR ca.Actif IS NULL)';
+ }
+
+ // 🆕 Ajouter le filtre service si fourni
+ if (serviceId && serviceId !== 'all') {
+ query += ' AND ca.ServiceId = ?';
+ params.push(serviceId);
+ }
+
+ query += ' ORDER BY c.Nom, s.Nom, ca.nom, ca.prenom';
+
+ const [employes] = await pool.query(query, params);
+ res.json(employes);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ // ✅ NOUVELLE ROUTE: Désactiver un collaborateur
+ app.post('/employes/desactiver', authenticateToken, async (req, res) => {
+ try {
+ if (!['Admin', 'RH'].includes(req.user.role)) {
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { collaborateur_id, date_sortie } = req.body;
+
+ if (!collaborateur_id) {
+ return res.json({ success: false, message: 'ID collaborateur manquant' });
+ }
+
+ const dateSortie = date_sortie || new Date().toISOString().split('T')[0];
+
+ const [collab] = await pool.query(
+ 'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
+ [collaborateur_id]
+ );
+
+ if (collab.length === 0) {
+ return res.json({ success: false, message: 'Collaborateur non trouvé' });
+ }
+
+ await pool.query(
+ `UPDATE CollaborateurAD
+ SET Actif = FALSE, DateSortie = ?
+ WHERE id = ?`,
+ [dateSortie, collaborateur_id]
+ );
+
+ res.json({
+ success: true,
+ message: 'Collaborateur désactivé',
+ nom: `${collab[0].prenom} ${collab[0].nom}`,
+ date_sortie: dateSortie
+ });
+ } catch (error) {
+ console.error('Erreur desactiverCollaborateur:', error);
+ res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
+ }
+ });
+
+ // ✅ NOUVELLE ROUTE: Réactiver un collaborateur
+ app.post('/employes/reactiver', authenticateToken, async (req, res) => {
+ try {
+ if (!['Admin', 'RH'].includes(req.user.role)) {
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { collaborateur_id } = req.body;
+
+ if (!collaborateur_id) {
+ return res.json({ success: false, message: 'ID collaborateur manquant' });
+ }
+
+ const [collab] = await pool.query(
+ 'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
+ [collaborateur_id]
+ );
+
+ if (collab.length === 0) {
+ return res.json({ success: false, message: 'Collaborateur non trouvé' });
+ }
+
+ await pool.query(
+ `UPDATE CollaborateurAD
+ SET Actif = TRUE, DateSortie = NULL
+ WHERE id = ?`,
+ [collaborateur_id]
+ );
+
+ res.json({
+ success: true,
+ message: 'Collaborateur réactivé',
+ nom: `${collab[0].prenom} ${collab[0].nom}`
+ });
+ } catch (error) {
+ console.error('Erreur reactiverCollaborateur:', error);
+ res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message });
+ }
+ });
+
+ app.get('/types-conge', authenticateToken, async (req, res) => {
+ try {
+ const [types] = await pool.query(
+ 'SELECT Id as id, Nom as nom, CouleurHex as couleur FROM TypeConge WHERE Actif = 1 ORDER BY Nom'
+ );
+ res.json(types);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ app.get('/export/paie', authenticateToken, async (req, res) => {
+ try {
+ const { mois, annee } = req.query;
+
+ console.log(`📊 Export paie demandé : ${mois}/${annee}`);
+
+ // Date de début et fin du mois
+ const premierJour = `${annee}-${String(mois).padStart(2, '0')}-01`;
+ const dernierJour = new Date(annee, mois, 0).getDate();
+ const dernierJourMois = `${annee}-${String(mois).padStart(2, '0')}-${dernierJour}`;
+
+ console.log(`📅 Période : ${premierJour} à ${dernierJourMois}`);
+
+ // Récupérer toutes les demandes validées qui chevauchent le mois
+ const [demandes] = await pool.query(`
+ SELECT
+ ca.id as collaborateurId,
+ CONCAT(ca.prenom, ' ', ca.nom) as employe,
+ ca.email,
+ s.Nom as service,
+ tc.Nom as typeConge,
+ dct.NombreJours,
+ dc.DateDebut,
+ dc.DateFin,
+ dc.Statut
+ FROM DemandeConge dc
+ JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
+ JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
+ WHERE dc.Statut = 'Validée'
+ AND (
+ (dc.DateDebut BETWEEN ? AND ?) OR
+ (dc.DateFin BETWEEN ? AND ?) OR
+ (dc.DateDebut <= ? AND dc.DateFin >= ?)
+ )
+ AND (ca.Actif = 1 OR ca.Actif IS NULL)
+ ORDER BY ca.nom, ca.prenom, dc.DateDebut
+ `, [premierJour, dernierJourMois, premierJour, dernierJourMois, premierJour, dernierJourMois]);
+
+ console.log(`📋 Nombre de demandes trouvées : ${demandes.length}`);
+
+ if (demandes.length > 0) {
+ console.log('🔍 Exemple de demande:', demandes[0]);
+ }
+
+ // Regrouper par employé
+ const employesMap = new Map();
+
+ demandes.forEach(row => {
+ const key = row.collaborateurId;
+
+ if (!employesMap.has(key)) {
+ employesMap.set(key, {
+ employe: row.employe,
+ email: row.email,
+ service: row.service || 'Non assigné',
+ rtt: { nb: 0, dates: [] },
+ cp: { nb: 0, dates: [] },
+ aap: { nb: 0, dates: [] },
+ am: { nb: 0, dates: [] },
+ autres: []
+ });
+ }
+
+ const employe = employesMap.get(key);
+
+ // Formatter les dates
+ const dateDebut = new Date(row.DateDebut);
+ const dateFin = new Date(row.DateFin);
+
+ const formatDate = (date) => {
+ return String(date.getDate()).padStart(2, '0') + '/' +
+ String(date.getMonth() + 1).padStart(2, '0');
+ };
+
+ const dateDebutStr = formatDate(dateDebut);
+ const dateFinStr = formatDate(dateFin);
+ const periode = dateDebutStr === dateFinStr ? dateDebutStr : `${dateDebutStr}-${dateFinStr}`;
+
+ // ✅ CORRECTION : Convertir en nombre
+ const nombreJours = parseFloat(row.NombreJours);
+
+ // Catégoriser selon le type de congé
+ const typeConge = row.typeConge.toLowerCase();
+
+ console.log(` Type détecté: "${row.typeConge}" (${nombreJours}j)`);
+
+ if (typeConge.includes('rtt')) {
+ employe.rtt.nb += nombreJours; // ✅ nombreJours est maintenant un nombre
+ employe.rtt.dates.push(periode);
+ console.log(` ✅ Classé en RTT (total: ${employe.rtt.nb}j)`);
+ } else if (typeConge.includes('cp') || typeConge.includes('congé') || typeConge.includes('conge')) {
+ employe.cp.nb += nombreJours; // ✅ nombreJours est maintenant un nombre
+ employe.cp.dates.push(periode);
+ console.log(` ✅ Classé en CP (total: ${employe.cp.nb}j)`);
+ } else if (typeConge.includes('aap') || typeConge.includes('absence autorisée') ||
+ typeConge.includes('enfant malade') || typeConge.includes('récup') ||
+ typeConge.includes('recuperation')) {
+ employe.aap.nb += nombreJours;
+ employe.aap.dates.push(periode);
+ console.log(` ✅ Classé en AAP (total: ${employe.aap.nb}j)`);
+ } else if (typeConge.includes('maladie') || typeConge.includes('arrêt') || typeConge.includes('arret')) {
+ employe.am.nb += nombreJours;
+ employe.am.dates.push(periode);
+ console.log(` ✅ Classé en AM (total: ${employe.am.nb}j)`);
+ } else {
+ // Autres types
+ const existingAutre = employe.autres.find(a => a.type === row.typeConge);
+ if (existingAutre) {
+ existingAutre.nb += nombreJours;
+ existingAutre.dates.push(periode);
+ } else {
+ employe.autres.push({
+ type: row.typeConge,
+ nb: nombreJours,
+ dates: [periode]
+ });
+ }
+ console.log(` ✅ Classé en Autres (total: ${nombreJours}j)`);
+ }
+ });
+
+ // Formatter les dates (joindre avec " ; ")
+ const dataFormatted = Array.from(employesMap.values()).map(emp => ({
+ employe: emp.employe,
+ email: emp.email,
+ service: emp.service,
+ rtt: { nb: emp.rtt.nb, dates: emp.rtt.dates.join(' ; ') },
+ cp: { nb: emp.cp.nb, dates: emp.cp.dates.join(' ; ') },
+ aap: { nb: emp.aap.nb, dates: emp.aap.dates.join(' ; ') },
+ am: { nb: emp.am.nb, dates: emp.am.dates.join(' ; ') },
+ autres: emp.autres.map(a => ({
+ type: a.type,
+ nb: a.nb,
+ dates: a.dates.join(' ; ')
+ }))
+ }));
+
+ console.log(`✅ ${dataFormatted.length} collaborateurs dans le rapport`);
+
+ res.json(dataFormatted);
+ } catch (error) {
+ console.error('❌ Erreur export paie:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+
+
+ // ========================================
+ // ROUTE COMPLÈTE : /reinitializeAllCounters
+ // ========================================
+ app.post('/reinitializeAllCounters', async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ console.log('🔄 Réinitialisation de tous les compteurs...');
+
+ const collaborateurs = await conn.query(`
+ SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId, SocieteId, role
+ FROM CollaborateurAD
+ WHERE actif = 1 OR actif IS NULL
+ `);
+
+ console.log(`📋 ${collaborateurs.length} collaborateurs trouvés`);
+
+ const dateRefParam = req.body.dateReference;
+ const today = dateRefParam ? new Date(dateRefParam) : new Date();
+ const currentYear = today.getFullYear();
+ const previousYear = currentYear - 1;
+ const results = [];
+
+ // Récupérer les types de congés
+ const cpType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']);
+ const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['RTT']);
+
+ for (const collab of collaborateurs) {
+ console.log(`\n${'='.repeat(60)}`);
+ console.log(`👤 ${collab.prenom} ${collab.nom} (ID: ${collab.id})`);
+ console.log(` Type contrat: ${collab.TypeContrat || '37h'}`);
+ console.log(` Date entrée: ${collab.DateEntree ? new Date(collab.DateEntree).toISOString().split('T')[0] : 'NULL'}`);
+ console.log(` Rôle: ${collab.role || 'Non défini'}`);
+
+ const dateEntree = collab.DateEntree;
+ const typeContrat = collab.TypeContrat || '37h';
+ const isApprenti = collab.role === 'Apprenti';
+
+ // ==========================================
+ // CALCUL CP
+ // ==========================================
+ let acquisCP = calculerAcquisitionCP(today, dateEntree);
+
+ // ✅ PROTECTION : Si NaN, forcer à 0
+ if (isNaN(acquisCP) || !isFinite(acquisCP)) {
+ console.warn(`⚠️ Acquisition CP invalide - Forcé à 0`);
+ acquisCP = 0;
+ }
+
+ // ==========================================
+ // CALCUL RTT
+ // ==========================================
+ let acquisRTT = 0;
+ if (!isApprenti) {
+ try {
+ const rttData = await calculerAcquisitionRTT(conn, collab.id, today);
+ acquisRTT = rttData.acquisition;
+
+ // ✅ PROTECTION : Si NaN, forcer à 0
+ if (isNaN(acquisRTT) || !isFinite(acquisRTT)) {
+ console.warn(`⚠️ Acquisition RTT invalide - Forcé à 0`);
+ acquisRTT = 0;
+ }
+ } catch (error) {
+ console.error(`❌ Erreur calcul RTT:`, error.message);
+ acquisRTT = 0;
+ }
+ }
+
+ // ==========================================
+ // MISE À JOUR CP N
+ // ==========================================
+ if (cpType.length > 0) {
+ const deductionsCP = await conn.query(`
+ SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
+ FROM DeductionDetails dd
+ JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
+ WHERE dc.CollaborateurADId = ?
+ AND dd.TypeCongeId = ?
+ AND dd.Annee = ?
+ AND dc.Statut != 'Refusé'
+ `, [collab.id, cpType[0].Id, currentYear]);
+
+ let totalConsomme = parseFloat(deductionsCP[0].totalConsomme || 0);
+
+ // ✅ PROTECTION
+ if (isNaN(totalConsomme) || !isFinite(totalConsomme)) {
+ console.warn(`⚠️ Total consommé CP invalide - Forcé à 0`);
+ totalConsomme = 0;
+ }
+
+ const compteurExisting = await conn.query(`
+ SELECT SoldeReporte FROM CompteurConges
+ WHERE CollaborateurADId = ?
+ AND TypeCongeId = ?
+ AND Annee = ?
+ `, [collab.id, cpType[0].Id, currentYear]);
+
+ let soldeReporte = compteurExisting.length > 0
+ ? parseFloat(compteurExisting[0].SoldeReporte || 0)
+ : 0;
+
+ // ✅ PROTECTION
+ if (isNaN(soldeReporte) || !isFinite(soldeReporte)) {
+ console.warn(`⚠️ Solde reporté CP invalide - Forcé à 0`);
+ soldeReporte = 0;
+ }
+
+ let nouveauSolde = Math.max(0, acquisCP + soldeReporte - totalConsomme);
+
+ // ✅ PROTECTION FINALE
+ if (isNaN(nouveauSolde) || !isFinite(nouveauSolde)) {
+ console.warn(`⚠️ Nouveau solde CP invalide - Forcé à 0`);
+ nouveauSolde = 0;
+ }
+
+ console.log(` 📊 CP - Acquis: ${acquisCP.toFixed(2)}j, Consommé: ${totalConsomme.toFixed(2)}j, Reporté: ${soldeReporte.toFixed(2)}j, Solde: ${nouveauSolde.toFixed(2)}j`);
+
+ if (compteurExisting.length > 0) {
+ await conn.query(`
+ UPDATE CompteurConges
+ SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [acquisCP, nouveauSolde, collab.id, cpType[0].Id, currentYear]);
+ } else {
+ await conn.query(`
+ INSERT INTO CompteurConges
+ (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
+ VALUES (?, ?, ?, ?, ?, 0, NOW())
+ `, [collab.id, cpType[0].Id, currentYear, acquisCP, nouveauSolde]);
+ }
+
+ // Créer CP N-1 si nécessaire
+ const cpN1 = await conn.query(`
+ SELECT Id FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collab.id, cpType[0].Id, previousYear]);
+
+ if (cpN1.length === 0) {
+ await conn.query(`
+ INSERT INTO CompteurConges
+ (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
+ VALUES (?, ?, ?, 0, 0, 0, NOW())
+ `, [collab.id, cpType[0].Id, previousYear]);
+ }
+ }
+
+ // ==========================================
+ // MISE À JOUR RTT N
+ // ==========================================
+ if (rttType.length > 0 && !isApprenti) {
+ const deductionsRTT = await conn.query(`
+ SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme
+ FROM DeductionDetails dd
+ JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id
+ WHERE dc.CollaborateurADId = ?
+ AND dd.TypeCongeId = ?
+ AND dd.Annee = ?
+ AND dc.Statut != 'Refusé'
+ `, [collab.id, rttType[0].Id, currentYear]);
+
+ let totalConsomme = parseFloat(deductionsRTT[0].totalConsomme || 0);
+
+ // ✅ PROTECTION
+ if (isNaN(totalConsomme) || !isFinite(totalConsomme)) {
+ console.warn(`⚠️ Total consommé RTT invalide - Forcé à 0`);
+ totalConsomme = 0;
+ }
+
+ let nouveauSolde = Math.max(0, acquisRTT - totalConsomme);
+
+ // ✅ PROTECTION FINALE
+ if (isNaN(nouveauSolde) || !isFinite(nouveauSolde)) {
+ console.warn(`⚠️ Nouveau solde RTT invalide - Forcé à 0`);
+ nouveauSolde = 0;
+ }
+
+ console.log(` 📊 RTT - Acquis: ${acquisRTT.toFixed(2)}j, Consommé: ${totalConsomme.toFixed(2)}j, Solde: ${nouveauSolde.toFixed(2)}j`);
+
+ const compteurExisting = await conn.query(`
+ SELECT Id FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collab.id, rttType[0].Id, currentYear]);
+
+ if (compteurExisting.length > 0) {
+ await conn.query(`
+ UPDATE CompteurConges
+ SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
+ WHERE Id = ?
+ `, [acquisRTT, nouveauSolde, compteurExisting[0].Id]);
+ } else {
+ await conn.query(`
+ INSERT INTO CompteurConges
+ (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
+ VALUES (?, ?, ?, ?, ?, 0, NOW())
+ `, [collab.id, rttType[0].Id, currentYear, acquisRTT, nouveauSolde]);
+ }
+
+ // Créer RTT N-1 si nécessaire
+ const rttN1 = await conn.query(`
+ SELECT Id FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collab.id, rttType[0].Id, previousYear]);
+
+ if (rttN1.length === 0) {
+ await conn.query(`
+ INSERT INTO CompteurConges
+ (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
+ VALUES (?, ?, ?, 0, 0, 0, NOW())
+ `, [collab.id, rttType[0].Id, previousYear]);
+ }
+ }
+
+ // Ajouter au résultat
+ results.push({
+ collaborateur: `${collab.prenom} ${collab.nom}`,
+ typecontrat: typeContrat,
+ cpacquis: acquisCP.toFixed(2),
+ rttacquis: acquisRTT.toFixed(2)
+ });
+ }
+
+ await conn.commit();
+ console.log('\n✅ Réinitialisation terminée');
+
+ res.json({
+ success: true,
+ message: `Compteurs réinitialisés pour ${collaborateurs.length} collaborateurs`,
+ datereference: today.toISOString().split('T')[0],
+ totalcollaborateurs: collaborateurs.length,
+ results: results
+ });
+
+ } catch (error) {
+ await conn.rollback();
+ console.error('❌ Erreur réinitialisation:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Erreur lors de la réinitialisation',
+ error: error.message
+ });
+ } finally {
+ conn.release();
+ }
+ });
+ // Route pour obtenir les informations d'un collaborateur
+ app.get('/employes/:id/info', authenticateToken, async (req, res) => {
+ try {
+ const [employe] = await pool.query(
+ `SELECT
+ id,
+ nom,
+ prenom,
+ email,
+ role,
+ TypeContrat,
+ Actif,
+ DateSortie,
+ ServiceId
+ FROM CollaborateurAD
+ WHERE id = ?`,
+ [req.params.id]
+ );
+
+ if (employe.length === 0) {
+ return res.status(404).json({ error: 'Employé non trouvé' });
+ }
+
+ const collab = employe[0];
+
+ // Déterminer si c'est un apprenti
+ const estApprenti = collab.TypeContrat === 'Apprentissage' ||
+ collab.role === 'Apprenti' ||
+ collab.role?.toLowerCase().includes('apprenti');
+
+ res.json({
+ ...collab,
+ estApprenti,
+ droitRTT: !estApprenti
+ });
+ } catch (error) {
+ console.error('Erreur récupération info employé:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ // ✅ Modifier la route GET /api/compteurs pour inclure les infos sur les apprentis
+ app.get('/compteurs', authenticateToken, async (req, res) => {
+ try {
+ const includeInactifs = req.query.include_inactifs === 'true';
+
+ let query = `
+ SELECT
+ cc.Id as id,
+ cc.CollaborateurADId as collaborateurId,
+ cc.TypeCongeId as typeCongeId,
+ CONCAT(ca.prenom, ' ', ca.nom) AS employe,
+ ca.email,
+ ca.Actif,
+ ca.role,
+ ca.TypeContrat,
+ s.Nom AS service,
+ tc.Nom AS typeConge,
+ cc.Annee AS annee,
+ cc.Total AS total,
+ cc.Solde AS solde,
+ cc.SoldeReporte AS soldeReporte,
+ (cc.Total - cc.Solde) AS consomme
+ FROM CompteurConges cc
+ JOIN CollaborateurAD ca ON cc.CollaborateurADId = ca.id
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
+ `;
+
+ if (!includeInactifs) {
+ query += ' WHERE (ca.Actif = 1 OR ca.Actif IS NULL)';
+ }
+
+ query += ' ORDER BY ca.Actif DESC, ca.nom, ca.prenom, tc.Nom';
+
+ const [compteurs] = await pool.query(query);
+
+ // Ajouter l'indicateur apprenti à chaque compteur
+ const compteursAvecStatut = compteurs.map(c => ({
+ ...c,
+ estApprenti: c.TypeContrat === 'Apprentissage' ||
+ c.role === 'Apprenti' ||
+ c.role?.toLowerCase().includes('apprenti')
+ }));
+
+ res.json(compteursAvecStatut);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ app.put('/compteurs/:id', authenticateToken, async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ if (!['Admin', 'RH', 'Validateur'].includes(req.user.role)) {
+ await conn.rollback();
+ conn.release();
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { id } = req.params;
+ const { total, solde } = req.body;
+
+ // ✅ CONVERTIR EN NOMBRES
+ const totalNum = parseFloat(total);
+ const soldeNum = parseFloat(solde);
+
+ const [existing] = await conn.query('SELECT * FROM CompteurConges WHERE Id = ?', [id]);
+
+ if (existing.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return res.status(404).json({ error: 'Compteur non trouvé' });
+ }
+
+ const collaborateurId = existing[0].CollaborateurADId;
+ const typeCongeId = existing[0].TypeCongeId;
+ const annee = existing[0].Annee;
+
+ // Récupérer le nom du type de congé
+ const [typeInfo] = await conn.query(
+ 'SELECT Nom FROM TypeConge WHERE Id = ?',
+ [typeCongeId]
+ );
+
+ const typeConge = typeInfo.length > 0 ? typeInfo[0].Nom : 'Inconnu';
+
+ // ⭐ MISE À JOUR AVEC TRANSACTION
+ await conn.query(
+ `UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`,
+ [totalNum, soldeNum, id]
+ );
+
+ await conn.commit();
+
+ // Notifier les clients SSE
+ notifyClients({
+ type: 'compteur-updated',
+ collaborateurId: collaborateurId,
+ typeCongeId: typeCongeId,
+ typeConge: typeConge,
+ annee: annee,
+ action: 'modification_rh',
+ nouveauTotal: totalNum,
+ nouveauSolde: soldeNum,
+ timestamp: new Date().toISOString()
+ }, collaborateurId);
+
+ // ⭐ WEBHOOK AMÉLIORÉ AVEC TOUTES LES INFOS
+ try {
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.COMPTEUR_UPDATED,
+ {
+ collaborateurId: collaborateurId,
+ typeCongeId: typeCongeId,
+ typeConge: typeConge,
+ typeUpdate: 'modification_manuelle_rh',
+ annee: annee,
+ nouveauTotal: totalNum,
+ nouveauSolde: soldeNum,
+ source: 'rh',
+ timestamp: new Date().toISOString()
+ }
+ );
+ console.log(`✅ Webhook envoyé pour ${collaborateurId} (${typeConge} ${annee}: Total=${totalNum}j, Solde=${soldeNum}j)`);
+ } catch (webhookError) {
+ console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
+ }
+
+ res.json({
+ message: 'Compteur modifié avec succès',
+ total: totalNum,
+ solde: soldeNum
+ });
+ } catch (error) {
+ await conn.rollback();
+ console.error('Erreur modification compteur:', error);
+ res.status(500).json({ error: error.message });
+ } finally {
+ conn.release();
+ }
+ });
+ app.get('/compteurs', authenticateToken, async (req, res) => {
+ try {
+ const { user_id } = req.query;
+ const includeInactifs = req.query.include_inactifs === 'true';
+
+ let query = `
+ SELECT
+ cc.Id as id,
+ cc.CollaborateurADId as collaborateurId,
+ cc.TypeCongeId as typeCongeId,
+ CONCAT(ca.prenom, ' ', ca.nom) AS employe,
+ ca.email,
+ ca.Actif,
+ ca.role,
+ ca.TypeContrat,
+ s.Nom AS service,
+ tc.Nom AS typeConge,
+ cc.Annee AS annee,
+ cc.Total AS total,
+ cc.Solde AS solde,
+ cc.SoldeReporte AS soldeReporte,
+ (cc.Total - cc.Solde) AS consomme
+ FROM CompteurConges cc
+ JOIN CollaborateurAD ca ON cc.CollaborateurADId = ca.id
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
+ WHERE 1=1
+ `;
+
+ const params = [];
+
+ // Filtre par utilisateur si fourni
+ if (user_id) {
+ query += ' AND cc.CollaborateurADId = ?';
+ params.push(user_id);
+ }
+
+ // Filtre actif/inactif
+ if (!includeInactifs) {
+ query += ' AND (ca.Actif = 1 OR ca.Actif IS NULL)';
+ }
+
+ query += ' ORDER BY ca.Actif DESC, ca.nom, ca.prenom, tc.Nom';
+
+ const [compteurs] = await pool.query(query, params);
+
+ // Ajouter l'indicateur apprenti
+ const compteursAvecStatut = compteurs.map(c => ({
+ ...c,
+ estApprenti: c.TypeContrat === 'Apprentissage' ||
+ c.role === 'Apprenti' ||
+ c.role?.toLowerCase().includes('apprenti')
+ }));
+
+ res.json(compteursAvecStatut);
+ } catch (error) {
+ console.error('❌ Erreur récupération compteurs:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+ // ================================================
+ // ROUTE POUR AJOUTER DES JOURS DE RÉCUPÉRATION
+ // ================================================
+
+ app.post('/compteurs/ajouter-recup', authenticateToken, async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ if (!['Admin', 'RH'].includes(req.user.role)) {
+ await conn.rollback();
+ conn.release();
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { collaborateurId, nombreJours, commentaire } = req.body;
+
+ if (!collaborateurId || !nombreJours || nombreJours <= 0) {
+ await conn.rollback();
+ conn.release();
+ return res.json({
+ success: false,
+ message: 'Données invalides'
+ });
+ }
+
+ console.log('📥 Ajout de récupération:', {
+ collaborateurId,
+ nombreJours,
+ commentaire
+ });
+
+ // Récupérer les infos du collaborateur
+ const [collaborateur] = await conn.query(
+ 'SELECT prenom, nom, email, Actif FROM CollaborateurAD WHERE id = ?',
+ [collaborateurId]
+ );
+
+ if (collaborateur.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return res.status(404).json({
+ success: false,
+ message: 'Collaborateur non trouvé'
+ });
+ }
+
+ if (collaborateur[0].Actif === 0) {
+ await conn.rollback();
+ conn.release();
+ return res.status(403).json({
+ success: false,
+ message: 'Impossible d\'ajouter des jours à un compte désactivé'
+ });
+ }
+
+ const collab = collaborateur[0];
+ const currentYear = new Date().getFullYear();
+
+ // Récupérer l'ID du type "Récupération"
+ const [recupType] = await conn.query(
+ 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
+ ['Récupération']
+ );
+
+ if (recupType.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return res.status(500).json({
+ success: false,
+ message: 'Type de congé "Récupération" non trouvé'
+ });
+ }
+
+ const recupTypeId = recupType[0].Id;
+
+ // Vérifier si le compteur existe déjà
+ const [compteurExisting] = await conn.query(
+ `SELECT Id, Total, Solde FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
+ [collaborateurId, recupTypeId, currentYear]
+ );
+
+ if (compteurExisting.length > 0) {
+ // Mettre à jour le compteur existant
+ await conn.query(
+ `UPDATE CompteurConges
+ SET Total = Total + ?,
+ Solde = Solde + ?,
+ DerniereMiseAJour = NOW()
+ WHERE Id = ?`,
+ [nombreJours, nombreJours, compteurExisting[0].Id]
+ );
+
+ console.log(`✅ Compteur Récupération mis à jour: +${nombreJours}j (nouveau solde: ${(parseFloat(compteurExisting[0].Solde) + nombreJours).toFixed(2)}j)`);
+ } else {
+ // Créer un nouveau compteur
+ await conn.query(
+ `INSERT INTO CompteurConges
+ (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
+ VALUES (?, ?, ?, ?, ?, 0, NOW())`,
+ [collaborateurId, recupTypeId, currentYear, nombreJours, nombreJours]
+ );
+
+ console.log(`✅ Compteur Récupération créé: ${nombreJours}j`);
+ }
+
+ // Enregistrer dans l'historique
+ await conn.query(
+ `INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
+ VALUES (?, ?, ?, NOW())`,
+ [
+ req.user.id,
+ 'Ajout récupération',
+ `Ajout de ${nombreJours}j de récupération pour ${collab.prenom} ${collab.nom}${commentaire ? ` - ${commentaire}` : ''}`
+ ]
+ );
+
+ await conn.commit();
+
+ // Notifier les clients SSE
+ notifyClients({
+ type: 'compteur-updated',
+ collaborateurId: collaborateurId,
+ typeConge: 'Récupération',
+ action: 'ajout',
+ nombreJours: nombreJours,
+ timestamp: new Date().toISOString()
+ }, collaborateurId);
+
+ // Envoyer webhook au serveur collaborateurs
+ try {
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.COMPTEUR_UPDATED,
+ { collaborateurId: collaborateurId }
+ );
+ } catch (webhookError) {
+ console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
+ }
+
+ // Envoyer un email de notification au collaborateur
+ const accessToken = await getGraphToken();
+ if (accessToken && collab.email) {
+ const fromEmail = 'noreply@ensup.eu';
+ const subject = '✅ Jours de récupération ajoutés';
+ const body = `
+
+
+
✅ Jours de récupération ajoutés
+
+
+
Bonjour ${collab.prenom} ${collab.nom},
+
Des jours de récupération ont été ajoutés à votre compteur.
+
+
Jours ajoutés : ${nombreJours} jour${nombreJours > 1 ? 's' : ''}
+ ${commentaire ? `
Motif : ${commentaire}
` : ''}
+
Année : ${currentYear}
+
+
Ces jours sont désormais disponibles dans votre solde de récupération.
+
+
+ `;
+
+ try {
+ await sendMailGraph(accessToken, fromEmail, collab.email, subject, body);
+ console.log('✅ Email de notification envoyé');
+ } catch (emailError) {
+ console.error('❌ Erreur envoi email:', emailError);
+ }
+ }
+
+ conn.release();
+
+ res.json({
+ success: true,
+ message: 'Jours de récupération ajoutés avec succès',
+ collaborateur: `${collab.prenom} ${collab.nom}`,
+ nombreJours: nombreJours,
+ annee: currentYear
+ });
+
+ } catch (error) {
+ await conn.rollback();
+ if (conn) conn.release();
+ console.error('❌ Erreur ajout récupération:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Erreur serveur',
+ error: error.message
+ });
+ }
+ });
+
+ // ================================================
+ // ROUTE POUR RETIRER DES JOURS DE RÉCUPÉRATION
+ // ================================================
+
+ app.post('/compteurs/retirer-recup', authenticateToken, async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ if (!['Admin', 'RH'].includes(req.user.role)) {
+ await conn.rollback();
+ conn.release();
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { collaborateurId, nombreJours, commentaire } = req.body;
+
+ if (!collaborateurId || !nombreJours || nombreJours <= 0) {
+ await conn.rollback();
+ conn.release();
+ return res.json({
+ success: false,
+ message: 'Données invalides'
+ });
+ }
+
+ console.log('📤 Retrait de récupération:', {
+ collaborateurId,
+ nombreJours,
+ commentaire
+ });
+
+ // Récupérer les infos du collaborateur
+ const [collaborateur] = await conn.query(
+ 'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
+ [collaborateurId]
+ );
+
+ if (collaborateur.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return res.status(404).json({
+ success: false,
+ message: 'Collaborateur non trouvé'
+ });
+ }
+
+ const collab = collaborateur[0];
+ const currentYear = new Date().getFullYear();
+
+ // Récupérer l'ID du type "Récupération"
+ const [recupType] = await conn.query(
+ 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
+ ['Récupération']
+ );
+
+ if (recupType.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return res.status(500).json({
+ success: false,
+ message: 'Type de congé "Récupération" non trouvé'
+ });
+ }
+
+ const recupTypeId = recupType[0].Id;
+
+ // Vérifier le solde actuel
+ const [compteur] = await conn.query(
+ `SELECT Id, Total, Solde FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
+ [collaborateurId, recupTypeId, currentYear]
+ );
+
+ if (compteur.length === 0 || compteur[0].Solde < nombreJours) {
+ await conn.rollback();
+ conn.release();
+ return res.status(400).json({
+ success: false,
+ message: 'Solde de récupération insuffisant',
+ soldeActuel: compteur.length > 0 ? compteur[0].Solde : 0
+ });
+ }
+
+ // Retirer les jours
+ await conn.query(
+ `UPDATE CompteurConges
+ SET Total = GREATEST(0, Total - ?),
+ Solde = GREATEST(0, Solde - ?),
+ DerniereMiseAJour = NOW()
+ WHERE Id = ?`,
+ [nombreJours, nombreJours, compteur[0].Id]
+ );
+
+ console.log(`✅ ${nombreJours}j retirés du compteur Récupération`);
+
+ // Enregistrer dans l'historique
+ await conn.query(
+ `INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
+ VALUES (?, ?, ?, NOW())`,
+ [
+ req.user.id,
+ 'Retrait récupération',
+ `Retrait de ${nombreJours}j de récupération pour ${collab.prenom} ${collab.nom}${commentaire ? ` - ${commentaire}` : ''}`
+ ]
+ );
+
+ await conn.commit();
+
+ // Notifier les clients SSE
+ notifyClients({
+ type: 'compteur-updated',
+ collaborateurId: collaborateurId,
+ typeConge: 'Récupération',
+ action: 'retrait',
+ nombreJours: nombreJours,
+ timestamp: new Date().toISOString()
+ }, collaborateurId);
+
+ // Envoyer webhook
+ try {
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.COMPTEUR_UPDATED,
+ { collaborateurId: collaborateurId }
+ );
+ } catch (webhookError) {
+ console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
+ }
+
+ conn.release();
+
+ res.json({
+ success: true,
+ message: 'Jours de récupération retirés avec succès',
+ collaborateur: `${collab.prenom} ${collab.nom}`,
+ nombreJours: nombreJours,
+ annee: currentYear
+ });
+
+ } catch (error) {
+ await conn.rollback();
+ if (conn) conn.release();
+ console.error('❌ Erreur retrait récupération:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Erreur serveur',
+ error: error.message
+ });
+ }
+ });
+
+ app.get('/equipes', authenticateToken, async (req, res) => {
+ try {
+ const [equipes] = await pool.query(`
+ SELECT
+ s.Id,
+ s.Nom as nomService,
+ s.Nom as service,
+ COUNT(DISTINCT CASE WHEN (ca.Actif = 1 OR ca.Actif IS NULL) THEN ca.id END) as nombreMembres,
+ COUNT(DISTINCT CASE WHEN dc.Statut = 'En attente' AND (ca.Actif = 1 OR ca.Actif IS NULL) THEN dc.Id END) as demandesEnAttente,
+ COALESCE(
+ -- D'abord essayer ServiceAffectation
+ (SELECT GROUP_CONCAT(DISTINCT c1.Nom ORDER BY c1.Nom SEPARATOR ',')
+ FROM ServiceAffectation sa1
+ JOIN Campus c1 ON sa1.CampusId = c1.Id
+ WHERE sa1.ServiceId = s.Id),
+ -- Sinon utiliser les campus des collaborateurs
+ (SELECT GROUP_CONCAT(DISTINCT c2.Nom ORDER BY c2.Nom SEPARATOR ',')
+ FROM CollaborateurAD ca2
+ JOIN Campus c2 ON ca2.CampusId = c2.Id
+ WHERE ca2.ServiceId = s.Id AND (ca2.Actif = 1 OR ca2.Actif IS NULL)),
+ 'Non assigné'
+ ) as campus
+ FROM Services s
+ LEFT JOIN CollaborateurAD ca ON s.Id = ca.ServiceId AND (ca.Actif = 1 OR ca.Actif IS NULL)
+ LEFT JOIN DemandeConge dc ON ca.id = dc.CollaborateurADId AND dc.Statut = 'En attente'
+ GROUP BY s.Id, s.Nom
+ HAVING nombreMembres > 0 OR EXISTS (
+ SELECT 1 FROM ServiceAffectation sa2 WHERE sa2.ServiceId = s.Id
+ )
+ ORDER BY s.Nom
+ `);
+
+ res.json(equipes);
+ } catch (error) {
+ console.error('❌ Erreur /api/equipes:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ app.get('/equipes/:id', authenticateToken, async (req, res) => {
+ try {
+ const [equipes] = await pool.query(`
+ SELECT
+ s.Id,
+ s.Nom,
+ s.Nom as nomService,
+ COUNT(DISTINCT CASE WHEN (ca.Actif = 1 OR ca.Actif IS NULL) THEN ca.id END) as nombreMembres,
+ COUNT(DISTINCT CASE WHEN dc.Statut = 'En attente' AND (ca.Actif = 1 OR ca.Actif IS NULL) THEN dc.Id END) as demandesEnAttente,
+ COALESCE(GROUP_CONCAT(DISTINCT c.Nom), 'Non assigné') as campus
+ FROM Services s
+ LEFT JOIN CollaborateurAD ca ON s.Id = ca.ServiceId
+ LEFT JOIN DemandeConge dc ON ca.id = dc.CollaborateurADId
+ LEFT JOIN ServiceAffectation sa ON s.Id = sa.ServiceId
+ LEFT JOIN Campus c ON sa.CampusId = c.Id
+ WHERE s.Id = ?
+ GROUP BY s.Id, s.Nom
+ `, [req.params.id]);
+
+ if (equipes.length === 0) {
+ return res.status(404).json({ error: 'Service non trouvé' });
+ }
+
+ res.json(equipes[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ app.get('/equipes/:id/membres', authenticateToken, async (req, res) => {
+ try {
+ const [membres] = await pool.query(`
+ SELECT
+ ca.id,
+ CONCAT(ca.prenom, ' ', ca.nom) as nom,
+ ca.email,
+ ca.role,
+ ca.Actif,
+ ca.DateSortie,
+ COALESCE(cc.Solde, 0) as soldeCP,
+ COALESCE(cc2.Solde, 0) as soldeRTT
+ FROM CollaborateurAD ca
+ LEFT JOIN CompteurConges cc ON ca.id = cc.CollaborateurADId
+ AND cc.TypeCongeId = 1
+ AND cc.Annee = YEAR(NOW())
+ LEFT JOIN CompteurConges cc2 ON ca.id = cc2.CollaborateurADId
+ AND cc2.TypeCongeId = 2
+ AND cc2.Annee = YEAR(NOW())
+ WHERE ca.ServiceId = ?
+ AND (ca.Actif = 1 OR ca.Actif IS NULL)
+ ORDER BY ca.nom, ca.prenom
+`, [req.params.id]);
+
+ res.json(membres);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ app.get('/stats', authenticateToken, async (req, res) => {
+ try {
+ const [stats] = await pool.query(`
+ SELECT
+ COUNT(CASE WHEN Statut = 'En attente' AND (ca.Actif = 1 OR ca.Actif IS NULL) THEN 1 END) as enAttente,
+ COUNT(CASE WHEN Statut = 'Validée' AND MONTH(DateValidation) = MONTH(NOW()) AND (ca.Actif = 1 OR ca.Actif IS NULL) THEN 1 END) as valideeCeMois,
+ COUNT(DISTINCT ca.ServiceId) as nombreEquipes
+ FROM DemandeConge dc
+ JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
+ WHERE (ca.Actif = 1 OR ca.Actif IS NULL)
+ `);
+
+ res.json(stats[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ app.get('/historique', authenticateToken, async (req, res) => {
+ try {
+ const { dateDebut, dateFin, action } = req.query;
+
+ let query = `
+ SELECT
+ ha.Id,
+ CONCAT(ca.prenom, ' ', ca.nom) AS collaborateur,
+ ha.Action,
+ ha.Details,
+ ha.DateAction,
+ ha.AdresseIP,
+ ha.DemandeCongeId
+ FROM HistoriqueActions ha
+ JOIN CollaborateurAD ca ON ha.CollaborateurADId = ca.id
+ WHERE 1=1
+ `;
+
+ const params = [];
+
+ if (dateDebut) {
+ query += ' AND ha.DateAction >= ?';
+ params.push(dateDebut);
+ }
+
+ if (dateFin) {
+ query += ' AND ha.DateAction <= ?';
+ params.push(dateFin);
+ }
+
+ if (action && action !== 'all') {
+ query += ' AND ha.Action = ?';
+ params.push(action);
+ }
+
+ query += ' ORDER BY ha.DateAction DESC LIMIT 500';
+
+ const [historique] = await pool.query(query, params);
+ res.json(historique);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ // ================================================
+ // ROUTES COMPTES-RENDUS D'ACTIVITÉS (RH)
+ // ================================================
+
+ // GET - Stats globales des comptes-rendus
+ app.get('/compte-rendu/stats', authenticateToken, async (req, res) => {
+ const { annee, mois, service_id } = req.query;
+
+ console.log('📊 Route /api/compte-rendu/stats appelée');
+ console.log(' Année:', annee, 'Mois:', mois, 'Service:', service_id);
+
+ try {
+ let query = `
+ SELECT
+ COUNT(DISTINCT ca.id) as totalCollaborateurs,
+ COALESCE(SUM(CASE WHEN crm.Verrouille = 1 OR EXISTS(
+ SELECT 1 FROM CompteRenduActivites cra
+ WHERE cra.CollaborateurADId = ca.id
+ AND cra.Annee = ? AND cra.Mois = ?
+ ) THEN 1 ELSE 0 END), 0) as mensuelsValides,
+ COALESCE(COUNT(DISTINCT ca.id) - SUM(CASE WHEN crm.Verrouille = 1 OR EXISTS(
+ SELECT 1 FROM CompteRenduActivites cra
+ WHERE cra.CollaborateurADId = ca.id
+ AND cra.Annee = ? AND cra.Mois = ?
+ ) THEN 1 ELSE 0 END), 0) as mensuelsEnAttente
+ FROM CollaborateurAD ca
+ LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
+ AND crm.Annee = ?
+ AND crm.Mois = ?
+ WHERE ca.TypeContrat = 'forfait_jour'
+ AND (ca.Actif = 1 OR ca.Actif IS NULL)
+ `;
+
+ const params = [annee, mois, annee, mois, annee, mois];
+
+ // Ajouter le filtre service si fourni
+ if (service_id && service_id !== 'all') {
+ query += ' AND ca.ServiceId = ?';
+ params.push(service_id);
+ }
+
+ const [stats] = await pool.query(query, params);
+
+ console.log('✅ Stats récupérées:', stats[0]);
+
+ res.json({
+ totalCollaborateurs: stats[0].totalCollaborateurs || 0,
+ mensuelsValides: stats[0].mensuelsValides || 0,
+ mensuelsEnAttente: stats[0].mensuelsEnAttente || 0
+ });
+ } catch (error) {
+ console.error('❌ Erreur stats:', error);
+ console.error(' Message:', error.message);
+ console.error(' Code:', error.code);
+
+ // Retourner des stats par défaut en cas d'erreur
+ res.json({
+ totalCollaborateurs: 0,
+ mensuelsValides: 0,
+ mensuelsEnAttente: 0,
+ error: error.message
+ });
+ }
+ });
+
+
+ // Liste des collaborateurs en forfait jour avec leur statut de saisie
+ app.get('/compte-rendu/collaborateurs', authenticateToken, async (req, res) => {
+ try {
+ if (!['RH', 'Admin'].includes(req.user.role)) {
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { annee, mois, service_id } = req.query;
+
+ let query = `
+ SELECT
+ ca.id,
+ ca.nom,
+ ca.prenom,
+ CONCAT(ca.prenom, ' ', ca.nom) AS nomComplet,
+ ca.email,
+ s.Nom AS service,
+ s.Id AS serviceId,
+ crm.Statut as statutMensuel,
+ crm.Verrouille,
+ crm.NbJoursTravailles,
+ crm.NbJoursNonRespectsReposQuotidien,
+ crm.NbJoursNonRespectsReposHebdo,
+ crm.DateValidation,
+ (SELECT COUNT(*) FROM CompteRenduActivites cra
+ WHERE cra.CollaborateurADId = ca.id
+ AND cra.Annee = ? AND cra.Mois = ?) as nbJoursSaisis
+ FROM CollaborateurAD ca
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
+ AND crm.Annee = ? AND crm.Mois = ?
+ WHERE ca.TypeContrat = 'forfait_jour'
+ AND (ca.Actif = 1 OR ca.Actif IS NULL)
+ `;
+
+ const params = [annee, mois, annee, mois];
+
+ // 🆕 Ajouter le filtre service si fourni
+ if (service_id && service_id !== 'all') {
+ query += ' AND ca.ServiceId = ?';
+ params.push(service_id);
+ }
+
+ query += ' ORDER BY ca.nom, ca.prenom';
+
+ const [collaborateurs] = await pool.query(query, params);
+
+ res.json(collaborateurs);
+ } catch (error) {
+ console.error('Erreur liste collaborateurs compte-rendu:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ // Verrouiller un compte-rendu (depuis RH)
+ app.post('/compte-rendu/verrouiller', authenticateToken, async (req, res) => {
+ try {
+ if (!['RH', 'Admin'].includes(req.user.role)) {
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { collaborateur_id, annee, mois } = req.body;
+
+ console.log('🔒 Verrouillage manuel par RH');
+ console.log(' Collaborateur:', collaborateur_id, 'Année:', annee, 'Mois:', mois);
+
+ // Calculer les stats
+ const [stats] = await pool.query(`
+ SELECT
+ COUNT(*) as nbJours,
+ SUM(CASE WHEN NOT ReposQuotidienRespect THEN 1 ELSE 0 END) as nbNonRespectQuotidien,
+ SUM(CASE WHEN NOT ReposHebdomadaireRespect THEN 1 ELSE 0 END) as nbNonRespectHebdo
+ FROM CompteRenduActivites
+ WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? AND JourTravaille = TRUE
+ `, [collaborateur_id, annee, mois]);
+
+ // Créer ou mettre à jour le mensuel
+ await pool.query(`
+ INSERT INTO CompteRenduMensuel
+ (CollaborateurADId, Annee, Mois, NbJoursTravailles,
+ NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
+ Statut, DateValidation, Verrouille)
+ VALUES (?, ?, ?, ?, ?, ?, 'Validé', NOW(), TRUE)
+ ON DUPLICATE KEY UPDATE
+ NbJoursTravailles = VALUES(NbJoursTravailles),
+ NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien),
+ NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo),
+ Statut = 'Validé',
+ DateValidation = NOW(),
+ Verrouille = TRUE,
+ DateModification = NOW()
+ `, [collaborateur_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]);
+
+ // Enregistrer l'action dans l'historique
+ await pool.query(`
+ INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
+ VALUES (?, ?, ?, NOW())
+ `, [
+ req.user.id,
+ 'Verrouillage compte-rendu',
+ `Verrouillage du mois ${mois}/${annee} pour le collaborateur ${collaborateur_id}`
+ ]);
+
+ console.log(' ✅ Mois verrouillé avec succès');
+
+ res.json({
+ success: true,
+ message: 'Compte-rendu verrouillé avec succès'
+ });
+
+ } catch (error) {
+ console.error('❌ Erreur verrouillage compte-rendu:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+ });
+
+ // Déverrouiller un compte-rendu (depuis RH)
+ app.post('/compte-rendu/deverrouiller', authenticateToken, async (req, res) => {
+ try {
+ if (!['RH', 'Admin'].includes(req.user.role)) {
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { collaborateur_id, annee, mois } = req.body;
+
+ console.log('🔓 Déverrouillage manuel par RH');
+ console.log(' Collaborateur:', collaborateur_id, 'Année:', annee, 'Mois:', mois);
+
+ // Mettre à jour le statut
+ await pool.query(`
+ UPDATE CompteRenduMensuel
+ SET Verrouille = FALSE,
+ Statut = 'En cours',
+ DateModification = NOW()
+ WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ?
+ `, [collaborateur_id, annee, mois]);
+
+ // Enregistrer l'action dans l'historique
+ await pool.query(`
+ INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
+ VALUES (?, ?, ?, NOW())
+ `, [
+ req.user.id,
+ 'Déverrouillage compte-rendu',
+ `Déverrouillage du mois ${mois}/${annee} pour le collaborateur ${collaborateur_id}`
+ ]);
+
+ console.log(' ✅ Mois déverrouillé avec succès');
+
+ res.json({
+ success: true,
+ message: 'Compte-rendu déverrouillé. Le collaborateur peut maintenant modifier ses saisies.'
+ });
+
+ } catch (error) {
+ console.error('❌ Erreur déverrouillage compte-rendu:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+ });
+
+
+ // Export Excel de tous les comptes-rendus
+ app.get('/compte-rendu/export-excel', authenticateToken, async (req, res) => {
+ try {
+ if (!['RH', 'Admin'].includes(req.user.role)) {
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { annee, mois, service_id } = req.query;
+
+ let query = `
+ SELECT
+ CONCAT(ca.prenom, ' ', ca.nom) AS Collaborateur,
+ ca.email AS Email,
+ s.Nom AS Service,
+ crm.NbJoursTravailles AS 'Jours travaillés',
+ crm.NbJoursNonRespectsReposQuotidien AS 'Non-respect repos quotidien',
+ crm.NbJoursNonRespectsReposHebdo AS 'Non-respect repos hebdo',
+ crm.Statut AS Statut,
+ crm.Verrouille AS Verrouillé,
+ DATE_FORMAT(crm.DateValidation, '%d/%m/%Y %H:%i') AS 'Date validation'
+ FROM CollaborateurAD ca
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ LEFT JOIN CompteRenduMensuel crm ON ca.id = crm.CollaborateurADId
+ AND crm.Annee = ? AND crm.Mois = ?
+ WHERE ca.TypeContrat = 'forfait_jour'
+ AND (ca.Actif = 1 OR ca.Actif IS NULL)
+ `;
+
+ const params = [annee, mois];
+
+ // 🆕 Ajouter le filtre service si fourni
+ if (service_id && service_id !== 'all') {
+ query += ' AND ca.ServiceId = ?';
+ params.push(service_id);
+ }
+
+ query += ' ORDER BY s.Nom, ca.nom, ca.prenom';
+
+ const [data] = await pool.query(query, params);
+
+ res.json(data);
+ } catch (error) {
+ console.error('Erreur export Excel:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+
+ app.get('/compte-rendu/services', authenticateToken, async (req, res) => {
+ try {
+ if (!['RH', 'Admin'].includes(req.user.role)) {
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const [services] = await pool.query(`
+ SELECT
+ s.Id,
+ s.Nom,
+ COUNT(DISTINCT ca.id) as nombreForfaitJour
+ FROM Services s
+ LEFT JOIN CollaborateurAD ca ON s.Id = ca.ServiceId
+ AND ca.TypeContrat = 'forfait_jour'
+ AND (ca.Actif = 1 OR ca.Actif IS NULL)
+ GROUP BY s.Id, s.Nom
+ HAVING nombreForfaitJour > 0
+ ORDER BY s.Nom
+ `);
+
+ res.json(services);
+ } catch (error) {
+ console.error('Erreur liste services:', error);
+ res.status(500).json({ error: error.message });
+ }
+ });
+ app.get('/compte-rendu-activites', authenticateToken, async (req, res) => {
+ try {
+ const { user_id, annee, mois } = req.query;
+
+ console.log('📥 GET /api/compte-rendu-activites');
+ console.log(' User ID:', user_id, 'Année:', annee, 'Mois:', mois);
+
+ if (!user_id || !annee || !mois) {
+ return res.status(400).json({
+ success: false,
+ message: 'Paramètres manquants (user_id, annee, mois)'
+ });
+ }
+
+ // Récupérer les jours du compte-rendu
+ const [jours] = await pool.query(`
+ SELECT
+ Id,
+ CollaborateurADId,
+ JourDate,
+ JourTravaille,
+ ReposQuotidienRespect,
+ ReposHebdomadaireRespect,
+ CommentaireRepos,
+ DateSaisie,
+ DateModification,
+ Annee,
+ Mois
+ FROM CompteRenduActivites
+ WHERE CollaborateurADId = ?
+ AND Annee = ?
+ AND Mois = ?
+ ORDER BY JourDate ASC
+ `, [user_id, annee, mois]);
+
+ console.log(` ✅ ${jours.length} jours trouvés`);
+
+ // 🆕 SI DES JOURS EXISTENT, CRÉER/METTRE À JOUR AUTOMATIQUEMENT LE RÉCAPITULATIF MENSUEL VERROUILLÉ
+ let mensuelData = null;
+
+ if (jours.length > 0) {
+ // Calculer les statistiques
+ const nbJoursTravailles = jours.filter(j => j.JourTravaille).length;
+ const nbNonRespectQuotidien = jours.filter(j => !j.ReposQuotidienRespect && j.JourTravaille).length;
+ const nbNonRespectHebdo = jours.filter(j => !j.ReposHebdomadaireRespect && j.JourTravaille).length;
+
+ // Vérifier si le mensuel existe déjà
+ const [existingMensuel] = await pool.query(`
+ SELECT * FROM CompteRenduMensuel
+ WHERE CollaborateurADId = ?
+ AND Annee = ?
+ AND Mois = ?
+ LIMIT 1
+ `, [user_id, annee, mois]);
+
+ if (existingMensuel.length === 0) {
+ // 🆕 CRÉER AUTOMATIQUEMENT UN MENSUEL VERROUILLÉ
+ await pool.query(`
+ INSERT INTO CompteRenduMensuel
+ (CollaborateurADId, Annee, Mois, NbJoursTravailles,
+ NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo,
+ Statut, DateValidation, Verrouille, DateModification)
+ VALUES (?, ?, ?, ?, ?, ?, 'Validé', NOW(), TRUE, NOW())
+ `, [user_id, annee, mois, nbJoursTravailles, nbNonRespectQuotidien, nbNonRespectHebdo]);
+
+ console.log(' ✅ Mensuel créé automatiquement et verrouillé');
+
+ mensuelData = {
+ CollaborateurADId: parseInt(user_id),
+ Annee: parseInt(annee),
+ Mois: parseInt(mois),
+ NbJoursTravailles: nbJoursTravailles,
+ NbJoursNonRespectsReposQuotidien: nbNonRespectQuotidien,
+ NbJoursNonRespectsReposHebdo: nbNonRespectHebdo,
+ Statut: 'Validé',
+ Verrouille: 1,
+ DateValidation: new Date(),
+ DateModification: new Date()
+ };
+ } else {
+ // 🆕 METTRE À JOUR LES STATS ET VERROUILLER AUTOMATIQUEMENT
+ await pool.query(`
+ UPDATE CompteRenduMensuel
+ SET NbJoursTravailles = ?,
+ NbJoursNonRespectsReposQuotidien = ?,
+ NbJoursNonRespectsReposHebdo = ?,
+ Statut = 'Validé',
+ Verrouille = TRUE,
+ DateValidation = NOW(),
+ DateModification = NOW()
+ WHERE CollaborateurADId = ?
+ AND Annee = ?
+ AND Mois = ?
+ `, [nbJoursTravailles, nbNonRespectQuotidien, nbNonRespectHebdo, user_id, annee, mois]);
+
+ console.log(' ✅ Mensuel mis à jour et verrouillé automatiquement');
+
+ mensuelData = {
+ ...existingMensuel[0],
+ NbJoursTravailles: nbJoursTravailles,
+ NbJoursNonRespectsReposQuotidien: nbNonRespectQuotidien,
+ NbJoursNonRespectsReposHebdo: nbNonRespectHebdo,
+ Statut: 'Validé',
+ Verrouille: 1,
+ DateValidation: new Date(),
+ DateModification: new Date()
+ };
+ }
+ } else {
+ // Pas de jours saisis, vérifier quand même s'il existe un mensuel
+ const [existingMensuel] = await pool.query(`
+ SELECT * FROM CompteRenduMensuel
+ WHERE CollaborateurADId = ?
+ AND Annee = ?
+ AND Mois = ?
+ LIMIT 1
+ `, [user_id, annee, mois]);
+
+ mensuelData = existingMensuel.length > 0 ? existingMensuel[0] : null;
+ console.log(' ℹ️ Aucun jour saisi pour ce mois');
+ }
+
+ res.json({
+ success: true,
+ jours: jours,
+ mensuel: mensuelData,
+ collaborateurId: parseInt(user_id),
+ annee: parseInt(annee),
+ mois: parseInt(mois),
+ autoValidated: jours.length > 0 // Indicateur de validation automatique
+ });
+
+ } catch (error) {
+ console.error('❌ Erreur GET /api/compte-rendu-activites:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Erreur serveur',
+ error: error.message
+ });
+ }
+ });
+
+
+ // ⭐ NOUVELLE ROUTE : Saisie de récupération par les RH pour un collaborateur
+ app.post('/compteurs/saisir-recup-collaborateur', authenticateToken, async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ await conn.beginTransaction();
+
+ if (!['Admin', 'RH'].includes(req.user.role)) {
+ await conn.rollback();
+ conn.release();
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const {
+ collaborateur_id,
+ date,
+ periode_journee, // 'Matin', 'Après-midi', 'Journée entière'
+ commentaire
+ } = req.body;
+
+ console.log('📥 === SAISIE RÉCUP PAR RH ===');
+ console.log('Collaborateur:', collaborateur_id);
+ console.log('Date:', date);
+ console.log('Période:', periode_journee);
+
+ if (!collaborateur_id || !date || !periode_journee) {
+ await conn.rollback();
+ conn.release();
+ return res.json({
+ success: false,
+ message: 'Données manquantes'
+ });
+ }
+
+ const dateObj = new Date(date);
+
+ // Calculer le nombre de jours selon la période
+ let nombre_heures;
+ switch (periode_journee) {
+ case 'Matin':
+ case 'Après-midi':
+ nombre_heures = 0.5;
+ break;
+ case 'Journée entière':
+ nombre_heures = 1;
+ break;
+ default:
+ await conn.rollback();
+ conn.release();
+ return res.json({
+ success: false,
+ message: 'Période invalide'
+ });
+ }
+
+ // Vérifier que ce jour/période n'a pas déjà été saisi
+ const [existing] = await conn.query(`
+ SELECT dc.Id
+ FROM DemandeConge dc
+ JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId
+ JOIN TypeConge tc ON dct.TypeCongeId = tc.Id
+ WHERE dc.CollaborateurADId = ?
+ AND dc.DateDebut = ?
+ AND tc.Nom = 'Récupération'
+ AND dct.PeriodeJournee = ?
+ `, [collaborateur_id, date, periode_journee]);
+
+ if (existing.length > 0) {
+ await conn.rollback();
+ conn.release();
+ return res.json({
+ success: false,
+ message: `Cette date (${periode_journee}) a déjà été déclarée`
+ });
+ }
+
+ // Récupérer infos collaborateur
+ const [userInfo] = await conn.query(
+ 'SELECT prenom, nom, email FROM CollaborateurAD WHERE id = ?',
+ [collaborateur_id]
+ );
+
+ if (userInfo.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return res.json({
+ success: false,
+ message: 'Collaborateur non trouvé'
+ });
+ }
+
+ const user = userInfo[0];
+ const userName = `${user.prenom} ${user.nom}`;
+ const dateFormatted = dateObj.toLocaleDateString('fr-FR');
+
+ // Récupérer le type Récupération
+ // 🎨 Récupérer le type Récupération avec sa couleur
+ const [recupType] = await conn.query(
+ 'SELECT Id, CouleurHex FROM TypeConge WHERE Nom = ? LIMIT 1',
+ ['Récupération']
+ );
+
+ if (recupType.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return res.json({
+ success: false,
+ message: 'Type Récupération non trouvé. Veuillez créer ce type de congé.'
+ });
+ }
+
+ const recupTypeId = recupType[0].Id;
+ const couleurRecup = recupType[0].CouleurHex || '#d946ef'; // Fuchsia par défaut
+ const currentYear = dateObj.getFullYear();
+
+ console.log(`🎨 Type Récupération trouvé - ID: ${recupTypeId}, Couleur: ${couleurRecup}`);
+
+ // CRÉER LA DEMANDE (validée automatiquement avec ValidateurADId)
+ const commentaireFinal = commentaire || `🎯Jour travaillé (${periode_journee}) - Saisi par RH`;
+
+ const [result] = await conn.query(`
+ INSERT INTO DemandeConge
+ (CollaborateurADId, DateDebut, DateFin,
+ Statut, DateDemande, DateValidation, ValidateurADId, Commentaire, NombreJours)
+ VALUES (?, ?, ?, 'Validée', NOW(), NOW(), ?, ?, ?)
+`, [collaborateur_id, date, date, req.user.id, commentaireFinal, nombre_heures]);
+
+ const demandeId = result.insertId;
+
+ // SAUVEGARDER DANS DemandeCongeType
+ await conn.query(`
+ INSERT INTO DemandeCongeType
+ (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee)
+ VALUES (?, ?, ?, ?)
+ `, [demandeId, recupTypeId, nombre_heures, periode_journee]);
+
+ // ACCUMULER DANS LE COMPTEUR
+ const [compteur] = await conn.query(`
+ SELECT Id, Total, Solde
+ FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collaborateur_id, recupTypeId, currentYear]);
+
+ if (compteur.length > 0) {
+ await conn.query(`
+ UPDATE CompteurConges
+ SET Total = Total + ?,
+ Solde = Solde + ?,
+ DerniereMiseAJour = NOW()
+ WHERE Id = ?
+ `, [nombre_heures, nombre_heures, compteur[0].Id]);
+
+ console.log(`✅ Compteur mis à jour: ${parseFloat(compteur[0].Solde) + nombre_heures}j`);
+ } else {
+ await conn.query(`
+ INSERT INTO CompteurConges
+ (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour)
+ VALUES (?, ?, ?, ?, ?, 0, NOW())
+ `, [collaborateur_id, recupTypeId, currentYear, nombre_heures, nombre_heures]);
+
+ console.log(`✅ Compteur créé: ${nombre_heures}j`);
+ }
+
+ // ENREGISTRER L'ACCUMULATION
+ await conn.query(`
+ INSERT INTO DeductionDetails
+ (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises)
+ VALUES (?, ?, ?, 'Accum Récup', ?)
+ `, [demandeId, recupTypeId, currentYear, nombre_heures]);
+
+ // CRÉER NOTIFICATION POUR LE COLLABORATEUR
+ await conn.query(`
+ INSERT INTO Notifications
+ (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu)
+ VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, NOW(), 0)
+ `, [
+ collaborateur_id,
+ `Date ${dateFormatted} (${periode_journee}) enregistrée par RH : +${nombre_heures}j de récupération`,
+ demandeId
+ ]);
+
+ // ENREGISTRER L'ACTION DANS L'HISTORIQUE
+ await conn.query(
+ `INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DateAction)
+ VALUES (?, ?, ?, NOW())`,
+ [
+ req.user.id,
+ 'Saisie récupération RH',
+ `Ajout de ${nombre_heures}j (${periode_journee}) pour ${userName} - ${dateFormatted}`
+ ]
+ );
+
+ await conn.commit();
+
+ // ⭐ ENVOYER WEBHOOK AU SERVEUR COLLABORATEURS
+ // ⭐ ENVOYER WEBHOOKS AU SERVEUR COLLABORATEURS
+ // ⭐ ENVOYER WEBHOOKS AU SERVEUR COLLABORATEURS
+ try {
+ // 1. Notifier la création de la demande validée (IMPORTANT pour le calendrier)
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.DEMANDE_VALIDATED,
+ {
+ demandeId: demandeId,
+ statut: 'Validée',
+ collaborateurId: collaborateur_id,
+ validateurId: req.user.id,
+ typeConge: 'Récupération',
+ type: 'Récupération',// ✅ Envoyer le nom exact
+ couleurHex: couleurRecup, // ⭐ AJOUTER LA COULEUR
+ date: date,
+ periode: periode_journee,
+ nombre_heures: nombre_heures
+ }
+ );
+
+ // 2. Notifier la mise à jour du compteur
+ await webhookManager.sendWebhook(
+ WEBHOOKS.COLLABORATEURS_URL,
+ EVENTS.COMPTEUR_UPDATED,
+ {
+ collaborateurId: collaborateur_id,
+ typeUpdate: 'recup_ajoutee',
+ date: date,
+ periode: periode_journee,
+ jours: nombre_heures
+ }
+ );
+
+ console.log('✅ Webhooks envoyés au serveur collaborateurs');
+ } catch (webhookError) {
+ console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message);
+ }
+ // Notifier les clients SSE (IMPORTANT pour rafraîchir le calendrier)
+ notifyClients({
+ type: 'demande-validated',
+ demandeId: demandeId,
+ statut: 'Validée',
+ collaborateurId: collaborateur_id,
+ typeConge: 'Récupération',
+ timestamp: new Date().toISOString()
+ }, collaborateur_id);
+
+ notifyClients({
+ type: 'compteur-updated',
+ collaborateurId: collaborateur_id,
+ typeConge: 'Récupération',
+ action: 'ajout_rh',
+ nombreJours: nombre_heures,
+ timestamp: new Date().toISOString()
+ }, collaborateur_id);
+
+ // ENVOYER EMAIL AU COLLABORATEUR
+ const accessToken = await getGraphToken();
+ if (accessToken && user.email) {
+ const fromEmail = 'noreply@ensup.eu';
+ const subject = '✅ Récupération enregistrée par RH';
+ const body = `
+
+
+
✅ Récupération ajoutée
+
+
+
Bonjour ${userName},
+
Le service RH a enregistré une récupération pour vous.
+
+
Date : ${dateFormatted}
+
Période : ${periode_journee}
+
Jours ajoutés : ${nombre_heures} jour${nombre_heures > 1 ? 's' : ''}
+ ${commentaire ? `
Commentaire : ${commentaire}
` : ''}
+
+
Ces jours sont désormais disponibles dans votre solde de récupération.
+
+
+ `;
+
+ try {
+ await sendMailGraph(accessToken, fromEmail, user.email, subject, body);
+ console.log('✅ Email de notification envoyé');
+ } catch (emailError) {
+ console.error('❌ Erreur envoi email:', emailError);
+ }
+ }
+
+ conn.release();
+ res.json({
+ success: true,
+ message: `Récupération enregistrée pour ${userName}`,
+ collaborateur: userName,
+ date: dateFormatted,
+ periode: periode_journee,
+ jours_ajoutes: nombre_heures,
+ demande_id: demandeId,
+ couleur: couleurRecup // 🎨 Retourner la couleur
+ });
+
+ } catch (error) {
+ await conn.rollback();
+ if (conn) conn.release();
+ console.error('❌ Erreur saisie récup RH:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Erreur serveur',
+ error: error.message
+ });
+ }
+ });
+ // 🆕 BONUS : Route pour exporter le PDF d'un compte-rendu
+ app.get('/export-compte-rendu-pdf', authenticateToken, async (req, res) => {
+ try {
+ if (!['RH', 'Admin'].includes(req.user.role)) {
+ return res.status(403).json({ error: 'Accès non autorisé' });
+ }
+
+ const { user_id, annee, mois } = req.query;
+
+ console.log('📄 Export PDF compte-rendu');
+ console.log(' User ID:', user_id, 'Année:', annee, 'Mois:', mois);
+
+ // Récupérer les infos du collaborateur
+ const [collaborateur] = await pool.query(`
+ SELECT
+ ca.id,
+ ca.nom,
+ ca.prenom,
+ ca.email,
+ s.Nom as service
+ FROM CollaborateurAD ca
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ WHERE ca.id = ?
+ `, [user_id]);
+
+ if (collaborateur.length === 0) {
+ return res.status(404).json({ error: 'Collaborateur non trouvé' });
+ }
+
+ const collab = collaborateur[0];
+
+ // Récupérer les jours
+ const [jours] = await pool.query(`
+ SELECT
+ JourDate,
+ JourTravaille,
+ ReposQuotidienRespect,
+ ReposHebdomadaireRespect,
+ CommentaireRepos
+ FROM CompteRenduActivites
+ WHERE CollaborateurADId = ?
+ AND Annee = ?
+ AND Mois = ?
+ ORDER BY JourDate ASC
+ `, [user_id, annee, mois]);
+
+ // Récupérer le mensuel
+ const [mensuel] = await pool.query(`
+ SELECT
+ NbJoursTravailles,
+ NbJoursNonRespectsReposQuotidien,
+ NbJoursNonRespectsReposHebdo,
+ Statut,
+ Verrouille,
+ DateValidation
+ FROM CompteRenduMensuel
+ WHERE CollaborateurADId = ?
+ AND Annee = ?
+ AND Mois = ?
+ LIMIT 1
+ `, [user_id, annee, mois]);
+
+ const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
+ 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
+
+ // GÉNÉRER LE PDF
+ const doc = new PDFDocument({
+ margin: 50,
+ size: 'A4'
+ });
+
+ // Headers pour le téléchargement
+ res.setHeader('Content-Type', 'application/pdf');
+ res.setHeader('Content-Disposition', `attachment; filename=compte-rendu-${collab.nom}-${annee}-${String(mois).padStart(2, '0')}.pdf`);
+
+ // Pipe le PDF directement dans la réponse
+ doc.pipe(res);
+
+ // ===== EN-TÊTE =====
+ doc.fontSize(20)
+ .fillColor('#0ea5e9')
+ .text('Compte-rendu d\'activités', { align: 'center' })
+ .moveDown(0.5);
+
+ doc.fontSize(14)
+ .fillColor('#6b7280')
+ .text(`${monthNames[parseInt(mois) - 1]} ${annee}`, { align: 'center' })
+ .moveDown(2);
+
+ // ===== INFORMATIONS COLLABORATEUR =====
+ doc.fontSize(12)
+ .fillColor('#000000')
+ .text('Collaborateur', { underline: true })
+ .moveDown(0.3);
+
+ doc.fontSize(10)
+ .fillColor('#374151')
+ .text(`Nom: ${collab.prenom} ${collab.nom}`)
+ .text(`Email: ${collab.email}`)
+ .text(`Service: ${collab.service || 'Non défini'}`)
+ .moveDown(1.5);
+
+ // ===== RÉSUMÉ MENSUEL =====
+ if (mensuel.length > 0) {
+ const m = mensuel[0];
+
+ doc.fontSize(12)
+ .fillColor('#000000')
+ .text('Résumé du mois', { underline: true })
+ .moveDown(0.3);
+
+ // Rectangle de fond pour les stats
+ const y = doc.y;
+ doc.rect(50, y, 495, 80)
+ .fillAndStroke('#f0f9ff', '#0ea5e9');
+
+ doc.fillColor('#000000')
+ .fontSize(10)
+ .text(`Jours travaillés: ${m.NbJoursTravailles || 0}`, 60, y + 10)
+ .text(`Non-respect repos quotidien: ${m.NbJoursNonRespectsReposQuotidien || 0}`, 60, y + 30)
+ .text(`Non-respect repos hebdomadaire: ${m.NbJoursNonRespectsReposHebdo || 0}`, 60, y + 50);
+
+ const statutText = m.Verrouille ? 'Verrouillé' : 'Ouvert';
+ doc.text(`Statut: ${statutText}`, 300, y + 10);
+
+ if (m.DateValidation) {
+ const dateValidation = new Date(m.DateValidation).toLocaleDateString('fr-FR');
+ doc.text(`Date validation: ${dateValidation}`, 300, y + 30);
+ }
+
+ doc.y = y + 90;
+ doc.moveDown(1);
+ }
+
+ // ===== DÉTAIL DES JOURS =====
+ if (jours.length > 0) {
+ doc.fontSize(12)
+ .fillColor('#000000')
+ .text('Détail des jours', { underline: true })
+ .moveDown(0.5);
+
+ // ✅ DÉFINIR LES POSITIONS DES COLONNES
+ const cols = {
+ date: 60,
+ travaille: 150,
+ repos: 285, // Centre de la colonne "Respect des repos"
+ commentaire: 420
+ };
+
+ // En-têtes du tableau (bien alignés)
+ const headerY = doc.y;
+ doc.fontSize(9)
+ .fillColor('#6b7280');
+
+ doc.text('Date', cols.date, headerY, { width: 80, continued: false });
+ doc.text('Travaillé', cols.travaille - 10, headerY, { width: 90, align: 'center', continued: false });
+ doc.text('Respect des repos', cols.repos - 60, headerY, { width: 120, align: 'center', continued: false });
+ doc.text('Commentaire', cols.commentaire, headerY, { width: 125, continued: false });
+
+ doc.moveDown(0.3);
+ doc.moveTo(50, doc.y)
+ .lineTo(545, doc.y)
+ .stroke('#d1d5db');
+ doc.moveDown(0.3);
+
+ // 🎨 Fonction pour dessiner un cercle avec coche verte
+ const drawGreenCheck = (x, y) => {
+ // Cercle vert
+ doc.circle(x, y, 8)
+ .fillAndStroke('#22c55e', '#16a34a');
+
+ // Coche blanche
+ doc.strokeColor('#ffffff')
+ .lineWidth(2)
+ .moveTo(x - 4, y)
+ .lineTo(x - 1, y + 3)
+ .lineTo(x + 4, y - 4)
+ .stroke();
+
+ // Remettre la couleur de trait par défaut
+ doc.strokeColor('#000000').lineWidth(1);
+ };
+
+ // 🎨 Fonction pour dessiner un cercle avec X rouge
+ const drawRedX = (x, y) => {
+ // Cercle rouge
+ doc.circle(x, y, 8)
+ .fillAndStroke('#ef4444', '#dc2626');
+
+ // X blanc
+ doc.strokeColor('#ffffff')
+ .lineWidth(2)
+ .moveTo(x - 4, y - 4)
+ .lineTo(x + 4, y + 4)
+ .stroke();
+
+ doc.moveTo(x + 4, y - 4)
+ .lineTo(x - 4, y + 4)
+ .stroke();
+
+ // Remettre la couleur de trait par défaut
+ doc.strokeColor('#000000').lineWidth(1);
+ };
+
+ // Lignes du tableau
+ jours.forEach((jour, index) => {
+ if (doc.y > 700) {
+ doc.addPage();
+ doc.y = 50;
+ }
+
+ const jourDate = new Date(jour.JourDate).toLocaleDateString('fr-FR');
+ const travaille = jour.JourTravaille ? '✓' : '✗';
+
+ // Vérifier si tout est respecté
+ const toutRespect = jour.ReposQuotidienRespect && jour.ReposHebdomadaireRespect;
+ const commentaire = jour.CommentaireRepos || '-';
+
+ const rowY = doc.y;
+
+ // Alterner les couleurs de fond
+ if (index % 2 === 0) {
+ doc.rect(50, rowY - 2, 495, 20)
+ .fill('#f9fafb');
+ }
+
+ // Date
+ doc.fontSize(8)
+ .fillColor('#000000')
+ .text(jourDate, cols.date, rowY + 3, { width: 80, continued: false });
+
+ // Travaillé (centré)
+ doc.text(travaille, cols.travaille - 10, rowY + 3, { width: 90, align: 'center', continued: false });
+
+ // 🎨 AFFICHER LE CERCLE AVEC COCHE OU X (centré)
+ if (toutRespect) {
+ // ✅ Cercle vert avec coche blanche
+ drawGreenCheck(cols.repos, rowY + 7);
+ } else {
+ // ❌ Cercle rouge avec X blanc
+ drawRedX(cols.repos, rowY + 7);
+
+ // Ajouter les détails en petit texte rouge
+ const details = [];
+ if (!jour.ReposQuotidienRespect) details.push('Q');
+ if (!jour.ReposHebdomadaireRespect) details.push('H');
+
+ doc.fontSize(6)
+ .fillColor('#ef4444')
+ .text(details.join(' '), cols.repos + 12, rowY + 4, { width: 50, continued: false });
+ }
+
+ // Commentaire
+ doc.fillColor('#000000')
+ .fontSize(7)
+ .text(commentaire.substring(0, 30), cols.commentaire, rowY + 3, { width: 125, continued: false });
+
+ doc.moveDown(0.8);
+ });
+ } else {
+ doc.fontSize(10)
+ .fillColor('#6b7280')
+ .text('Aucune saisie pour ce mois', { align: 'center' })
+ .moveDown(1);
+ }
+
+ // ===== PIED DE PAGE =====
+ doc.fontSize(8)
+ .fillColor('#9ca3af')
+ .text(
+ `Document généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}`,
+ 50,
+ 750,
+ { align: 'center' }
+ );
+
+ // Finaliser le PDF
+ doc.end();
+
+ console.log('✅ PDF généré avec succès');
+
+ } catch (error) {
+ console.error('❌ Erreur export PDF:', error);
+
+ // Si le document n'a pas encore été envoyé
+ if (!res.headersSent) {
+ res.status(500).json({ error: error.message });
+ }
+ }
+ });
+ app.post('/v2/compteurs/update', async (req, res) => {
+ const conn = await pool.getConnection();
+
+ try {
+ const { collaborateurId, typeConge, annee, nouveauTotal } = req.body;
+
+ // Sécurité RH
+ if (!['RH', 'Admin'].includes(req.user?.role)) {
+ return res.status(403).json({ error: 'Accès refusé' });
+ }
+
+ // Conversion type CP/RTT → Id
+ const [typeRow] = await conn.query(
+ 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
+ [typeConge === 'CP' ? 'Congé payé' : 'RTT']
+ );
+
+ if (typeRow.length === 0) {
+ return res.status(400).json({ error: 'Type de congé inconnu' });
+ }
+
+ const typeCongeId = typeRow[0].Id;
+
+ // Récupération compteur existant
+ const [compteur] = await conn.query(`
+ SELECT Id, Total, Solde, SoldeReporte
+ FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collaborateurId, typeCongeId, annee]);
+
+ if (compteur.length === 0) {
+ return res.status(404).json({ error: 'Compteur non trouvé' });
+ }
+
+ const compteurId = compteur[0].Id;
+
+ // Mise à jour propre V2
+ await conn.query(`
+ UPDATE CompteurConges
+ SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
+ WHERE Id = ?
+ `, [nouveauTotal, nouveauTotal, compteurId]);
+
+ // Webhook SSE → pour que le collaborateur voit la mise à jour
+ notifyCollabClients({
+ type: 'compteur-updated',
+ collaborateurId,
+ typeConge,
+ annee,
+ nouveauTotal,
+ timestamp: new Date().toISOString()
+ }, collaborateurId);
+
+ res.json({ success: true, message: "Compteur mis à jour (V2)" });
+
+ } catch (error) {
+ console.error('Erreur update compteur V2:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ } finally {
+ conn.release();
+ }
+ });
+
+ // ============================================================
+ // ENDPOINTS API V2 - REQUÊTES CORRIGÉES
+ // Ajustées selon la structure réelle de la base DemandeConge
+ // ============================================================
+
+ app.post('/v2/compteurs/update', async (req, res) => {
+ const conn = await pool.getConnection();
+
+ try {
+ const { collaborateurId, typeConge, annee, nouveauTotal } = req.body;
+
+ // Sécurité RH
+ if (!['RH', 'Admin'].includes(req.user?.role)) {
+ return res.status(403).json({ error: 'Accès refusé' });
+ }
+
+ // Conversion type CP/RTT → Id
+ const [typeRow] = await conn.query(
+ 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1',
+ [typeConge === 'CP' ? 'Congé payé' : 'RTT']
+ );
+
+ if (typeRow.length === 0) {
+ return res.status(400).json({ error: 'Type de congé inconnu' });
+ }
+
+ const typeCongeId = typeRow[0].Id;
+
+ // Récupération compteur existant
+ const [compteur] = await conn.query(`
+ SELECT Id, Total, Solde, SoldeReporte
+ FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collaborateurId, typeCongeId, annee]);
+
+ if (compteur.length === 0) {
+ return res.status(404).json({ error: 'Compteur non trouvé' });
+ }
+
+ const compteurId = compteur[0].Id;
+
+ // Mise à jour propre V2
+ await conn.query(`
+ UPDATE CompteurConges
+ SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
+ WHERE Id = ?
+ `, [nouveauTotal, nouveauTotal, compteurId]);
+
+ // Webhook SSE → pour que le collaborateur voit la mise à jour
+ notifyCollabClients({
+ type: 'compteur-updated',
+ collaborateurId,
+ typeConge,
+ annee,
+ nouveauTotal,
+ timestamp: new Date().toISOString()
+ }, collaborateurId);
+
+ res.json({ success: true, message: "Compteur mis à jour (V2)" });
+
+ } catch (error) {
+ console.error('Erreur update compteur V2:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ } finally {
+ conn.release();
+ }
+ });
+
+ app.get('/v2/compteurs', async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ // ============================================================
+ // CORRECTIONS APPORTÉES :
+ // - c.Nom → c.nom (minuscule dans CollaborateurAD)
+ // - c.Prenom → c.prenom (minuscule)
+ // - c.Email → c.email (minuscule)
+ // - c.Service → c.service (minuscule)
+ // - c.Id → c.id (minuscule)
+ // - c.Role → c.role (minuscule)
+ // - c.EstApprenti → SUPPRIMÉ (n'existe pas dans la table)
+ // ============================================================
+ const [rows] = await conn.query(`
+ SELECT
+ cc.Id AS id,
+ cc.CollaborateurADId AS collaborateurId,
+ cc.TypeCongeId AS typeCongeId,
+ CONCAT(c.nom, ' ', c.prenom) AS employe,
+ c.email AS email,
+ c.service AS service,
+ tc.Nom AS typeConge,
+ cc.Annee AS annee,
+ cc.Total AS total,
+ cc.Solde AS solde,
+ cc.SoldeReporte AS soldeReporte,
+ (cc.Total - cc.Solde) AS consomme,
+ c.TypeContrat AS typeContrat,
+ c.role AS role
+ FROM CompteurConges cc
+ INNER JOIN CollaborateurAD c ON cc.CollaborateurADId = c.id
+ INNER JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
+ ORDER BY c.nom ASC, cc.Annee DESC
+ `);
+
+ return res.json(rows);
+
+ } catch (error) {
+ console.error("❌ Erreur GET compteurs V2 :", error);
+ return res.status(500).json({ message: "Erreur serveur V2" });
+ } finally {
+ conn.release();
+ }
+ });
+
+ app.put('/v2/compteurs/update', async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ const {
+ collaborateurId,
+ typeConge,
+ annee,
+ nouveauTotal,
+ nouveauSolde,
+ source
+ } = req.body;
+
+ if (!collaborateurId || !typeConge || !annee) {
+ return res.status(400).json({ message: "Champs manquants" });
+ }
+
+ // Trouver l'ID du type de congé
+ // Note: Utiliser = au lieu de LIKE pour plus de précision
+ const [typeRow] = await conn.query(
+ "SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1",
+ [typeConge === 'CP' ? 'Congé payé' : 'RTT']
+ );
+ if (typeRow.length === 0) {
+ return res.status(404).json({ message: "Type de congé introuvable" });
+ }
+ const typeCongeId = typeRow[0].Id;
+
+ // Trouver compteur
+ const [comp] = await conn.query(
+ `SELECT Id FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`,
+ [collaborateurId, typeCongeId, annee]
+ );
+ if (comp.length === 0) {
+ return res.status(404).json({ message: "Compteur introuvable" });
+ }
+
+ // Maj compteur
+ await conn.query(`
+ UPDATE CompteurConges
+ SET Total = ?, Solde = ?, DerniereMiseAJour = NOW()
+ WHERE Id = ?
+ `, [nouveauTotal, nouveauSolde, comp[0].Id]);
+
+ res.json({ success: true, message: "Compteur mis à jour (V2)" });
+
+ } catch (err) {
+ console.error("Erreur update compteur V2:", err);
+ res.status(500).json({ message: "Erreur serveur" });
+ } finally {
+ conn.release();
+ }
+ });
+
+ app.post('/v2/compteurs/reinitialiser', async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ const { dateReference, collaborateurId } = req.body;
+
+ if (!dateReference) {
+ return res.status(400).json({ message: "dateReference manquante" });
+ }
+
+ // Appel à ta fonction métier V2 (acquisition + arrêté)
+ await recalculerTousLesCompteursV2(conn, dateReference, collaborateurId);
+
+ res.json({ message: "Réinitialisation V2 effectuée" });
+
+ } catch (err) {
+ console.error("Erreur réinitialisation V2:", err);
+ res.status(500).json({ message: "Erreur serveur" });
+ } finally {
+ conn.release();
+ }
+ });
+
+ app.post('/v2/compteurs/initialiser-tous-manuel', async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ const {
+ anneeActuelle,
+ cpActuel,
+ rttActuel,
+ anneePrecedente,
+ cpPrecedent,
+ rttPrecedent
+ } = req.body;
+
+ if (!anneeActuelle || cpActuel == null)
+ return res.status(400).json({ message: "Champs manquants" });
+
+ const result = await initialiserCompteursManuelV2(
+ conn,
+ anneeActuelle,
+ cpActuel,
+ rttActuel,
+ anneePrecedente,
+ cpPrecedent,
+ rttPrecedent
+ );
+
+ res.json(result);
+
+ } catch (err) {
+ console.error("Erreur init manuelle V2:", err);
+ res.status(500).json({ message: "Erreur serveur" });
+ } finally {
+ conn.release();
+ }
+ });
+
+ // ============================================
+ // CORRECTION : Gérer les dates d'entrée nulles
+ // ============================================
+
+ /**
+ * Calcule l'acquisition CP avec la formule Excel exacte
+ */
+ function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) {
+ const d = new Date(dateReference);
+ d.setHours(0, 0, 0, 0);
+ const annee = d.getFullYear();
+ const mois = d.getMonth() + 1; // 1-12
+
+ // 1. Déterminer le début de l'exercice CP (01/06)
+ let exerciceDebut;
+ if (mois >= 6) {
+ exerciceDebut = new Date(annee, 5, 1); // 01/06/N
+ } else {
+ exerciceDebut = new Date(annee - 1, 5, 1); // 01/06/N-1
+ }
+ exerciceDebut.setHours(0, 0, 0, 0);
+
+ // 2. Ajuster si date d'entrée postérieure
+ let dateDebutAcquis = new Date(exerciceDebut);
+ if (dateEntree && dateEntree !== null) { // ✅ AJOUT : Vérifier que dateEntree n'est pas null
+ const entree = new Date(dateEntree);
+ entree.setHours(0, 0, 0, 0);
+
+ // ✅ AJOUT : Vérifier que la date est valide
+ if (!isNaN(entree.getTime()) && entree > exerciceDebut) {
+ dateDebutAcquis = entree;
+ }
+ }
+
+ // 3. Calculer avec la formule Excel
+ const coeffCP = 25 / 12; // 2.0833
+ const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, coeffCP);
+
+ // 4. Plafonner à 25 jours
+ return Math.min(acquisition, 25);
+ }
+
+ /**
+ * Calcule l'acquisition RTT avec la configuration variable
+ */
+ async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) {
+ const d = new Date(dateReference);
+ d.setHours(0, 0, 0, 0);
+ const annee = d.getFullYear();
+
+ // 1. Récupérer les infos du collaborateur
+ const collabInfo = await conn.query(
+ `SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`,
+ [collaborateurId]
+ );
+
+ if (collabInfo.length === 0) {
+ throw new Error(`Collaborateur ${collaborateurId} non trouvé`);
+ }
+
+ const typeContrat = collabInfo[0].TypeContrat || '37h';
+ const dateEntree = collabInfo[0].DateEntree;
+ const isApprenti = collabInfo[0].role === 'Apprenti';
+
+ // 2. Apprentis : pas de RTT
+ if (isApprenti) {
+ return {
+ acquisition: 0,
+ moisTravailles: 0,
+ config: { joursAnnuels: 0, acquisitionMensuelle: 0 },
+ typeContrat: typeContrat
+ };
+ }
+
+ // 3. Récupérer la configuration RTT
+ const config = await getConfigurationRTT(conn, annee, typeContrat);
+
+ // 4. Début d'acquisition : 01/01/N ou date d'entrée
+ let dateDebutAcquis = new Date(annee, 0, 1); // 01/01/N
+ dateDebutAcquis.setHours(0, 0, 0, 0);
+
+ if (dateEntree && dateEntree !== null) { // ✅ AJOUT : Vérifier que dateEntree n'est pas null
+ const entree = new Date(dateEntree);
+ entree.setHours(0, 0, 0, 0);
+
+ // ✅ AJOUT : Vérifier que la date est valide
+ if (!isNaN(entree.getTime())) {
+ if (entree.getFullYear() === annee && entree > dateDebutAcquis) {
+ dateDebutAcquis = entree;
+ }
+ if (entree.getFullYear() > annee) {
+ return {
+ acquisition: 0,
+ moisTravailles: 0,
+ config: config,
+ typeContrat: typeContrat
+ };
+ }
+ }
+ }
+
+ // 5. Calculer avec la formule Excel
+ const acquisition = calculerAcquisitionFormuleExcel(
+ dateDebutAcquis,
+ d,
+ config.acquisitionMensuelle
+ );
+
+ // 6. Calculer les mois travaillés
+ const moisTravailles = config.acquisitionMensuelle > 0
+ ? acquisition / config.acquisitionMensuelle
+ : 0;
+
+ // 7. Plafonner au maximum annuel
+ const acquisitionFinale = Math.min(acquisition, config.joursAnnuels);
+
+ return {
+ acquisition: Math.round(acquisitionFinale * 100) / 100,
+ moisTravailles: Math.round(moisTravailles * 100) / 100,
+ config: config,
+ typeContrat: typeContrat
+ };
+ }
+ function dateDifMonths(date1, date2) {
+ const d1 = new Date(date1);
+ const d2 = new Date(date2);
+
+ // Vérifier que les dates sont valides
+ if (isNaN(d1.getTime()) || isNaN(d2.getTime())) {
+ console.error('❌ Date invalide dans dateDifMonths');
+ return 0;
+ }
+
+ let months = (d2.getFullYear() - d1.getFullYear()) * 12;
+ months += d2.getMonth() - d1.getMonth();
+
+ // Si le jour de d2 < jour de d1, on n'a pas encore complété le mois
+ if (d2.getDate() < d1.getDate()) {
+ months--;
+ }
+
+ return Math.max(0, months);
+ }
+
+ /**
+ * Formule Excel exacte pour calculer l'acquisition
+ */
+ function calculerAcquisitionFormuleExcel(dateDebut, dateReference, coeffMensuel) {
+ const b1 = new Date(dateDebut);
+ const b2 = new Date(dateReference);
+ b1.setHours(0, 0, 0, 0);
+ b2.setHours(0, 0, 0, 0);
+
+ // ✅ AJOUT : Vérifier que les dates sont valides
+ if (isNaN(b1.getTime()) || isNaN(b2.getTime())) {
+ console.error('❌ Date invalide dans calculerAcquisitionFormuleExcel');
+ return 0;
+ }
+
+ // ✅ AJOUT : Vérifier que coeffMensuel est un nombre
+ if (isNaN(coeffMensuel) || coeffMensuel <= 0) {
+ console.error('❌ Coefficient mensuel invalide:', coeffMensuel);
+ return 0;
+ }
+
+ // Si date référence avant date début
+ if (b2 < b1) return 0;
+
+ // Si même mois et même année
+ if (b1.getFullYear() === b2.getFullYear() && b1.getMonth() === b2.getMonth()) {
+ const joursTotal = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate();
+ const joursAcquis = b2.getDate() - b1.getDate() + 1;
+ return Math.round((joursAcquis / joursTotal) * coeffMensuel * 100) / 100;
+ }
+
+ // 1. Fraction du PREMIER mois
+ const joursFinMoisB1 = new Date(b1.getFullYear(), b1.getMonth() + 1, 0).getDate();
+ const jourB1 = b1.getDate();
+ const fractionPremierMois = (joursFinMoisB1 - jourB1 + 1) / joursFinMoisB1;
+
+ // 2. Mois COMPLETS entre
+ const moisComplets = dateDifMonths(b1, b2) - 1;
+
+ // 3. Fraction du DERNIER mois
+ const joursFinMoisB2 = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate();
+ const jourB2 = b2.getDate();
+ const fractionDernierMois = jourB2 / joursFinMoisB2;
+
+ // 4. Total
+ const totalMois = fractionPremierMois + Math.max(0, moisComplets) + fractionDernierMois;
+ const acquisition = totalMois * coeffMensuel;
+
+ return Math.round(acquisition * 100) / 100;
+ }
+ /**
+ * Récupère la configuration RTT pour une année et un type de contrat
+ */
+ async function getConfigurationRTT(conn, annee, typeContrat = '37h') {
+ try {
+ // Chercher en base de données
+ const config = await conn.query(
+ `SELECT JoursAnnuels, AcquisitionMensuelle
+ FROM ConfigurationRTT
+ WHERE Annee = ? AND TypeContrat = ?
+ LIMIT 1`,
+ [annee, typeContrat]
+ );
+
+ if (config.length > 0) {
+ return {
+ joursAnnuels: parseFloat(config[0].JoursAnnuels),
+ acquisitionMensuelle: parseFloat(config[0].AcquisitionMensuelle)
+ };
+ }
+
+ // Sinon, utiliser les règles par défaut
+ return getConfigurationRTTDefaut(annee, typeContrat);
+ } catch (error) {
+ console.error('Erreur getConfigurationRTT:', error);
+ return getConfigurationRTTDefaut(annee, typeContrat);
+ }
+ }
+
+ /**
+ * Configuration RTT par défaut selon les règles métier
+ */
+ function getConfigurationRTTDefaut(annee, typeContrat = '37h') {
+ // 37h : toujours 10 RTT/an
+ if (typeContrat === '37h' || typeContrat === 'tempspartiel') {
+ return {
+ joursAnnuels: 10,
+ acquisitionMensuelle: 10 / 12 // 0.8333
+ };
+ }
+
+ // Forfait jour : dépend de l'année
+ if (typeContrat === 'forfaitjour') {
+ if (annee <= 2025) {
+ // 2025 et avant : 10 RTT/an
+ return {
+ joursAnnuels: 10,
+ acquisitionMensuelle: 10 / 12 // 0.8333
+ };
+ } else {
+ // 2026 et après : 12 RTT/an
+ return {
+ joursAnnuels: 12,
+ acquisitionMensuelle: 12 / 12 // 1.0
+ };
+ }
+ }
+
+ // Par défaut : 10 RTT/an
+ return {
+ joursAnnuels: 10,
+ acquisitionMensuelle: 10 / 12
+ };
+ }
+
+ // 📊 ROUTE POUR L'ESPACE RH - Tous les compteurs détaillés
+ app.get('/getAllDetailedCounters', async (req, res) => {
+ const conn = await pool.getConnection();
+ try {
+ console.log('📊 Récupération de TOUS les compteurs détaillés (Version Fixée V2 + V1)');
+
+ // Récupérer tous les collaborateurs actifs
+ const [collaborateurs] = await conn.query(`
+ SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.role, ca.TypeContrat, ca.DateEntree,
+ s.Nom as service
+ FROM CollaborateurAD ca
+ LEFT JOIN Services s ON ca.ServiceId = s.Id
+ WHERE ca.Actif = 1 OR ca.Actif IS NULL
+ ORDER BY ca.nom, ca.prenom
+ `);
+
+ const resultats = [];
+ const currentYear = new Date().getFullYear();
+ const previousYear = currentYear - 1;
+
+ // Pré-charger les TypeConge Id
+ const [[cpType]] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Congé payé' LIMIT 1`);
+ const [[rttType]] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`);
+ const [[recupType]] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`);
+
+ for (const collab of collaborateurs) {
+
+ //
+ // -----------------------------------------------------
+ // 1️⃣ CONGÉS PAYÉS — ANNÉE N (V2 si existant, sinon V1)
+ // -----------------------------------------------------
+ //
+
+ let [cpN] = await conn.query(`
+ SELECT Total, Solde
+ FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collab.id, cpType.Id, currentYear]);
+
+ let cpTotalN, cpSoldeN;
+
+ if (cpN.length > 0) {
+ // Utiliser données V2
+ cpTotalN = parseFloat(cpN[0].Total);
+ cpSoldeN = parseFloat(cpN[0].Solde);
+ } else {
+ // Fallback ancien calcul
+ cpTotalN = calculerAcquisitionCP(new Date(), collab.DateEntree);
+ const [[consomme]] = await conn.query(`
+ SELECT COALESCE(SUM(JoursUtilises), 0) AS total
+ FROM DeductionDetails dd
+ JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
+ WHERE dc.CollaborateurADId = ?
+ AND dd.TypeCongeId = ?
+ AND dd.Annee = ?
+ AND dd.TypeDeduction NOT IN ('Accum. Récup', 'N Anticipé')
+ AND dc.Statut != 'Refusé'
+ `, [collab.id, cpType.Id, currentYear]);
+
+ cpSoldeN = Math.max(0, cpTotalN - consomme.total);
+ }
+
+ resultats.push({
+ collaborateurId: collab.id,
+ employe: `${collab.prenom} ${collab.nom}`,
+ email: collab.email,
+ service: collab.service || 'Non assigné',
+ typeConge: "Congé payé",
+ annee: currentYear,
+ total: parseFloat(cpTotalN.toFixed(2)),
+ solde: parseFloat(cpSoldeN.toFixed(2)),
+ consomme: parseFloat((cpTotalN - cpSoldeN).toFixed(2)),
+ role: collab.role,
+ typeContrat: collab.TypeContrat
+ });
+
+ //
+ // -----------------------------------------------------
+ // 2️⃣ CP N-1 (toujours basé sur CompteurConges)
+ // -----------------------------------------------------
+ //
+
+ let [cpN1] = await conn.query(`
+ SELECT Total, Solde
+ FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collab.id, cpType.Id, previousYear]);
+
+ if (cpN1.length > 0 && cpN1[0].Solde > 0) {
+ const total = parseFloat(cpN1[0].Total);
+ const solde = parseFloat(cpN1[0].Solde);
+
+ resultats.push({
+ collaborateurId: collab.id,
+ employe: `${collab.prenom} ${collab.nom}`,
+ email: collab.email,
+ service: collab.service || 'Non assigné',
+ typeConge: "Congé payé",
+ annee: previousYear,
+ total,
+ solde,
+ consomme: total - solde,
+ role: collab.role,
+ typeContrat: collab.TypeContrat
+ });
+ }
+
+ //
+ // -----------------------------------------------------
+ // 3️⃣ RTT — ANNÉE N (V2 si existant)
+ // -----------------------------------------------------
+ //
+
+ if (collab.role !== 'Apprenti') {
+ let [rttN] = await conn.query(`
+ SELECT Total, Solde
+ FROM CompteurConges
+ WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
+ `, [collab.id, rttType.Id, currentYear]);
+
+ let rttTotalN, rttSoldeN;
+
+ if (rttN.length > 0) {
+ rttTotalN = parseFloat(rttN[0].Total);
+ rttSoldeN = parseFloat(rttN[0].Solde);
+ } else {
+ // Fallback ancien calcul
+ const rtt = await calculerAcquisitionRTT(conn, collab.id, new Date());
+
+ const [[consommeRTT]] = await conn.query(`
+ SELECT COALESCE(SUM(JoursUtilises), 0) AS total
+ FROM DeductionDetails dd
+ JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
+ WHERE dc.CollaborateurADId = ?
+ AND dd.TypeCongeId = ?
+ AND dd.Annee = ?
+ AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Récup Dosée')
+ AND dc.Statut != 'Refusé'
+ `, [collab.id, rttType.Id, currentYear]);
+
+ rttTotalN = rtt.acquisition;
+ rttSoldeN = Math.max(0, rttTotalN - consommeRTT.total);
+ }
+
+ resultats.push({
+ collaborateurId: collab.id,
+ employe: `${collab.prenom} ${collab.nom}`,
+ email: collab.email,
+ service: collab.service || 'Non assigné',
+ typeConge: "RTT",
+ annee: currentYear,
+ total: parseFloat(rttTotalN.toFixed(2)),
+ solde: parseFloat(rttSoldeN.toFixed(2)),
+ consomme: parseFloat((rttTotalN - rttSoldeN).toFixed(2)),
+ role: collab.role,
+ typeContrat: collab.TypeContrat
+ });
+ }
+
+ //
+ // -----------------------------------------------------
+ // 4️⃣ RÉCUP — ANNÉE N
+ // -----------------------------------------------------
+ //
+
+ const [[accum]] = await conn.query(`
+ SELECT COALESCE(SUM(JoursUtilises), 0) AS total
+ FROM DeductionDetails dd
+ JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
+ WHERE dc.CollaborateurADId = ?
+ AND dd.TypeCongeId = ?
+ AND dd.Annee = ?
+ AND dd.TypeDeduction IN ('Accum. Récup', 'Accum. Recup')
+ `, [collab.id, recupType.Id, currentYear]);
+
+ const [[consomm]] = await conn.query(`
+ SELECT COALESCE(SUM(JoursUtilises), 0) AS total
+ FROM DeductionDetails dd
+ JOIN DemandeConge dc ON dc.Id = dd.DemandeCongeId
+ WHERE dc.CollaborateurADId = ?
+ AND dd.TypeCongeId = ?
+ AND dd.Annee = ?
+ AND dd.TypeDeduction IN ('Récup Dosée', 'Recup Dosee')
+ `, [collab.id, recupType.Id, currentYear]);
+
+ const recupTotal = parseFloat(accum.total);
+ const recupCons = parseFloat(consomm.total);
+ const recupSolde = Math.max(0, recupTotal - recupCons);
+
+ if (recupTotal + recupCons > 0) {
+ resultats.push({
+ collaborateurId: collab.id,
+ employe: `${collab.prenom} ${collab.nom}`,
+ email: collab.email,
+ service: collab.service || 'Non assigné',
+ typeConge: "Récupération",
+ annee: currentYear,
+ total: recupTotal,
+ solde: recupSolde,
+ consomme: recupCons,
+ role: collab.role,
+ typeContrat: collab.TypeContrat
+ });
+ }
+ }
+
+ conn.release();
+ res.json(resultats);
+
+ } catch (err) {
+ conn.release();
+ console.error("❌ Erreur GET ALL:", err);
+ res.status(500).json({ error: err.message });
+ }
+ });
+
+
+ // ✅ ROUTE STATS MANQUANTE
+ app.get('/api/stats', authenticateToken, async (req, res) => {
+ try {
+ console.log('📊 GET /api/stats appelé par', req.user.email);
+
+ // Stats simples pour test
+ const stats = {
+ totalEmployes: 0,
+ demandesEnAttente: 0,
+ demandesValidees: 0,
+ timestamp: new Date().toISOString()
+ };
+
+ // TODO: Requêtes SQL réelles ici
+ res.json(stats);
+ } catch (error) {
+ console.error('Erreur /api/stats:', error);
+ res.status(500).json({ error: 'Erreur stats' });
+ }
+ });
+
+
+
+ // ================================================
+ // DÉMARRAGE DU SERVEUR
+ // ================================================
+
+ console.log('✅ 99. Toutes les routes définies, démarrage du serveur...');
+
+ const server = app.listen(PORT, () => {
+ console.log('\n================================================');
+ console.log(`✅ SERVEUR RH DÉMARRÉ sur http://localhost:${PORT}`);
+ console.log('🔔 Server-Sent Events activés sur /api/events');
+ console.log('🔗 WEBHOOKS configurés:');
+ console.log(` - Serveur Collaborateurs: ${WEBHOOKS.COLLABORATEURS_URL}`);
+ console.log(` - Route webhook receiver: POST /api/webhook/receive`);
+ console.log('');
+ console.log('📋 Routes disponibles:');
+ console.log(' - GET /api/events (SSE)');
+ console.log(' - POST /api/webhook/receive');
+ console.log(' - POST /api/auth/login ✅ AVEC FILTRE ACTIF');
+ console.log(' - GET /api/demandes');
+ console.log(' - GET /api/demandes/:id');
+ console.log(' - POST /api/demandes');
+ console.log(' - PUT /api/demandes/:id');
+ console.log(' - DELETE /api/demandes/:id');
+ console.log(' - PUT /api/demandes/:id/valider');
+ console.log(' - GET /api/employes?include_inactifs=true ✅ MODIFIÉ');
+ console.log(' - POST /api/employes/desactiver 🆕 NOUVEAU');
+ console.log(' - POST /api/employes/reactiver 🆕 NOUVEAU');
+ console.log(' - GET /api/types-conge');
+ console.log(' - GET /api/export/paie');
+ console.log(' - GET /api/compteurs?include_inactifs=true ✅ MODIFIÉ');
+ console.log(' - POST /api/compteurs/reinitialiser');
+ console.log(' - POST /api/compteurs/initialiser-manuel');
+ console.log(' - POST /api/compteurs/initialiser-tous-manuel ✅ MODIFIÉ');
+ console.log(' - PUT /api/compteurs/:id');
+ console.log(' - POST /api/compteurs/ajouter-recup 🆕 NOUVEAU');
+ console.log(' - POST /api/compteurs/retirer-recup 🆕 NOUVEAU');
+ console.log(' - GET /api/equipes ✅ MODIFIÉ');
+ console.log(' - GET /api/equipes/:id ✅ MODIFIÉ');
+ console.log(' - GET /api/equipes/:id/membres ✅ MODIFIÉ');
+ console.log(' - GET /api/stats ✅ MODIFIÉ');
+ console.log(' - GET /api/historique');
+ console.log('================================================');
+ });
+
+ server.on('error', (error) => {
+ console.error('\n❌ ERREUR SERVEUR:', error);
+ if (error.code === 'EADDRINUSE') {
+ console.error(`⚠️ Le port ${PORT} est déjà utilisé`);
+ }
+ });
+
+ setInterval(() => { }, 60000);
+
+} catch (error) {
+ console.error('\n❌❌❌ ERREUR FATALE AU CHARGEMENT ❌❌❌');
+ console.error(error);
+ console.error(error.stack);
+ process.exit(1);
+}
\ No newline at end of file
diff --git a/Backend/webhook-config.js b/Backend/webhook-config.js
new file mode 100644
index 0000000..cfe85e8
--- /dev/null
+++ b/Backend/webhook-config.js
@@ -0,0 +1,18 @@
+// Configuration des webhooks
+const WEBHOOKS = {
+ COLLABORATEURS_URL: process.env.COLLABORATEURS_URL || 'http://localhost:3000',
+ RH_URL: process.env.RH_URL || 'http://localhost:3001',
+ SECRET_KEY: process.env.WEBHOOK_SECRET || 'secret-key-securise'
+};
+
+// Types d'événements
+const EVENTS = {
+ DEMANDE_VALIDATED: 'demande.validated',
+ DEMANDE_CREATED: 'demande.created',
+ DEMANDE_UPDATED: 'demande.updated',
+ DEMANDE_CANCELLED: 'demande.cancelled',
+ DEMANDE_DELETED: 'demande.deleted',
+ COMPTEUR_UPDATED: 'compteur.updated'
+};
+
+module.exports = { WEBHOOKS, EVENTS };
\ No newline at end of file
diff --git a/Backend/webhook-utils.js b/Backend/webhook-utils.js
new file mode 100644
index 0000000..9fb4d78
--- /dev/null
+++ b/Backend/webhook-utils.js
@@ -0,0 +1,111 @@
+// webhook-utils.js (VERSION COMMONJS)
+const axios = require('axios');
+const crypto = require('crypto');
+
+class WebhookManager {
+ constructor(secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ /**
+ * Génère une signature HMAC SHA-256 pour sécuriser le webhook
+ * @param {Object} payload - Les données à signer
+ * @returns {string} La signature hexadécimale
+ */
+ generateSignature(payload) {
+ return crypto
+ .createHmac('sha256', this.secretKey)
+ .update(JSON.stringify(payload))
+ .digest('hex');
+ }
+
+ /**
+ * Vérifie la signature d'un webhook reçu
+ * @param {Object} payload - Les données reçues
+ * @param {string} receivedSignature - La signature reçue dans le header
+ * @returns {boolean} True si la signature est valide
+ */
+ verifySignature(payload, receivedSignature) {
+ if (!receivedSignature) {
+ console.error('❌ Aucune signature fournie');
+ return false;
+ }
+
+ try {
+ const expectedSignature = this.generateSignature(payload);
+ return crypto.timingSafeEqual(
+ Buffer.from(expectedSignature),
+ Buffer.from(receivedSignature)
+ );
+ } catch (error) {
+ console.error('❌ Erreur vérification signature:', error);
+ return false;
+ }
+ }
+
+ /**
+ * Envoie un webhook à une URL cible avec retry automatique
+ * @param {string} targetUrl - URL du serveur cible
+ * @param {string} eventType - Type d'événement (ex: 'demande.validated')
+ * @param {Object} data - Données de l'événement
+ * @param {number} retries - Nombre de tentatives (défaut: 3)
+ * @returns {Promise