From 91cd1dff2f9cf49740848399119c905f1186c7dd Mon Sep 17 00:00:00 2001 From: Imer ouijdane Date: Mon, 12 Jan 2026 12:16:53 +0100 Subject: [PATCH] First_Commit --- docker-compose.yml | 20 +- package-lock.json | 229 +- package.json | 4 +- project/DockerfileGTA.frontend | 14 +- project/public/Backend/DockerfileGTA.backend | 2 +- project/public/Backend/package.json | 5 +- project/public/Backend/server-test.js | 602 +- project/public/Backend/server.js | 5646 +++++++++++------ project/src/App.jsx | 4 +- project/src/AuthConfig.js | 45 +- .../src/components/EditLeaveRequestModal.jsx | 899 ++- project/src/components/GlobalTutorial.jsx | 4 +- .../src/components/NewLeaveRequestModal.jsx | 387 +- project/src/components/ProtectedRoute.jsx | 41 +- project/src/components/Sidebar.jsx | 22 +- project/src/context/AuthContext.jsx | 260 +- project/src/main.jsx | 122 +- project/src/pages/Calendar.jsx | 421 +- project/src/pages/Collaborateur.jsx | 8 +- project/src/pages/CompteRenduActivite.jsx | 235 +- project/src/pages/Dashboard.jsx | 196 +- project/src/pages/EmployeeDetails.jsx | 296 +- project/src/pages/Login.jsx | 54 +- project/src/pages/Manager.jsx | 171 +- project/src/pages/Requests.jsx | 122 +- project/vite.config.js | 4 +- 26 files changed, 6720 insertions(+), 3093 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 54a13ed..39db9ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,36 +1,38 @@ services: backend: + image: ouijdaneim/gta-backend-dev:latest # ✅ Ajoute cette ligne build: context: ./project/public/Backend dockerfile: DockerfileGTA.backend - container_name: gta-backend + container_name: gtaDev-backend hostname: backend ports: - - "8012:3000" + - "8014:3004" volumes: - ./project/public/Backend/uploads:/app/uploads networks: - - gta-network + - gtaDev-network restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" frontend: + image: ouijdaneim/gta-frontend-dev:latest # ✅ Ajoute cette ligne build: context: ./project dockerfile: DockerfileGTA.frontend - container_name: gta-frontend + container_name: gtaDev-frontend hostname: frontend ports: - - "3013:80" + - "3015:90" environment: - - VITE_API_URL=http://backend:3000 + - VITE_API_URL=http://backend:3004 networks: - - gta-network + - gtaDev-network depends_on: - backend restart: unless-stopped networks: - gta-network: - driver: bridge \ No newline at end of file + gtaDev-network: + driver: bridge diff --git a/package-lock.json b/package-lock.json index 69facfc..48e7ab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,9 +6,11 @@ "": { "dependencies": { "cors": "^2.8.5", + "date-fns": "^4.1.0", "express": "^5.1.0", "framer-motion": "^12.23.22", - "node-cron": "^4.2.1" + "node-cron": "^4.2.1", + "react-datepicker": "^9.1.0" }, "devDependencies": { "@testing-library/jest-dom": "^6.8.0", @@ -79,7 +81,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -675,7 +676,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -699,15 +699,14 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "dev": true, "license": "MIT", "optional": true, @@ -717,9 +716,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "dev": true, "license": "MIT", "optional": true, @@ -1180,6 +1179,59 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2017,27 +2069,6 @@ "@sinonjs/commons": "^3.0.1" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", @@ -2094,9 +2125,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, @@ -2104,13 +2135,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2946,7 +2970,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -3236,6 +3259,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3390,6 +3422,16 @@ "node": ">=18" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -3478,13 +3520,6 @@ "node": ">=8" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5464,7 +5499,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -5579,16 +5613,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.30.18", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", @@ -6284,34 +6308,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -6414,38 +6410,27 @@ "url": "https://opencollective.com/express" } }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "devOptional": true, + "node_modules/react-datepicker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz", + "integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==", "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "devOptional": true, - "license": "MIT", - "peer": true, "dependencies": { - "scheduler": "^0.26.0" + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" }, "peerDependencies": { - "react": "^19.1.1" + "date-fns-tz": "^3.0.0", + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "date-fns-tz": { + "optional": true + } } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -6619,13 +6604,6 @@ "node": ">=v12.22.7" } }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "devOptional": true, - "license": "MIT" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -7130,6 +7108,12 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "license": "MIT" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7246,7 +7230,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7531,7 +7514,6 @@ "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -7648,7 +7630,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index f0b2c87..54a06b5 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ }, "dependencies": { "cors": "^2.8.5", + "date-fns": "^4.1.0", "express": "^5.1.0", "framer-motion": "^12.23.22", - "node-cron": "^4.2.1" + "node-cron": "^4.2.1", + "react-datepicker": "^9.1.0" } } diff --git a/project/DockerfileGTA.frontend b/project/DockerfileGTA.frontend index 6d5281f..341818c 100644 --- a/project/DockerfileGTA.frontend +++ b/project/DockerfileGTA.frontend @@ -1,4 +1,4 @@ -FROM node:18-alpine +FROM node:20-alpine WORKDIR /app @@ -26,12 +26,12 @@ export default defineConfig({ }, server: { host: '0.0.0.0', - port: 80, + port: 90, strictPort: true, - allowedHosts: ['mygta.ensup-adm.net', 'localhost'], + allowedHosts: ['mygta-dev.ensup-adm.net', 'localhost'], proxy: { '/api': { - target: 'http://gta-backend:3000', + target: 'http://backend:3004', changeOrigin: true, secure: false, configure: (proxy, options) => { @@ -39,7 +39,7 @@ export default defineConfig({ console.log('Proxy error:', err); }); proxy.on('proxyReq', (proxyReq, req, res) => { - console.log('Proxying:', req.method, req.url, '-> http://gta-backend:3000'); + console.log('Proxying:', req.method, req.url, '-> http://backend:3004'); }); } } @@ -48,6 +48,6 @@ export default defineConfig({ }); VITECONFIG -EXPOSE 80 +EXPOSE 90 -CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "80"] +CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "90"] diff --git a/project/public/Backend/DockerfileGTA.backend b/project/public/Backend/DockerfileGTA.backend index 5a14dfd..8889208 100644 --- a/project/public/Backend/DockerfileGTA.backend +++ b/project/public/Backend/DockerfileGTA.backend @@ -18,7 +18,7 @@ COPY . . RUN mkdir -p /app/uploads/medical # Expose the port -EXPOSE 3000 +EXPOSE 3004 # Start the server CMD ["node", "server.js"] diff --git a/project/public/Backend/package.json b/project/public/Backend/package.json index 4f1eec3..a5c2ab2 100644 --- a/project/public/Backend/package.json +++ b/project/public/Backend/package.json @@ -1,4 +1,4 @@ -{ +{ "name": "gta-backend", "version": "1.0.0", "description": "GTA Backend API", @@ -10,7 +10,7 @@ }, "dependencies": { "express": "^4.18.2", - "mysql2": "^3.6.5", + "mssql": "^10.0.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "multer": "^1.4.5-lts.1", @@ -19,6 +19,7 @@ "body-parser": "^1.20.2", "axios": "^1.6.0", "node-cron": "^3.0.3" + }, "engines": { "node": ">=18.0.0" diff --git a/project/public/Backend/server-test.js b/project/public/Backend/server-test.js index 3c85606..d27c4f6 100644 --- a/project/public/Backend/server-test.js +++ b/project/public/Backend/server-test.js @@ -1,6 +1,7 @@ import express from 'express'; import cors from 'cors'; -import mysql from 'mysql2/promise'; +import sql from 'mssql'; +import axios from 'axios'; const app = express(); const PORT = 3000; @@ -8,117 +9,562 @@ const PORT = 3000; app.use(cors({ origin: '*' })); app.use(express.json()); -// Configuration de connexion MySQL -const dbConfig = { - host: '192.168.0.4', - user: 'wpuser', - password: '-2b/)ru5/Bi8P[7_', - database: 'DemandeConge', - port: 3306, - charset: 'utf8mb4', - connectTimeout: 60000, +// Configuration Azure AD +const AZURE_CONFIG = { + tenantId: '9840a2a0-6ae1-4688-b03d-d2ec291be0f9', + clientId: '4bb4cc24-bac3-427c-b02c-5d14fc67b561', + clientSecret: 'gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t', + groupId: 'c1ea877c-6bca-4f47-bfad-f223640813a0' }; -// ✅ CRÉER LE POOL ICI, AU NIVEAU GLOBAL -const pool = mysql.createPool(dbConfig); +// Configuration SQL Server +const dbConfig = { + server: '192.168.0.3', + user: 'gta_app', + password: 'GTA2025!Secure', + database: 'GTA', + port: 1433, + options: { + encrypt: true, + trustServerCertificate: true, + enableArithAbort: true, + connectTimeout: 60000, + requestTimeout: 60000 + }, + pool: { + max: 10, + min: 0, + idleTimeoutMillis: 30000 + } +}; -// Route test connexion base + comptage collaborateurs +// Créer le pool de connexions +const pool = new sql.ConnectionPool(dbConfig); + +// Connexion au démarrage +pool.connect() + .then(() => { + console.log('✅ Connecté à SQL Server'); + console.log(` Base: ${dbConfig.database}@${dbConfig.server}`); + }) + .catch(err => { + console.error('❌ Erreur connexion SQL Server:', err.message); + }); + +// ======================================== +// WRAPPER POUR COMPATIBILITÉ (style MySQL) +// ======================================== +pool.query = async function (queryText, params = []) { + if (!pool.connected) { + await pool.connect(); + } + + const request = pool.request(); + + // Ajouter les paramètres + params.forEach((value, index) => { + request.input(`param${index}`, value); + }); + + // Remplacer ? par @param0, @param1, etc. + let parameterizedQuery = queryText; + let paramIndex = 0; + parameterizedQuery = parameterizedQuery.replace(/\?/g, () => `@param${paramIndex++}`); + + // Conversion LIMIT → TOP + parameterizedQuery = parameterizedQuery.replace( + /LIMIT\s+(\d+)/gi, + (match, limit) => { + return parameterizedQuery.includes('SELECT') + ? parameterizedQuery.replace(/SELECT/i, `SELECT TOP ${limit}`) + : ''; + } + ); + + const result = await request.query(parameterizedQuery); + return result.recordset || []; +}; + +// ======================================== +// 🔑 FONCTION TOKEN MICROSOFT GRAPH +// ======================================== +async function getGraphToken() { + try { + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: AZURE_CONFIG.clientId, + client_secret: AZURE_CONFIG.clientSecret, + scope: 'https://graph.microsoft.com/.default' + }); + + const response = await axios.post( + `https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`, + params.toString(), + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ); + + return response.data.access_token; + } catch (error) { + console.error('❌ Erreur obtention token:', error.message); + return null; + } +} + +// ======================================== +// 🔄 FONCTION SYNCHRONISATION ENTRA ID +// ======================================== +async function syncEntraIdUsers() { + const syncResults = { + processed: 0, + inserted: 0, + updated: 0, + deactivated: 0, + errors: [] + }; + + try { + console.log('\n🔄 === DÉBUT SYNCHRONISATION ENTRA ID ==='); + + // 1️⃣ Obtenir le token + const accessToken = await getGraphToken(); + if (!accessToken) { + console.error('❌ Impossible d\'obtenir le token'); + return syncResults; + } + console.log('✅ Token obtenu'); + + // 2️⃣ Récupérer le groupe + const groupResponse = await axios.get( + `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, + { headers: { Authorization: `Bearer ${accessToken}` } } + ); + const groupName = groupResponse.data.displayName; + console.log(`📋 Groupe : ${groupName}`); + + // 3️⃣ Récupérer tous les membres avec pagination + let allAzureMembers = []; + let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle,officeLocation,accountEnabled&$top=999`; + + console.log('📥 Récupération des membres...'); + while (nextLink) { + const membersResponse = await axios.get(nextLink, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + allAzureMembers = allAzureMembers.concat(membersResponse.data.value); + nextLink = membersResponse.data['@odata.nextLink']; + + if (nextLink) { + console.log(` 📄 ${allAzureMembers.length} membres récupérés...`); + } + } + + console.log(`✅ ${allAzureMembers.length} membres trouvés`); + + // 4️⃣ Filtrer les membres valides + const validMembers = allAzureMembers.filter(m => { + if (!m.mail || m.mail.trim() === '') return false; + if (m.accountEnabled === false) return false; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(m.mail); + }); + + console.log(`✅ ${validMembers.length} membres valides`); + + // 5️⃣ Traitement avec transaction + const transaction = new sql.Transaction(pool); + await transaction.begin(); + + try { + const azureEmails = new Set(); + validMembers.forEach(m => { + azureEmails.add(m.mail.toLowerCase().trim()); + }); + + console.log('\n📝 Traitement des utilisateurs...'); + + // 6️⃣ Pour chaque membre + for (const m of validMembers) { + try { + const emailClean = m.mail.toLowerCase().trim(); + syncResults.processed++; + + // Vérifier existence + const request = new sql.Request(transaction); + request.input('email', sql.NVarChar, emailClean); + + const result = await request.query(` + SELECT id, email, entraUserId, actif + FROM CollaborateurAD + WHERE LOWER(email) = LOWER(@email) + `); + + if (result.recordset.length > 0) { + // MISE À JOUR + const updateRequest = new sql.Request(transaction); + updateRequest.input('entraUserId', sql.NVarChar, m.id); + updateRequest.input('prenom', sql.NVarChar, m.givenName || ''); + updateRequest.input('nom', sql.NVarChar, m.surname || ''); + updateRequest.input('departement', sql.NVarChar, m.department || ''); + updateRequest.input('fonction', sql.NVarChar, m.jobTitle || ''); + updateRequest.input('campus', sql.NVarChar, m.officeLocation || ''); + updateRequest.input('email', sql.NVarChar, emailClean); + + await updateRequest.query(` + UPDATE CollaborateurAD + SET + entraUserId = @entraUserId, + prenom = @prenom, + nom = @nom, + departement = @departement, + fonction = @fonction, + campus = @campus, + actif = 1 + WHERE LOWER(email) = LOWER(@email) + `); + + syncResults.updated++; + console.log(` ✓ Mis à jour : ${emailClean}`); + + } else { + // INSERTION + const insertRequest = new sql.Request(transaction); + insertRequest.input('entraUserId', sql.NVarChar, m.id); + insertRequest.input('prenom', sql.NVarChar, m.givenName || ''); + insertRequest.input('nom', sql.NVarChar, m.surname || ''); + insertRequest.input('email', sql.NVarChar, emailClean); + insertRequest.input('departement', sql.NVarChar, m.department || ''); + insertRequest.input('fonction', sql.NVarChar, m.jobTitle || ''); + insertRequest.input('campus', sql.NVarChar, m.officeLocation || ''); + + await insertRequest.query(` + INSERT INTO CollaborateurAD + (entraUserId, prenom, nom, email, departement, fonction, campus, role, SocieteId, actif, dateCreation, TypeContrat) + VALUES (@entraUserId, @prenom, @nom, @email, @departement, @fonction, @campus, 'Collaborateur', 1, 1, GETDATE(), '37h') + `); + + syncResults.inserted++; + console.log(` ✓ Créé : ${emailClean}`); + } + + } catch (userError) { + syncResults.errors.push({ + email: m.mail, + error: userError.message + }); + console.error(` ❌ Erreur ${m.mail}:`, userError.message); + } + } + + // 7️⃣ DÉSACTIVATION des comptes absents + console.log('\n🔍 Désactivation des comptes obsolètes...'); + + if (azureEmails.size > 0) { + const activeEmailsList = Array.from(azureEmails).map(e => `'${e}'`).join(','); + + const deactivateRequest = new sql.Request(transaction); + const deactivateResult = await deactivateRequest.query(` + UPDATE CollaborateurAD + SET actif = 0 + WHERE + email IS NOT NULL + AND email != '' + AND LOWER(email) NOT IN (${activeEmailsList}) + AND actif = 1 + `); + + syncResults.deactivated = deactivateResult.rowsAffected[0]; + console.log(` ✓ ${syncResults.deactivated} compte(s) désactivé(s)`); + } + + await transaction.commit(); + + console.log('\n📊 === RÉSUMÉ ==='); + console.log(` Groupe: ${groupName}`); + console.log(` Total Entra: ${allAzureMembers.length}`); + console.log(` Valides: ${validMembers.length}`); + console.log(` Traités: ${syncResults.processed}`); + console.log(` Créés: ${syncResults.inserted}`); + console.log(` Mis à jour: ${syncResults.updated}`); + console.log(` Désactivés: ${syncResults.deactivated}`); + console.log(` Erreurs: ${syncResults.errors.length}`); + + } catch (error) { + await transaction.rollback(); + throw error; + } + + } catch (error) { + console.error('\n❌ ERREUR SYNCHRONISATION:', error.message); + } + + return syncResults; +} + +// ======================================== +// 📡 ROUTES API +// ======================================== + +// Route test connexion app.get('/api/db-status', async (req, res) => { try { - const [rows] = await pool.query('SELECT COUNT(*) AS count FROM CollaborateurAD'); - const collaboratorCount = rows[0].count; + const result = await pool.query('SELECT COUNT(*) AS count FROM CollaborateurAD', []); + const collaboratorCount = result[0]?.count || 0; + res.json({ success: true, - message: 'Connexion à la base OK', + message: 'Connexion SQL Server OK', collaboratorCount, }); } catch (error) { - console.error('Erreur connexion base:', error); + console.error('Erreur connexion:', error); res.status(500).json({ success: false, - message: 'Erreur de connexion à la base', + message: 'Erreur connexion base', error: error.message, }); } }); +// Route sync unitaire app.post('/api/initial-sync', async (req, res) => { - let conn; try { - conn = await pool.getConnection(); - const email = req.body.mail || req.body.userPrincipalName; - const entraId = req.body.id; - console.log('🔄 Initial Sync pour:', email); + const email = (req.body.mail || req.body.userPrincipalName)?.toLowerCase().trim(); + const entraUserId = req.body.id; - // 1. Chercher user - const [users] = await conn.query('SELECT * FROM CollaborateurAD WHERE email = ?', [email]); - let userId; - let userRole; + if (!email) { + return res.json({ success: false, message: 'Email manquant' }); + } + + console.log(`\n🔄 Sync utilisateur : ${email}`); + + const transaction = new sql.Transaction(pool); + await transaction.begin(); + + try { + // Vérifier existence + const checkRequest = new sql.Request(transaction); + checkRequest.input('email', sql.NVarChar, email); + + const existing = await checkRequest.query(` + SELECT id, email, actif + FROM CollaborateurAD + WHERE LOWER(email) = LOWER(@email) + `); + + if (existing.recordset.length > 0) { + // UPDATE + const updateRequest = new sql.Request(transaction); + updateRequest.input('collaborateurADId', sql.NVarChar, entraUserId); + updateRequest.input('prenom', sql.NVarChar, req.body.givenName || ''); + updateRequest.input('nom', sql.NVarChar, req.body.surname || ''); + updateRequest.input('departement', sql.NVarChar, req.body.department || ''); + updateRequest.input('fonction', sql.NVarChar, req.body.jobTitle || ''); + updateRequest.input('campus', sql.NVarChar, req.body.officeLocation || ''); + updateRequest.input('email', sql.NVarChar, email); + updateRequest.input('dateMaj', sql.DateTime, new Date()); + + await updateRequest.query(` + UPDATE CollaborateurAD + SET + CollaborateurADId = @collaborateurADId, + prenom = @prenom, + nom = @nom, + departement = @departement, + fonction = @fonction, + campus = @campus, + actif = 1, + dateMiseAJour = @dateMaj + WHERE LOWER(email) = LOWER(@email) + `); + + console.log(` ✅ Mis à jour : ${email}`); + } else { + // INSERT + const insertRequest = new sql.Request(transaction); + insertRequest.input('collaborateurADId', sql.NVarChar, entraUserId); + insertRequest.input('prenom', sql.NVarChar, req.body.givenName || ''); + insertRequest.input('nom', sql.NVarChar, req.body.surname || ''); + insertRequest.input('email', sql.NVarChar, email); + insertRequest.input('departement', sql.NVarChar, req.body.department || ''); + insertRequest.input('fonction', sql.NVarChar, req.body.jobTitle || ''); + insertRequest.input('campus', sql.NVarChar, req.body.officeLocation || ''); + insertRequest.input('dateCreation', sql.DateTime, new Date()); + insertRequest.input('dateMaj', sql.DateTime, new Date()); + + await insertRequest.query(` + INSERT INTO CollaborateurAD + (CollaborateurADId, prenom, nom, email, departement, fonction, campus, service, societe, actif, dateCreation, dateMiseAJour) + VALUES (@collaborateurADId, @prenom, @nom, @email, @departement, @fonction, @campus, NULL, NULL, 1, @dateCreation, @dateMaj) + `); + + console.log(` ✅ Créé : ${email}`); + } + + // Récupérer données + const getUserRequest = new sql.Request(transaction); + getUserRequest.input('email', sql.NVarChar, email); + + const userData = await getUserRequest.query(` + SELECT id as localUserId, email, prenom, nom, fonction, departement + FROM CollaborateurAD + WHERE LOWER(email) = LOWER(@email) + `); + + await transaction.commit(); + + if (userData.recordset.length === 0) { + throw new Error('Utilisateur introuvable après sync'); + } + + res.json({ + success: true, + message: 'Sync réussie', + localUserId: userData.recordset[0].localUserId, + user: userData.recordset[0] + }); + + } catch (error) { + await transaction.rollback(); + throw error; + } + + } catch (error) { + console.error('❌ Erreur sync:', error); + res.json({ + success: false, + message: error.message + }); + } +}); + +// Route check groups +app.post('/api/check-user-groups', async (req, res) => { + try { + const { userPrincipalName } = req.body; + + if (!userPrincipalName) { + return res.json({ authorized: false, message: 'Email manquant' }); + } + + const users = await pool.query( + 'SELECT id, email, prenom, nom, actif FROM CollaborateurAD WHERE email = ?', + [userPrincipalName] + ); if (users.length > 0) { - // UPDATE - userId = users[0].id; - userRole = users[0].role; - await conn.query('UPDATE CollaborateurAD SET entraUserId = ?, DerniereConnexion = NOW() WHERE id = ?', [entraId, userId]); - console.log('✅ User mis à jour:', userId); - } else { - // INSERT - const [resInsert] = await conn.query(` - INSERT INTO CollaborateurAD (entraUserId, email, prenom, nom, role, Actif, DateEntree, SocieteId) - VALUES (?, ?, ?, ?, 'Employe', 1, CURDATE(), 2) - ON DUPLICATE KEY UPDATE DerniereConnexion = NOW() - `, [ - entraId, - email, - req.body.givenName || '', - req.body.surname || '' - ]); + const user = users[0]; - if (resInsert.insertId === 0) { - const [u] = await conn.query('SELECT id, role FROM CollaborateurAD WHERE email = ?', [email]); - userId = u[0].id; - userRole = u[0].role; - } else { - userId = resInsert.insertId; - userRole = 'Employe'; + if (user.actif === 0) { + return res.json({ authorized: false, message: 'Compte désactivé' }); } - console.log('✅ User créé/récupéré:', userId); + + return res.json({ + authorized: true, + localUserId: user.id, + user: user + }); + } + + res.json({ + authorized: true, + message: 'Sera créé au login' + }); + + } catch (error) { + console.error('❌ Erreur check:', error); + res.json({ authorized: false, error: error.message }); + } +}); + +// Route sync complète manuelle +app.post('/api/sync-all', async (req, res) => { + try { + console.log('🚀 Sync complète manuelle...'); + const results = await + IdUsers(); + + res.json({ + success: true, + message: 'Sync terminée', + stats: results + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +// Route diagnostic +app.get('/api/diagnostic-sync', async (req, res) => { + try { + const totalDB = await pool.query( + 'SELECT COUNT(*) as total, SUM(CASE WHEN actif = 1 THEN 1 ELSE 0 END) as actifs FROM CollaborateurAD', + [] + ); + + const sansEmail = await pool.query( + 'SELECT COUNT(*) as total FROM CollaborateurAD WHERE email IS NULL OR email = \'\'', + [] + ); + + const derniers = await pool.query( + 'SELECT TOP 10 id, prenom, nom, email, CollaborateurADId, actif FROM CollaborateurAD ORDER BY id DESC', + [] + ); + + // Test Entra + let entraStatus = { connected: false }; + try { + const token = await getGraphToken(); + if (token) { + const groupResponse = await axios.get( + `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, + { headers: { Authorization: `Bearer ${token}` } } + ); + entraStatus = { + connected: true, + groupName: groupResponse.data.displayName + }; + } + } catch (err) { + entraStatus.error = err.message; } res.json({ success: true, - localUserId: userId, - role: userRole + database: { + total: totalDB[0]?.total || 0, + actifs: totalDB[0]?.actifs || 0, + sansEmail: sansEmail[0]?.total || 0 + }, + entraId: entraStatus, + derniers_utilisateurs: derniers }); - } catch (error) { - console.error('❌ CRASH initial-sync:', error); - res.json({ - success: true, - localUserId: 1, - role: 'Secours' - }); - } finally { - if (conn) conn.release(); - } -}); -// ✅ AJOUTER LA ROUTE MANQUANTE check-user-groups -app.post('/api/check-user-groups', async (req, res) => { - try { - // Pour l'instant, autoriser tout le monde - res.json({ - authorized: true, - groups: [] - }); } catch (error) { - console.error('❌ Erreur check-user-groups:', error); res.status(500).json({ - authorized: false, + success: false, error: error.message }); } }); -app.listen(PORT, () => { - console.log(`✅ ✅ ✅ SERVEUR TEST DÉMARRÉ SUR LE PORT ${PORT} ✅ ✅ ✅`); -}); \ No newline at end of file +// ======================================== +// 🚀 DÉMARRAGE +// ======================================== +app.listen(PORT, "0.0.0.0", async () => { + console.log("✅ =========================================="); + console.log(" SERVEUR TEST DÉMARRÉ"); + console.log(" Port:", PORT); + console.log(` Base SQL Server: ${dbConfig.database}@${dbConfig.server}`); + console.log("=========================================="); + + // Sync auto après 5 secondes + setTimeout(async () => { + console.log("\n🚀 Sync Entra ID automatique..."); + await syncEntraIdUsers(); + }, 5000); +}); diff --git a/project/public/Backend/server.js b/project/public/Backend/server.js index a0ebb1d..e6d2e37 100644 --- a/project/public/Backend/server.js +++ b/project/public/Backend/server.js @@ -1,5 +1,13 @@ -import express from 'express'; -import mysql from 'mysql2/promise'; +// ============================================================================ +// 🚀 GTA - GESTION DES TEMPS ET ABSENCES +// ============================================================================ +// Serveur Backend Node.js avec SQL Server +// Port: 3004 +// Base de données: GTA (SQL Server) +// ============================================================================ + +import express from 'express'; +import sql from 'mssql'; import cors from 'cors'; import axios from 'axios'; import multer from 'multer'; @@ -17,7 +25,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); -const PORT = 3000; +const PORT = 3004; const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY); const sseClientsCollab = new Set(); @@ -33,7 +41,7 @@ process.on('unhandledRejection', (reason, promise) => { }); app.use(cors({ - origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta.ensup-adm.net'], + origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta-dev.ensup-adm.net'], credentials: true })); @@ -41,14 +49,24 @@ app.use(cors({ app.use(express.json()); app.use(express.urlencoded({ extended: true })); - const dbConfig = { - host: '192.168.0.4', - user: 'wpuser', - password: '-2b/)ru5/Bi8P[7_', - database: 'DemandeConge', - port:'3306', - charset: 'utf8mb4' + server: '192.168.0.3', // ⭐ 'server' au lieu de 'host' + user: 'gta_app', + password: 'GTA2025!Secure', + database: 'GTA', + port: 1433, // ⭐ Nombre, pas string + options: { + encrypt: true, // ⭐ Pas de SSL en réseau local + trustServerCertificate: true, + enableArithAbort: true, + connectTimeout: 60000, + requestTimeout: 60000 + }, + pool: { + max: 10, + min: 0, + idleTimeoutMillis: 30000 + } }; @@ -58,9 +76,6 @@ function nowFR() { return d.toISOString().slice(0, 19).replace('T', ' '); } -// ======================================== -// HELPER POUR DATES SANS CONVERSION UTC -// ======================================== function formatDateWithoutUTC(date) { if (!date) return null; const d = new Date(date); @@ -88,7 +103,6 @@ function formatDateToFrenchTime(date) { hour12: false }); } - // Ou plus simple, pour avoir un format ISO compatible avec le frontend function formatDateToFrenchISO(date) { if (!date) return null; @@ -100,22 +114,15 @@ function formatDateToFrenchISO(date) { return frenchDate.toISOString(); } - -// ======================================== -// FONCTIONS POUR GÉRER LES ARRÊTÉS COMPTABLES -// À ajouter après : const pool = mysql.createPool(dbConfig); -// ======================================== - /** * Récupère le dernier arrêté validé/clôturé */ async function getDernierArrete(conn) { const [arretes] = await conn.query(` - SELECT * FROM ArreteComptable - WHERE Statut IN ('Validé', 'Clôturé') - ORDER BY DateArrete DESC - LIMIT 1 - `); + SELECT TOP 1 * FROM ArreteComptable + WHERE Statut IN ('Validé', 'Clôturé') + ORDER BY DateArrete DESC +`); return arretes.length > 0 ? arretes[0] : null; } @@ -135,10 +142,6 @@ async function estAvantArrete(conn, date) { return dateTest <= dateArrete; } - -/** - * Récupère le solde figé d'un collaborateur pour un type de congé - */ async function getSoldeFige(conn, collaborateurId, typeCongeId, annee) { const dernierArrete = await getDernierArrete(conn); @@ -147,30 +150,25 @@ async function getSoldeFige(conn, collaborateurId, typeCongeId, annee) { } const [soldes] = await conn.query(` - SELECT * FROM SoldesFiges - WHERE ArreteId = ? - AND CollaborateurADId = ? - AND TypeCongeId = ? - AND Annee = ? - LIMIT 1 - `, [dernierArrete.Id, collaborateurId, typeCongeId, annee]); + SELECT TOP 1 * FROM SoldesFiges + WHERE ArreteId = ? + AND CollaborateurADId = ? + AND TypeCongeId = ? + AND Annee = ? +`, [dernierArrete.Id, collaborateurId, typeCongeId, annee]); return soldes.length > 0 ? soldes[0] : null; } -/** - * Calcule l'acquisition depuis le dernier arrêté - */ async function calculerAcquisitionDepuisArrete(conn, collaborateurId, typeConge, dateReference = new Date()) { const dernierArrete = await getDernierArrete(conn); const anneeRef = dateReference.getFullYear(); // Déterminer le type de congé const [typeRow] = await conn.query( - 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', + 'SELECT TOP 1 Id FROM TypeConge WHERE Nom = ?', [typeConge === 'CP' ? 'Congé payé' : 'RTT'] ); - if (typeRow.length === 0) { throw new Error(`Type de congé ${typeConge} non trouvé`); } @@ -226,7 +224,251 @@ async function calculerAcquisitionDepuisArrete(conn, collaborateurId, typeConge, return acquisFigee + acquisDepuisArrete; } -const pool = mysql.createPool(dbConfig); +const pool = new sql.ConnectionPool(dbConfig); + + + +// ⭐ CONNEXION AU DÉMARRAGE +pool.connect() + .then(() => { + console.log('✅ =========================================='); + console.log(' CONNECTÉ À SQL SERVER'); + console.log(` Base: ${dbConfig.database}@${dbConfig.host}`); + console.log('=========================================='); + }) + .catch(err => { + console.error('❌ =========================================='); + console.error(' ERREUR CONNEXION SQL SERVER'); + console.error(' Message:', err.message); + console.error('=========================================='); + }); + + +// ======================================== +// 🔧 MONKEY-PATCH MSSQL POUR SUPPORTER "?" +// ======================================== +// MONKEY-PATCH MSSQL POUR SUPPORTER ? ET LIMIT +const originalRequest = sql.Request; + +sql.Request = function (...args) { + const request = new originalRequest(...args); + const originalQuery = request.query.bind(request); + + request.query = async function (queryText, ...queryArgs) { + let convertedQuery = queryText; // 🔥 AJOUTÉ ICI + + try { + if (!queryArgs || queryArgs.length === 0 || !Array.isArray(queryArgs[0])) { + return await originalQuery(queryText); + } + + const params = queryArgs[0]; + + params.forEach((value, index) => { + request.input(`param${index}`, value); + }); + + let paramIndex = 0; + convertedQuery = convertedQuery.replace(/\?/g, () => `@param${paramIndex++}`); + + // CONVERSION GETDATE() → GETDATE() + convertedQuery = convertedQuery.replace(/NOW\(\)/gi, 'GETDATE()'); + + // CONVERSION LEAST() → CASE WHEN + while (convertedQuery.match(/LEAST\s*\(/i)) { + convertedQuery = convertedQuery.replace( + /LEAST\s*\(\s*([^,]+?)\s*,\s*([^)]+?)\s*\)/i, + '(CASE WHEN $1 < $2 THEN $1 ELSE $2 END)' + ); + } + + // LIMIT → TOP conversion + convertedQuery = convertedQuery.replace( + /LIMIT\s+(\d+)\s+OFFSET\s+(\d+)/gi, + 'OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY' + ); + + const simpleLimitMatch = convertedQuery.match(/LIMIT\s+(\d+)(?!\s+OFFSET)/i); + if (simpleLimitMatch) { + const limitValue = simpleLimitMatch[1]; + convertedQuery = convertedQuery.replace(/LIMIT\s+\d+(?!\s+OFFSET)/i, ''); + convertedQuery = convertedQuery.replace( + /SELECT(\s+DISTINCT)?/i, + `SELECT$1 TOP ${limitValue}` + ); + } + + return await originalQuery(convertedQuery); + } catch (error) { + console.error('❌ Erreur query SQL:', error.message); + console.error('Query originale:', queryText.substring(0, 300)); + console.error('Query convertie:', convertedQuery?.substring(0, 300)); + throw error; + } + }; + + return request; +}; + +console.log('✅ Driver mssql patché: support des ?, LIMIT, GETDATE() et LEAST() activé'); + +// ======================================== +// ⭐ WRAPPER POUR COMPATIBILITÉ MYSQL +// ======================================== + +/** + * Simule pool.getConnection() de MySQL + * Retourne un objet avec query(), beginTransaction(), commit(), rollback(), release() + */ +// ⭐ WRAPPER POUR COMPATIBILITÉ MYSQL +// ⭐ WRAPPER POUR COMPATIBILITÉ MYSQL +pool.getConnection = async function () { + if (!pool.connected) { + await pool.connect(); + } + + let transaction = null; + + return { + query: async function (queryText, params = []) { + // ⭐ FIX: Déclarer parameterizedQuery EN DEHORS du try block + let parameterizedQuery = queryText; + + try { + const request = transaction ? new sql.Request(transaction) : pool.request(); + + // Ajouter les paramètres (@param0, @param1, ...) + params.forEach((value, index) => { + request.input(`param${index}`, value); + }); + + // Remplacer ? par @param0, @param1, etc. + let paramIndex = 0; + parameterizedQuery = parameterizedQuery.replace(/\?/g, () => `@param${paramIndex++}`); + + // ⭐⭐⭐ CONVERSION LIMIT → TOP (VERSION CORRIGÉE) ⭐⭐⭐ + // 1. Gérer LIMIT avec OFFSET + parameterizedQuery = parameterizedQuery.replace( + /LIMIT\s+(\d+)\s+OFFSET\s+(\d+)/gi, + 'OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY' + ); + + // 2. Marquer tous les LIMIT (même sans espace avant) + parameterizedQuery = parameterizedQuery.replace( + /\s*LIMIT\s+(\d+)(?!\s+OFFSET)/gi, + ' __LIMIT__$1__' + ); + + // 3. Injecter TOP après SELECT + let limitValue = null; + const limitMatch = parameterizedQuery.match(/__LIMIT__(\d+)__/); + if (limitMatch) { + limitValue = limitMatch[1]; + parameterizedQuery = parameterizedQuery.replace(/__LIMIT__\d+__/g, ''); + } + + if (limitValue) { + parameterizedQuery = parameterizedQuery.replace( + /(SELECT\s+(?:DISTINCT\s+)?)/i, + `$1TOP ${limitValue} ` + ); + } + + // ⭐ FIX: Convertir TRUE/FALSE en 1/0 pour SQL Server + parameterizedQuery = parameterizedQuery.replace(/\bTRUE\b/gi, '1'); + parameterizedQuery = parameterizedQuery.replace(/\bFALSE\b/gi, '0'); + + const result = await request.query(parameterizedQuery); + return [result.recordset || []]; + + } catch (error) { + console.error('❌ Erreur query SQL:', error.message); + console.error('Query originale:', queryText); + console.error('Query convertie:', parameterizedQuery?.substring(0, 500)); + throw error; + } + }, + + beginTransaction: async function () { + transaction = new sql.Transaction(pool); + await transaction.begin(); + }, + + commit: async function () { + if (transaction) { + await transaction.commit(); + transaction = null; + } + }, + + rollback: async function () { + if (transaction) { + await transaction.rollback(); + transaction = null; + } + }, + + release: function () { + console.log('🔄 Connection released (no-op avec mssql)'); + } + }; +}; + +// ⭐ pool.query() direct (sans transaction) +// ⭐ pool.query() direct (sans transaction) +pool.query = async function (queryText, params = []) { + if (!pool.connected) { + await pool.connect(); + } + + // ⭐ FIX: Déclarer parameterizedQuery EN DEHORS du try/catch implicite + let parameterizedQuery = queryText; + + const request = pool.request(); + + params.forEach((value, index) => { + request.input(`param${index}`, value); + }); + + let paramIndex = 0; + parameterizedQuery = parameterizedQuery.replace(/\?/g, () => `@param${paramIndex++}`); + + // ⭐⭐⭐ CONVERSION LIMIT → TOP (VERSION CORRIGÉE) ⭐⭐⭐ + // 1. Gérer LIMIT avec OFFSET + parameterizedQuery = parameterizedQuery.replace( + /LIMIT\s+(\d+)\s+OFFSET\s+(\d+)/gi, + 'OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY' + ); + + // 2. Marquer tous les LIMIT (même sans espace avant) + parameterizedQuery = parameterizedQuery.replace( + /\s*LIMIT\s+(\d+)(?!\s+OFFSET)/gi, + ' __LIMIT__$1__' + ); + + // 3. Injecter TOP après SELECT + let limitValue = null; + const limitMatch = parameterizedQuery.match(/__LIMIT__(\d+)__/); + if (limitMatch) { + limitValue = limitMatch[1]; + parameterizedQuery = parameterizedQuery.replace(/__LIMIT__\d+__/g, ''); + } + + if (limitValue) { + parameterizedQuery = parameterizedQuery.replace( + /(SELECT\s+(?:DISTINCT\s+)?)/i, + `$1TOP ${limitValue} ` + ); + } + + // ⭐ FIX: Convertir TRUE/FALSE en 1/0 pour SQL Server + parameterizedQuery = parameterizedQuery.replace(/\bTRUE\b/gi, '1'); + parameterizedQuery = parameterizedQuery.replace(/\bFALSE\b/gi, '0'); + + const result = await request.query(parameterizedQuery); + return [result.recordset || []]; +}; + const AZURE_CONFIG = { tenantId: '9840a2a0-6ae1-4688-b03d-d2ec291be0f9', @@ -238,7 +480,7 @@ const AZURE_CONFIG = { const storage = multer.diskStorage({ destination: './uploads/', filename: (req, file, cb) => { - cb(null, Date.now() + path.extname(file.originalname)); + cb(null, Date.GETDATE() + path.extname(file.originalname)); } }); const upload = multer({ storage }); @@ -246,7 +488,7 @@ const upload = multer({ storage }); const medicalStorage = multer.diskStorage({ destination: './uploads/medical/', filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const uniqueSuffix = Date.GETDATE() + '-' + Math.round(Math.random() * 1E9); cb(null, 'medical-' + uniqueSuffix + path.extname(file.originalname)); } }); @@ -517,72 +759,17 @@ app.post('/api/webhook/receive', async (req, res) => { // Traiter selon le type d'événement switch (event) { case EVENTS.COMPTEUR_UPDATED: - console.log('📊 WEBHOOK COMPTEUR_UPDATED REÇ'); + console.log('📊 WEBHOOK COMPTEUR_UPDATED REÇU'); console.log('Collaborateur:', data.collaborateurId); console.log('Type mise à jour:', data.typeUpdate); console.log('Type congé:', data.typeConge); console.log('Année:', data.annee); + console.log('Nouveau Total:', data.nouveauTotal + 'j'); + console.log('Nouveau Solde:', data.nouveauSolde + 'j'); console.log('Source:', data.source); - // SI MODIFICATION RH OU RECALCUL, METTRE À JOUR LA BASE LOCALE - if ((data.source === 'rh' || data.source === 'recalcul') && - data.nouveauTotal !== undefined && - data.nouveauSolde !== undefined) { - - console.log('🔄 Synchronisation depuis RH (source:', data.source + ')...'); - console.log('Nouveau Total:', data.nouveauTotal + 'j'); - console.log('Nouveau Solde:', data.nouveauSolde + 'j'); - - const conn = await pool.getConnection(); - try { - // Identifier le type de congé - const typeName = data.typeConge === 'Congé payé' ? 'Congé payé' : - data.typeConge === 'RTT' ? 'RTT' : data.typeConge; - - const [typeRow] = await conn.query( - 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', - [typeName] - ); - - if (typeRow.length > 0) { - const typeCongeId = typeRow[0].Id; - - // Vérifier si le compteur existe - const [existing] = await conn.query( - `SELECT Id FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [data.collaborateurId, typeCongeId, data.annee] - ); - - if (existing.length > 0) { - // Mettre à jour - await conn.query( - `UPDATE CompteurConges - SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() - WHERE Id = ?`, - [data.nouveauTotal, data.nouveauSolde, existing[0].Id] - ); - console.log('✅ Compteur local mis à jour'); - } else { - // Créer - await conn.query( - `INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW())`, - [data.collaborateurId, typeCongeId, data.annee, data.nouveauTotal, data.nouveauSolde] - ); - console.log('✅ Compteur local créé'); - } - } - - conn.release(); - } catch (dbError) { - console.error('❌ Erreur mise à jour locale:', dbError.message); - if (conn) conn.release(); - } - } - - // NOTIFIER LE CLIENT SSE DU COLLABORATEUR + // ✅ PAS D'UPDATE EN BASE (car même DB partagée) + // ✅ UNIQUEMENT NOTIFICATION SSE pour rafraîchir l'interface notifyCollabClients({ type: 'compteur-updated', collaborateurId: data.collaborateurId, @@ -591,14 +778,13 @@ app.post('/api/webhook/receive', async (req, res) => { typeUpdate: data.typeUpdate, nouveauTotal: data.nouveauTotal, nouveauSolde: data.nouveauSolde, - source: data.source, // Garder la source originale + source: data.source, timestamp: new Date().toISOString() }, data.collaborateurId); console.log('✅ Notification SSE envoyée au collaborateur', data.collaborateurId); break; - case EVENTS.DEMANDE_VALIDATED: console.log('\n✅ === WEBHOOK DEMANDE_VALIDATED REÇU ==='); console.log(` Demande: ${data.demandeId}`); @@ -673,132 +859,8 @@ app.post('/api/webhook/receive', async (req, res) => { } }); -app.post('/api/syncCompteursFromRH', async (req, res) => { - try { - const { user_id } = req.body; - if (!user_id) { - return res.json({ success: false, message: 'user_id manquant' }); - } - console.log('\n🔄 === SYNCHRONISATION MANUELLE DEPUIS RH ==='); - console.log('User ID:', user_id); - - // Récupérer les compteurs depuis le serveur RH - const rhUrl = process.env.RH_SERVER_URL || 'http://localhost:3001'; - - try { - const response = await fetch(`${rhUrl}/api/compteurs?user_id=${user_id}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (!response.ok) { - throw new Error(`Erreur serveur RH: ${response.status}`); - } - - const rhCompteurs = await response.json(); - - console.log('📊 Compteurs RH récupérés:', rhCompteurs.length); - - // Mettre à jour la base locale - const conn = await pool.getConnection(); - await conn.beginTransaction(); - - let updated = 0; - let created = 0; - - for (const compteur of rhCompteurs) { - // Identifier le type de congé - const [typeRow] = await conn.query( - 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', - [compteur.typeConge] - ); - - if (typeRow.length === 0) { - console.warn(`⚠️ Type ${compteur.typeConge} non trouvé`); - continue; - } - - const typeCongeId = typeRow[0].Id; - - // Vérifier si existe - const [existing] = await conn.query(` - SELECT Id FROM CompteurConges - WHERE CollaborateurADId = ? - AND TypeCongeId = ? - AND Annee = ? - `, [compteur.collaborateurId, typeCongeId, compteur.annee]); - - if (existing.length > 0) { - // Mettre à jour - await conn.query(` - UPDATE CompteurConges - SET Total = ?, - Solde = ?, - SoldeReporte = ?, - DerniereMiseAJour = NOW() - WHERE Id = ? - `, [ - compteur.total, - compteur.solde, - compteur.soldeReporte || 0, - existing[0].Id - ]); - updated++; - } else { - // Créer - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, ?, NOW()) - `, [ - compteur.collaborateurId, - typeCongeId, - compteur.annee, - compteur.total, - compteur.solde, - compteur.soldeReporte || 0 - ]); - created++; - } - } - - await conn.commit(); - conn.release(); - - console.log(`✅ Synchronisation terminée: ${updated} mis à jour, ${created} créés`); - - res.json({ - success: true, - message: 'Synchronisation réussie', - stats: { - total: rhCompteurs.length, - updated: updated, - created: created - } - }); - - } catch (fetchError) { - console.error('❌ Erreur communication avec serveur RH:', fetchError.message); - res.status(500).json({ - success: false, - message: 'Impossible de contacter le serveur RH', - error: fetchError.message - }); - } - - } catch (error) { - console.error('❌ Erreur synchronisation:', error); - res.status(500).json({ - success: false, - message: 'Erreur serveur', - error: error.message - }); - } -}); function getDateFinMoisPrecedent(referenceDate = new Date()) { const now = new Date(referenceDate); now.setHours(0, 0, 0, 0); @@ -1082,67 +1144,360 @@ function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) { // 4️⃣ Plafonner à 25 jours return Math.min(acquisition, 25); } +// ======================================== +// CALCUL CP INTELLIGENT (MODE AUTO) +// ======================================== +/** + * Calcule l'acquisition CP avec détection automatique du mode + */ +function calculerAcquisitionCP_Smart(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️⃣ 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️⃣ Obtenir le mode de calcul + const modeInfo = getModeCalcul('CP', dateEntree); + const dateDebutAcquis = modeInfo.dateDebut; + + console.log(` 📅 ${modeInfo.description}`); + + // 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); +} + +// ======================================== +// CALCUL RTT INTELLIGENT (MODE AUTO) +// ======================================== + +/** + * Calcule l'acquisition RTT avec détection automatique du mode + */ +async function calculerAcquisitionRTT_Smart(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, + mode: 'APPRENTI' + }; + } + + // 3️⃣ Récupérer la configuration RTT (avec règles 2025/2026) + const config = await getConfigurationRTT(conn, annee, typeContrat); + + // 4️⃣ Obtenir le mode de calcul + const modeInfo = getModeCalcul('RTT', dateEntree); + const dateDebutAcquis = modeInfo.dateDebut; + + console.log(` 📅 ${modeInfo.description}`); + + // 5️⃣ Calculer avec la formule Excel + const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, config.acquisitionMensuelle); + + // 6️⃣ Calculer les mois travaillés (pour info) + 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, + mode: modeInfo.mode + }; +} +// ======================================== +// FONCTION DE DÉTECTION AUTOMATIQUE DU MODE +// ======================================== + +/** + * Détermine automatiquement si on doit utiliser le mode transition + * en comparant la date actuelle avec le début de l'exercice + */ +function isInExerciceActuel(typeConge) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const currentYear = today.getFullYear(); + const currentMonth = today.getMonth() + 1; // 1-12 + + if (typeConge === 'CP') { + // Exercice CP : 01/06/N → 31/05/N+1 + + // Déterminer le début de l'exercice ACTUEL + let debutExercice; + if (currentMonth >= 6) { + // On est entre juin et décembre → exercice commence le 01/06 de cette année + debutExercice = new Date(currentYear, 5, 1); // 01/06/N + } else { + // On est entre janvier et mai → exercice a commencé le 01/06 de l'année dernière + debutExercice = new Date(currentYear - 1, 5, 1); // 01/06/N-1 + } + + // ⭐ Si aujourd'hui est le PREMIER JOUR du nouvel exercice ou après → MODE NORMAL + // ⭐ Si on est encore dans l'exercice commencé avant → MODE TRANSITION + + const finExercice = new Date(debutExercice); + finExercice.setFullYear(finExercice.getFullYear() + 1); + finExercice.setMonth(4, 31); // 31/05/N+1 + + // Si on vient de passer le 01/06, c'est le nouvel exercice → mode NORMAL + const nouveauExercice = new Date(currentYear, 5, 1); + return today < nouveauExercice; // TRUE = mode transition (avant le 01/06) + + } else if (typeConge === 'RTT') { + // Année RTT : 01/01/N → 31/12/N + + const debutAnnee = new Date(currentYear, 0, 1); // 01/01/N + + // Si on vient de passer le 01/01, c'est la nouvelle année → mode NORMAL + return today < debutAnnee; // TRUE = mode transition (avant le 01/01) + } + + return false; +} + +/** + * Version améliorée : retourne le mode ET la date de début à utiliser + */ +/** + * Détermine le mode de calcul et la date de début selon le contexte + * @param {string} typeConge - 'CP' ou 'RTT' + * @param {Date|null} dateEntree - Date d'entrée du collaborateur + * @returns {Object} { mode, dateDebut, description } + */ +function getModeCalcul(typeConge, dateEntree = null) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const currentYear = today.getFullYear(); + const currentMonth = today.getMonth() + 1; + + if (typeConge === 'CP') { + // Déterminer le début de l'exercice ACTUEL + let debutExerciceActuel; + if (currentMonth >= 6) { + debutExerciceActuel = new Date(currentYear, 5, 1); // 01/06/N + } else { + debutExerciceActuel = new Date(currentYear - 1, 5, 1); // 01/06/N-1 + } + debutExerciceActuel.setHours(0, 0, 0, 0); + + // ⭐ RÈGLE DE BASCULE : + // On bascule en mode NORMAL uniquement si : + // 1. On est dans un NOUVEL exercice (après le prochain 01/06) + // 2. ET la personne est arrivée APRÈS le début de ce nouvel exercice + + // Calculer le début du PROCHAIN exercice + const prochainExercice = new Date(currentYear, 5, 1); // 01/06/N + if (currentMonth < 6) { + // Si on est avant juin, le prochain exercice est cette année + prochainExercice.setFullYear(currentYear); + } else { + // Si on est après juin, le prochain exercice est l'année prochaine + prochainExercice.setFullYear(currentYear + 1); + } + + // ⭐ BASCULE : on passe en mode NORMAL seulement si aujourd'hui >= prochain exercice + if (today >= prochainExercice && dateEntree) { + const entree = new Date(dateEntree); + entree.setHours(0, 0, 0, 0); + + // Si la personne est arrivée APRÈS le début du nouvel exercice + if (entree >= prochainExercice) { + return { + mode: 'NORMAL', + dateDebut: entree, + description: `CP avec DateEntree (${entree.toLocaleDateString('fr-FR')})` + }; + } + } + + // ⭐ MODE TRANSITION : calcul depuis début de l'exercice actuel (SANS DateEntree) + return { + mode: 'TRANSITION', + dateDebut: debutExerciceActuel, + description: `CP mode transition (depuis ${debutExerciceActuel.toLocaleDateString('fr-FR')})` + }; + + } else if (typeConge === 'RTT') { + const debutAnneeActuelle = new Date(currentYear, 0, 1); // 01/01/N + debutAnneeActuelle.setHours(0, 0, 0, 0); + + // ⭐ RÈGLE DE BASCULE : + // On bascule en mode NORMAL uniquement si : + // 1. On est dans une NOUVELLE année (après le prochain 01/01) + // 2. ET la personne est arrivée APRÈS le début de cette nouvelle année + + // Calculer le début de la PROCHAINE année + const prochaineAnnee = new Date(currentYear + 1, 0, 1); // 01/01/N+1 + + // ⭐ BASCULE : on passe en mode NORMAL seulement si aujourd'hui >= prochaine année + if (today >= prochaineAnnee && dateEntree) { + const entree = new Date(dateEntree); + entree.setHours(0, 0, 0, 0); + + // Si la personne est arrivée APRÈS le début de la nouvelle année + if (entree >= prochaineAnnee) { + return { + mode: 'NORMAL', + dateDebut: entree, + description: `RTT avec DateEntree (${entree.toLocaleDateString('fr-FR')})` + }; + } + } + + // ⭐ MODE TRANSITION : calcul depuis début de l'année actuelle (SANS DateEntree) + return { + mode: 'TRANSITION', + dateDebut: debutAnneeActuelle, + description: `RTT mode transition (depuis ${debutAnneeActuelle.toLocaleDateString('fr-FR')})` + }; + } + + return null; +} // ======================================== // TÂCHES CRON // ======================================== -cron.schedule('0 2 * * *', async () => { - console.log('🔄 [CRON] Mise à jour quotidienne des compteurs...'); +cron.schedule('1 0 1 1 *', async () => { + console.log('\n🎉 ===== RÉINITIALISATION RTT - 1ER JANVIER ====='); + + const conn = await pool.getConnection(); + try { - const conn = await pool.getConnection(); await conn.beginTransaction(); + const today = new Date(); + const nouvelleAnnee = today.getFullYear(); + const ancienneAnnee = nouvelleAnnee - 1; + + console.log(` 📅 Passage de ${ancienneAnnee} à ${nouvelleAnnee}`); + + const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ?', ['RTT']); + + if (rttType.length === 0) { + throw new Error('Type de congé RTT introuvable'); + } + + const rttTypeId = rttType[0].Id; + const [collaborateurs] = await conn.query(` - SELECT id, prenom, nom, CampusId - FROM CollaborateurAD - WHERE (actif = 1 OR actif IS NULL) + SELECT id, prenom, nom, TypeContrat, role + FROM CollaborateurAD + WHERE (Actif = 1 OR Actif IS NULL) + AND (role IS NULL OR role != 'Apprenti') `); - let successCount = 0; - const today = new Date(); + console.log(` 👥 ${collaborateurs.length} collaborateurs à traiter`); + + let compteursReinitialises = 0; for (const collab of collaborateurs) { - try { - await updateMonthlyCounters(conn, collab.id, today); - successCount++; - } catch (error) { - console.error(`❌ Erreur pour ${collab.prenom} ${collab.nom}:`, error.message); + const collaborateurId = collab.id; + + const [ancienCompteur] = await conn.query(` + SELECT Total, Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, ancienneAnnee]); + + if (ancienCompteur.length > 0) { + const soldeAncien = parseFloat(ancienCompteur[0].Solde || 0); + console.log(` 👤 ${collab.prenom} ${collab.nom}: ${soldeAncien.toFixed(2)}j RTT perdus`); + + await conn.query(` + UPDATE CompteurConges + SET DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, ancienneAnnee]); } + + const [nouveauCompteur] = await conn.query(` + SELECT id FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, nouvelleAnnee]); + + if (nouveauCompteur.length === 0) { + await conn.query(` + INSERT INTO CompteurConges ( + CollaborateurADId, TypeCongeId, Annee, + Total, Solde, SoldeReporte, DerniereMiseAJour + ) + VALUES (?, ?, ?, 0, 0, 0, GETDATE()) + `, [collaborateurId, rttTypeId, nouvelleAnnee]); + + console.log(` ✅ Compteur RTT ${nouvelleAnnee} créé à 0`); + } else { + await conn.query(` + UPDATE CompteurConges + SET Total = 0, Solde = 0, SoldeReporte = 0, DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, nouvelleAnnee]); + + console.log(` ✅ Compteur RTT ${nouvelleAnnee} réinitialisé à 0`); + } + + compteursReinitialises++; } await conn.commit(); - console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} compteurs mis à jour`); - conn.release(); + console.log(`\n✅ Réinitialisation RTT terminée : ${compteursReinitialises} compteurs`); + } catch (error) { - console.error('❌ [CRON] Erreur mise à jour quotidienne:', error); - } -}); - -cron.schedule('59 23 31 12 *', async () => { - console.log('🎆 [CRON] Traitement fin d\'année RTT...'); - try { - const conn = await pool.getConnection(); - await conn.beginTransaction(); - const [collaborateurs] = await conn.query('SELECT id, CampusId FROM CollaborateurAD'); - let successCount = 0; - for (const collab of collaborateurs) { - try { - await processEndOfYearRTT(conn, collab.id); - successCount++; - } catch (error) { - console.error(`❌ Erreur RTT pour ${collab.id}:`, error.message); - } - } - await conn.commit(); - console.log(`✅ [CRON] ${successCount}/${collaborateurs.length} RTT réinitialisés`); - conn.release(); - } catch (error) { - console.error('❌ [CRON] Erreur traitement fin d\'année:', error); + await conn.rollback(); + console.error('❌ Erreur réinitialisation RTT:', error); + } finally { + conn.release(); } +}, { + timezone: "Europe/Paris" }); +// ============================================================================ +// 📅 CRON REPORT CP - 31 mai 23h59 +// ============================================================================ cron.schedule('59 23 31 5 *', async () => { console.log('📅 [CRON] Traitement fin d\'exercice CP...'); try { @@ -1166,15 +1521,13 @@ cron.schedule('59 23 31 5 *', async () => { } }); -// ======================================== -// CRON : CRÉER ARRÊTÉS MENSUELS AUTOMATIQUEMENT -// ======================================== - +// ============================================================================ +// 📊 CRON ARRÊTÉS MENSUELS +// ============================================================================ cron.schedule('55 23 28-31 * *', async () => { const today = new Date(); const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0); - // ⭐ Vérifier qu'on est bien le dernier jour du mois if (today.getDate() === lastDay.getDate()) { console.log(`📅 [CRON] Création arrêté fin de mois: ${today.toISOString().split('T')[0]}`); @@ -1183,10 +1536,9 @@ cron.schedule('55 23 28-31 * *', async () => { await conn.beginTransaction(); const annee = today.getFullYear(); - const mois = today.getMonth() + 1; // 1-12 + const mois = today.getMonth() + 1; const dateArrete = today.toISOString().split('T')[0]; - // ⭐ Vérifier si l'arrêté n'existe pas déjà const [existing] = await conn.query( 'SELECT Id FROM ArreteComptable WHERE Annee = ? AND Mois = ?', [annee, mois] @@ -1199,11 +1551,10 @@ cron.schedule('55 23 28-31 * *', async () => { return; } - // ⭐ Créer l'arrêté const [result] = await conn.query(` INSERT INTO ArreteComptable (DateArrete, Annee, Mois, Libelle, Description, Statut, DateCreation) - VALUES (?, ?, ?, ?, ?, 'En cours', NOW()) + VALUES (?, ?, ?, ?, ?, 'En cours', GETDATE()) `, [ dateArrete, annee, @@ -1215,11 +1566,9 @@ cron.schedule('55 23 28-31 * *', async () => { const arreteId = result.insertId; console.log(`✅ [CRON] Arrêté créé: ID ${arreteId}`); - // ⭐ Créer le snapshot await conn.query('CALL sp_creer_snapshot_arrete(?)', [arreteId]); console.log(`📸 [CRON] Snapshot créé pour l'arrêté ${arreteId}`); - // ⭐ Compter les soldes figés const [count] = await conn.query( 'SELECT COUNT(*) as total FROM SoldesFiges WHERE ArreteId = ?', [arreteId] @@ -1238,7 +1587,9 @@ cron.schedule('55 23 28-31 * *', async () => { } }); -// Mail mensuel le 1er à 9h +// ============================================================================ +// 📧 CRON MAILS COMPTE-RENDU +// ============================================================================ cron.schedule('0 9 1 * *', async () => { console.log('📧 Envoi mails compte-rendu mensuel...'); const conn = await pool.getConnection(); @@ -1260,7 +1611,9 @@ cron.schedule('0 9 1 * *', async () => { conn.release(); }); -// Relance hebdomadaire le lundi à 9h +// ============================================================================ +// 🔔 CRON RELANCES +// ============================================================================ cron.schedule('0 9 * * 1', async () => { console.log('🔔 Relance hebdomadaire compte-rendu...'); const conn = await pool.getConnection(); @@ -1284,6 +1637,18 @@ cron.schedule('0 9 * * 1', async () => { conn.release(); }); +// ============================================================================ +// 🚀 RATTRAPAGE IMMÉDIAT - 5 minutes après démarrage +// ============================================================================ +setTimeout(() => { + console.log('🚀 ===== EXÉCUTION IMMÉDIATE - RATTRAPAGE DEPUIS LE 1ER DU MOIS ====='); + updateMonthlyCounters(true); // true = mode rattrapage +}, 5 * 60 * 1000); + +console.log('⏰ CRON quotidien programmé : 00h00 Europe/Paris'); +console.log('⏰ CRON réinitialisation RTT programmé : 1er janvier à 00h01'); +console.log(`⏰ CRON immédiat programmé dans 5 minutes`); + // ⭐ Fonction helper pour les noms de mois function getMonthName(mois) { @@ -1441,7 +1806,7 @@ async function processEndOfYearRTT(conn, collaborateurId) { const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); if (rttType.length === 0) return null; await conn.query( - `UPDATE CompteurConges SET Solde = 0, Total = 0, SoldeReporte = 0, DerniereMiseAJour = NOW() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + `UPDATE CompteurConges SET Solde = 0, Total = 0, SoldeReporte = 0, DerniereMiseAJour = GETDATE() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, [collaborateurId, rttType[0].Id, currentYear] ); return { type: 'RTT', action: 'reset_end_of_year', annee: currentYear }; @@ -1465,12 +1830,12 @@ async function processEndOfExerciceCP(conn, collaborateurId) { ); if (nextYearCounter.length > 0) { await conn.query( - `UPDATE CompteurConges SET SoldeReporte = ?, Solde = Solde + ?, DerniereMiseAJour = NOW() WHERE Id = ?`, + `UPDATE CompteurConges SET SoldeReporte = ?, Solde = Solde + ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, [soldeAReporter, soldeAReporter, nextYearCounter[0].Id] ); } else { await conn.query( - `INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, ?, ?, NOW())`, + `INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) VALUES (?, ?, ?, 0, ?, ?, GETDATE())`, [collaborateurId, cpTypeId, currentYear + 1, soldeAReporter, soldeAReporter] ); } @@ -1491,7 +1856,7 @@ async function deductLeaveBalance(conn, collaborateurId, typeCongeId, nombreJour const aDeduireN1 = Math.min(soldeN1, joursRestants); if (aDeduireN1 > 0) { await conn.query( - `UPDATE CompteurConges SET SoldeReporte = GREATEST(0, SoldeReporte - ?), Solde = GREATEST(0, Solde - ?) WHERE Id = ?`, + `UPDATE CompteurConges SET SoldeReporte = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END, Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ?`, [aDeduireN1, aDeduireN1, compteurN1[0].Id] ); deductions.push({ annee: previousYear, type: 'Reporté N-1', joursUtilises: aDeduireN1, soldeAvant: soldeN1 }); @@ -1508,7 +1873,7 @@ async function deductLeaveBalance(conn, collaborateurId, typeCongeId, nombreJour const aDeduireN = Math.min(soldeN, joursRestants); if (aDeduireN > 0) { await conn.query( - `UPDATE CompteurConges SET Solde = GREATEST(0, Solde - ?) WHERE Id = ?`, + `UPDATE CompteurConges SET Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ?`, [aDeduireN, compteurN[0].Id] ); deductions.push({ annee: currentYear, type: 'Année actuelle N', joursUtilises: aDeduireN, soldeAvant: soldeN }); @@ -1547,15 +1912,334 @@ async function checkLeaveBalance(conn, collaborateurId, repartition) { const insuffisants = verification.filter(v => !v.suffisant); return { valide: insuffisants.length === 0, details: verification, insuffisants }; } +// ============================================================================ +// 📅 FONCTION UTILITAIRE - Obtenir le nombre de jours du mois +// ============================================================================ +function getJoursDuMois(date) { + const annee = date.getFullYear(); + const mois = date.getMonth(); // 0-11 + // Le jour 0 du mois suivant = dernier jour du mois actuel + const dernierJour = new Date(annee, mois + 1, 0); + return dernierJour.getDate(); +} + + +// ============================================================================ +// 🔄 FONCTION DE MISE À JOUR DES COMPTEURS (CORRIGÉE - DÉCRÉMENTE) +// ============================================================================ + +async function updateMonthlyCounters(rattrapage = false) { + const conn = await pool.getConnection(); + + try { + await conn.beginTransaction(); + + const today = new Date(); + const currentYear = today.getFullYear(); + const currentMonth = today.getMonth(); + const jourActuel = today.getDate(); + + // ⭐ Obtenir le nombre de jours du mois actuel + const joursDuMois = getJoursDuMois(today); + + console.log(`\n🔄 === MISE À JOUR ${rattrapage ? 'RATTRAPAGE' : 'QUOTIDIENNE'} COMPTEURS - ${today.toLocaleDateString('fr-FR')} ===`); + console.log(` 📅 Mois actuel : ${joursDuMois} jours`); + + // ⭐ RATTRAPAGE : Calculer les jours manqués depuis le 1er du mois + let joursARattraper = 1; // Par défaut : incrément d'1 jour + + if (rattrapage) { + joursARattraper = jourActuel; // Du 1er au jour actuel + console.log(` 📅 Rattrapage depuis le 1er du mois : ${joursARattraper} jours`); + } + + // Récupérer tous les collaborateurs actifs + const [collaborateurs] = await conn.query(` + SELECT id, prenom, nom, DateEntree, TypeContrat, role + FROM CollaborateurAD + WHERE (Actif = 1 OR Actif IS NULL) + `); + + const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ?', ['Congé payé']); + const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ?', ['RTT']); + + let compteursMisAJour = 0; + + for (const collab of collaborateurs) { + const collaborateurId = collab.id; + const dateEntree = collab.DateEntree; + const typeContrat = collab.TypeContrat || '37h'; + const role = collab.role; + + console.log(`\n 👤 ${collab.prenom} ${collab.nom}`); + + // ==================================== + // 📅 CP - Incrément avec prorata mensuel + // ==================================== + if (cpType.length > 0) { + const modeCP = getModeCalcul('CP', dateEntree); + + // ⭐ Calcul acquisition mensuelle + const acquisitionMensuelleCP = 25 / 12; // 2.0833j/mois + + // ⭐ Calcul acquisition quotidienne selon le nombre de jours du mois + const incrementJournalierCP = acquisitionMensuelleCP / joursDuMois; + const incrementTotal = incrementJournalierCP * joursARattraper; + + console.log(` CP: ${incrementJournalierCP.toFixed(6)}j/jour (${acquisitionMensuelleCP.toFixed(4)}j/${joursDuMois}j)`); + + // Vérifier si compteur existe + const [compteurCP] = await conn.query(` + SELECT Total, Solde, SoldeReporte + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, cpType[0].Id, currentYear]); + + if (compteurCP.length > 0) { + const totalAvant = parseFloat(compteurCP[0].Total || 0); + const soldeAvant = parseFloat(compteurCP[0].Solde || 0); + + // ⭐ INCRÉMENTER (ne pas écraser) + await conn.query(` + UPDATE CompteurConges + SET + Total = LEAST(Total + ?, 25), + Solde = LEAST(Solde + ?, 25 + COALESCE(SoldeReporte, 0)), + DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [incrementTotal, incrementTotal, collaborateurId, cpType[0].Id, currentYear]); + + console.log(` CP: ${totalAvant.toFixed(2)}j → ${(totalAvant + incrementTotal).toFixed(2)}j (+${incrementTotal.toFixed(4)}j) [${modeCP.mode}]`); + compteursMisAJour++; + } else { + // Créer compteur initial avec Smart + const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree); + + // Générer l'ID manuellement + const compteurCPId = await getNextId(conn, 'CompteurConges'); + + await conn.query(` + INSERT INTO CompteurConges (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, ?, 0, GETDATE()) +`, [compteurCPId, collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP]); + + console.log(` ✅ CP créé avec ID ${compteurCPId}: ${acquisCP.toFixed(2)}j [${modeCP.mode}]`); + compteursMisAJour++; + } + } + + // ==================================== + // 📅 RTT - Incrément avec prorata mensuel + // ==================================== + if (rttType.length > 0 && role !== 'Apprenti') { + const modeRTT = getModeCalcul('RTT', dateEntree); + const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat); + + // ⭐ Calcul acquisition mensuelle + const acquisitionMensuelleRTT = rttConfig.joursAnnuels / 12; + + // ⭐ Calcul acquisition quotidienne selon le nombre de jours du mois + const incrementJournalierRTT = acquisitionMensuelleRTT / joursDuMois; + const incrementTotal = incrementJournalierRTT * joursARattraper; + + console.log(` RTT: ${incrementJournalierRTT.toFixed(6)}j/jour (${acquisitionMensuelleRTT.toFixed(4)}j/${joursDuMois}j)`); + + // Vérifier si compteur existe + const [compteurRTT] = await conn.query(` + SELECT Total, Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttType[0].Id, currentYear]); + + if (compteurRTT.length > 0) { + const totalAvant = parseFloat(compteurRTT[0].Total || 0); + const soldeAvant = parseFloat(compteurRTT[0].Solde || 0); + + // ⭐ INCRÉMENTER (ne pas écraser) + await conn.query(` + UPDATE CompteurConges + SET + Total = LEAST(Total + ?, ?), + Solde = LEAST(Solde + ?, ?), + DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [incrementTotal, rttConfig.joursAnnuels, incrementTotal, rttConfig.joursAnnuels, collaborateurId, rttType[0].Id, currentYear]); + + console.log(` RTT: ${totalAvant.toFixed(2)}j → ${(totalAvant + incrementTotal).toFixed(2)}j (+${incrementTotal.toFixed(4)}j) [${modeRTT.mode}]`); + compteursMisAJour++; + } else { + // Créer compteur initial avec Smart + const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, today); + + // ✅ CODE CORRIGÉ - Génération manuelle de l'ID + console.log(` 🆕 RTT créé pour ${collaborateurId}`); + + // Générer l'ID manuellement + const compteurRTTId = await getNextId(conn, 'CompteurConges'); + + await conn.query(` + INSERT INTO CompteurConges (Id, CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, GETDATE()) +`, [compteurRTTId, collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition]); + + console.log(` ✅ RTT créé avec ID ${compteurRTTId}: ${rttData.acquisition.toFixed(2)}j [${modeRTT.mode}]`); + compteursMisAJour++; + } + } + } + + await conn.commit(); + console.log(`\n✅ Mise à jour terminée : ${compteursMisAJour} compteurs pour ${collaborateurs.length} collaborateurs`); + + } catch (error) { + await conn.rollback(); + console.error('❌ Erreur mise à jour compteurs:', error); + throw error; + } finally { + conn.release(); + } +} + +// ============================================================================ +// ⏰ PLANIFICATION DES CRONS +// ============================================================================ + +// ❌ DÉSACTIVER le rattrapage immédiat (commenté) + + + +// ✅ EXÉCUTION QUOTIDIENNE à 00h01 (à partir de demain - DÉCRÉMENTE) +cron.schedule('1 0 * * *', () => { + console.log('🕐 ===== EXÉCUTION QUOTIDIENNE AUTOMATIQUE - 00h01 ====='); + updateMonthlyCounters(false); // false = incrément normal d'1 jour +}, { + timezone: "Europe/Paris" +}); + +console.log('⏰ CRON quotidien programmé : 00h01 Europe/Paris (à partir de demain)'); +console.log('⏰ CRON réinitialisation RTT programmé : 1er janvier à 00h01'); +console.log('⚠️ Rattrapage immédiat DÉSACTIVÉ'); + +// ============================================================================ +// 🎉 CRON - RÉINITIALISATION RTT AU 1ER JANVIER +// ============================================================================ +cron.schedule('1 0 1 1 *', async () => { + console.log('\n🎉 ===== RÉINITIALISATION RTT - 1ER JANVIER ====='); + + const conn = await pool.getConnection(); + + try { + await conn.beginTransaction(); + + const today = new Date(); + const nouvelleAnnee = today.getFullYear(); + const ancienneAnnee = nouvelleAnnee - 1; + + console.log(` 📅 Passage de ${ancienneAnnee} à ${nouvelleAnnee}`); + + // Récupérer le TypeCongeId pour RTT + const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ?', ['RTT']); + + if (rttType.length === 0) { + throw new Error('Type de congé RTT introuvable'); + } + + const rttTypeId = rttType[0].Id; + + // Récupérer tous les collaborateurs actifs (sauf apprentis) + const [collaborateurs] = await conn.query(` + SELECT id, prenom, nom, TypeContrat, role + FROM CollaborateurAD + WHERE (Actif = 1 OR Actif IS NULL) + AND (role IS NULL OR role != 'Apprenti') + `); + + console.log(` 👥 ${collaborateurs.length} collaborateurs à traiter`); + + let compteursReinitialises = 0; + + for (const collab of collaborateurs) { + const collaborateurId = collab.id; + + // 1️⃣ Archiver/Marquer l'ancien compteur RTT N-1 + const [ancienCompteur] = await conn.query(` + SELECT Total, Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, ancienneAnnee]); + + if (ancienCompteur.length > 0) { + const soldeAncien = parseFloat(ancienCompteur[0].Solde || 0); + + console.log(` 👤 ${collab.prenom} ${collab.nom}: ${soldeAncien.toFixed(2)}j RTT perdus`); + + // Marquer l'ancien compteur comme "clos" + await conn.query(` + UPDATE CompteurConges + SET DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, ancienneAnnee]); + } + + // 2️⃣ Créer ou réinitialiser le compteur RTT pour la nouvelle année + const [nouveauCompteur] = await conn.query(` + SELECT id FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, nouvelleAnnee]); + + if (nouveauCompteur.length === 0) { + // Créer le compteur à 0 + await conn.query(` + INSERT INTO CompteurConges ( + CollaborateurADId, + TypeCongeId, + Annee, + Total, + Solde, + SoldeReporte, + DerniereMiseAJour + ) + VALUES (?, ?, ?, 0, 0, 0, GETDATE()) + `, [collaborateurId, rttTypeId, nouvelleAnnee]); + + console.log(` ✅ Compteur RTT ${nouvelleAnnee} créé à 0`); + } else { + // Le compteur existe déjà, le remettre à 0 + await conn.query(` + UPDATE CompteurConges + SET Total = 0, Solde = 0, SoldeReporte = 0, DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, nouvelleAnnee]); + + console.log(` ✅ Compteur RTT ${nouvelleAnnee} réinitialisé à 0`); + } + + compteursReinitialises++; + } + + await conn.commit(); + console.log(`\n✅ Réinitialisation RTT terminée : ${compteursReinitialises} compteurs`); + + } catch (error) { + await conn.rollback(); + console.error('❌ Erreur réinitialisation RTT:', error); + } finally { + conn.release(); + } +}, { + timezone: "Europe/Paris" +}); + +console.log('⏰ CRON réinitialisation RTT programmé : 1er janvier à 00h01'); // ======================================== // MISE À JOUR DE updateMonthlyCounters // ======================================== -async function updateMonthlyCounters(conn, collaborateurId, dateReference = null) { - const today = dateReference ? new Date(dateReference) : getDateFinMoisPrecedent(); +async function updateMonthlyCounters_Smart(conn, collaborateurId, dateReference = null) { + const today = dateReference ? new Date(dateReference) : new Date(); const currentYear = today.getFullYear(); const updates = []; @@ -1573,21 +2257,21 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null const typeContrat = collabInfo[0].TypeContrat || '37h'; const isApprenti = collabInfo[0].role === 'Apprenti'; - console.log(`\n📊 === Mise à jour compteurs pour collaborateur ${collaborateurId} ===`); + console.log(`\n📊 === Mise à jour pour collaborateur ${collaborateurId} ===`); console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`); // ====================================== // CP (Congés Payés) // ====================================== - const exerciceCP = getExerciceCP(today); - const acquisitionCP = calculerAcquisitionCP(today, dateEntree); + const acquisitionCP = calculerAcquisitionCP_Smart(today, dateEntree); + + console.log(` CP - Acquisition: ${acquisitionCP.toFixed(2)}j`); const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); if (cpType.length > 0) { const cpTypeId = cpType[0].Id; - // 1️⃣ Récupérer le compteur existant const [existingCP] = await conn.query(` SELECT Id, Total, Solde, SoldeReporte FROM CompteurConges @@ -1599,20 +2283,14 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null const ancienSolde = parseFloat(existingCP[0].Solde || 0); const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0); - console.log(` CP - Ancien acquis: ${ancienTotal.toFixed(2)}j`); - console.log(` CP - Nouvel acquis: ${acquisitionCP.toFixed(2)}j`); - - // 2️⃣ Calculer l'incrément d'acquisition (nouveaux jours acquis ce mois) const incrementAcquis = acquisitionCP - ancienTotal; if (incrementAcquis > 0) { - console.log(` CP - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`); + console.log(` CP - Nouveaux jours: +${incrementAcquis.toFixed(2)}j`); - // 3️⃣ Vérifier si le collaborateur a de l'anticipé utilisé + // Gérer le remboursement d'anticipé (logique existante) const [anticipeUtilise] = await conn.query(` - SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe, - MIN(dd.Id) as firstDeductionId, - MIN(dd.DemandeCongeId) as demandeId + SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id WHERE dc.CollaborateurADId = ? @@ -1626,13 +2304,10 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0); if (anticipePris > 0) { - // 4️⃣ Calculer le montant à rembourser const aRembourser = Math.min(incrementAcquis, anticipePris); + console.log(` 💳 CP - Remboursement anticipé: ${aRembourser.toFixed(2)}j`); - console.log(` 💳 CP - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`); - - // 5️⃣ Rembourser l'anticipé en transférant vers "Année N" - // On récupère toutes les déductions anticipées pour ce type + // [Logique de remboursement complète - identique à avant] const [deductionsAnticipees] = await conn.query(` SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises FROM DeductionDetails dd @@ -1654,14 +2329,12 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null const joursAnticipes = parseFloat(deduction.JoursUtilises); const aDeduiteDeCetteDeduction = Math.min(resteARembourser, joursAnticipes); - // Réduire l'anticipé await conn.query(` UPDATE DeductionDetails - SET JoursUtilises = GREATEST(0, JoursUtilises - ?) + SET JoursUtilises = CASE WHEN (JoursUtilises - ?) < 0 THEN 0 ELSE (JoursUtilises - ?) END WHERE Id = ? `, [aDeduiteDeCetteDeduction, deduction.Id]); - // Vérifier si une déduction "Année N" existe déjà pour cette demande const [existingAnneeN] = await conn.query(` SELECT Id, JoursUtilises FROM DeductionDetails @@ -1672,14 +2345,12 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null `, [deduction.DemandeCongeId, cpTypeId, currentYear]); if (existingAnneeN.length > 0) { - // Augmenter la déduction "Année N" existante await conn.query(` UPDATE DeductionDetails SET JoursUtilises = JoursUtilises + ? WHERE Id = ? `, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]); } else { - // Créer une nouvelle déduction "Année N" await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) @@ -1688,8 +2359,6 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null } resteARembourser -= aDeduiteDeCetteDeduction; - - console.log(` ✅ CP - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`); } // Supprimer les déductions anticipées à zéro @@ -1703,7 +2372,7 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null } } - // 6️⃣ Recalculer le solde total (acquis + report - consommé) + // Recalculer le solde const [consomme] = await conn.query(` SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total FROM DeductionDetails dd @@ -1718,45 +2387,36 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null const totalConsomme = parseFloat(consomme[0].total || 0); const nouveauSolde = Math.max(0, acquisitionCP + soldeReporte - totalConsomme); - console.log(` CP - Consommé total: ${totalConsomme.toFixed(2)}j`); - console.log(` CP - Nouveau solde: ${nouveauSolde.toFixed(2)}j`); - - // 7️⃣ Mettre à jour le compteur await conn.query(` UPDATE CompteurConges - SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() + SET Total = ?, Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ? `, [acquisitionCP, nouveauSolde, existingCP[0].Id]); updates.push({ type: 'CP', - exercice: exerciceCP, acquisitionCumulee: acquisitionCP, increment: incrementAcquis, nouveauSolde: nouveauSolde }); } else { - // Créer le compteur s'il n'existe pas + // Créer le compteur await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW()) + VALUES (?, ?, ?, ?, ?, 0, GETDATE()) `, [collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP]); - console.log(` CP - Compteur créé: ${acquisitionCP.toFixed(2)}j`); - updates.push({ type: 'CP', - exercice: exerciceCP, - acquisitionCumulee: acquisitionCP, action: 'created', - nouveauSolde: acquisitionCP + acquisitionCumulee: acquisitionCP }); } } // ====================================== - // RTT + // RTT (identique avec Smart) // ====================================== if (!isApprenti) { const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); @@ -1833,7 +2493,7 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null // Réduire l'anticipé await conn.query(` UPDATE DeductionDetails - SET JoursUtilises = GREATEST(0, JoursUtilises - ?) + SET JoursUtilises = CASE WHEN (JoursUtilises - ?) < 0 THEN 0 ELSE (JoursUtilises - ?) END WHERE Id = ? `, [aDeduiteDeCetteDeduction, deduction.Id]); @@ -1898,7 +2558,7 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null // 7️⃣ Mettre à jour le compteur await conn.query(` UPDATE CompteurConges - SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() + SET Total = ?, Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ? `, [acquisitionRTT, nouveauSolde, existingRTT[0].Id]); @@ -1917,7 +2577,7 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW()) + VALUES (?, ?, ?, ?, ?, 0, GETDATE()) `, [collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT]); console.log(` RTT - Compteur créé: ${acquisitionRTT.toFixed(2)}j`); @@ -1976,13 +2636,17 @@ app.post('/api/login', async (req, res) => { message: 'Connexion réussie via Azure AD', user: { id: user.id, - prenom: user.prenom, - nom: user.nom, + prenom: user.prenom || 'Prénom', + nom: user.nom || 'Nom', email: user.email, - role: user.role, - service: user.service, + role: user.role || 'Collaborateur', + service: user.service || 'Non défini', societeId: user.SocieteId, - societeNom: user.societe_nom + societeNom: user.societe_nom || 'Non défini', + typeContrat: user.TypeContrat || '37h', + description: user.description || null, + dateEntree: user.DateEntree || null, + campusId: user.CampusId || null } }); } else { @@ -2113,7 +2777,6 @@ app.post('/api/check-user-groups', async (req, res) => { } }); - // ======================================== // ✅ CODE CORRIGÉ POUR getDetailedLeaveCounters // À remplacer dans server.js à partir de la ligne ~1600 @@ -2134,23 +2797,14 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { const userQuery = ` SELECT - ca.id, - ca.prenom, - ca.nom, - ca.email, - ca.role, - ca.TypeContrat, - ca.DateEntree, - ca.CampusId, - ca.SocieteId, - s.Nom as service, - so.Nom as societe_nom, - ca.description + ca.id, ca.prenom, ca.nom, ca.email, ca.role, ca.TypeContrat, ca.DateEntree, + ca.CampusId, ca.SocieteId, s.Nom as service, so.Nom as societeNom, ca.description FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ? - AND (ca.Actif = 1 OR ca.Actif IS NULL) + AND (ca.Actif = 1 OR ca.Actif IS NULL) + LIMIT 1 `; const [userInfo] = await conn.query(userQuery, [userIdParam]); @@ -2169,9 +2823,147 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { const currentYear = today.getFullYear(); const previousYear = currentYear - 1; - console.log(`\n📊 === CALCUL COMPTEURS pour ${user.prenom} ${user.nom} ===`); - console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`); + console.log('\n📊 === CALCUL COMPTEURS ==='); + console.log('User:', user.prenom, user.nom, '(ID:', userId, ')'); + console.log('Date référence:', today.toLocaleDateString('fr-FR')); + // ═══════════════════════════════════════════════════════════ + // 1️⃣ CALCUL AVEC LES FORMULES INTELLIGENTES + // ═══════════════════════════════════════════════════════════ + + // CP N + const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree); + console.log('🧮 CP calculé:', acquisCP.toFixed(2) + 'j'); + + // RTT N + let acquisRTT = 0; + let rttTypeId = null; + const [rttType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['RTT']); + + if (rttType.length > 0 && user.role !== 'Apprenti') { + rttTypeId = rttType[0].Id; + const rttData = await calculerAcquisitionRTT_Smart(conn, userId, today); + acquisRTT = rttData.acquisition; + console.log('🧮 RTT calculé:', acquisRTT.toFixed(2) + 'j'); + } + + // ═══════════════════════════════════════════════════════════ + // 2️⃣ RÉCUPÉRER INFOS POUR CALCUL DES SOLDES + // ═══════════════════════════════════════════════════════════ + + const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']); + + if (cpType.length === 0) { + conn.release(); + return res.json({ success: false, message: 'Type de congé CP non trouvé' }); + } + + // Solde reporté CP + let soldeReporte = 0; + const [compteurCP] = await conn.query(` + SELECT SoldeReporte FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [userId, cpType[0].Id, currentYear] + ); + if (compteurCP.length > 0) { + soldeReporte = parseFloat(compteurCP[0].SoldeReporte) || 0; + } + + // Consommé CP + const [consommeCP] = await conn.query(` + SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total + FROM DeductionDetails dd + JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id + WHERE dc.CollaborateurADId = ? + AND dd.TypeCongeId = ? + AND dd.Annee = ? + AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup') + AND dc.Statut != 'Refusé'`, + [userId, cpType[0].Id, currentYear] + ); + const totalConsommeCP = parseFloat(consommeCP[0].total) || 0; + const nouveauSoldeCP = Math.max(0, acquisCP + soldeReporte - totalConsommeCP); + + console.log('💰 CP - Acquis:', acquisCP.toFixed(2), '+ Reporté:', soldeReporte.toFixed(2), '- Consommé:', totalConsommeCP.toFixed(2), '= Solde:', nouveauSoldeCP.toFixed(2)); + + // Consommé RTT + let totalConsommeRTT = 0; + let nouveauSoldeRTT = 0; + if (rttTypeId) { + const [consommeRTT] = await conn.query(` + SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total + FROM DeductionDetails dd + JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id + WHERE dc.CollaborateurADId = ? + AND dd.TypeCongeId = ? + AND dd.Annee = ? + AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup', 'Récup Dose') + AND dc.Statut != 'Refusé'`, + [userId, rttTypeId, currentYear] + ); + totalConsommeRTT = parseFloat(consommeRTT[0].total) || 0; + nouveauSoldeRTT = Math.max(0, acquisRTT - totalConsommeRTT); + + console.log('💰 RTT - Acquis:', acquisRTT.toFixed(2), '- Consommé:', totalConsommeRTT.toFixed(2), '= Solde:', nouveauSoldeRTT.toFixed(2)); + } + + // ═══════════════════════════════════════════════════════════ + // 3️⃣ ENVOYER À GTA-RH POUR SYNCHRONISATION + // ═══════════════════════════════════════════════════════════ + + try { + const rhUrl = process.env.RH_SERVER_URL || 'http://192.168.0.4:3001'; + + const syncPayload = { + collaborateurId: userId, + annee: currentYear, + compteurs: [ + { + typeConge: 'Congé payé', + typeCongeId: cpType[0].Id, + total: parseFloat(acquisCP.toFixed(2)), + solde: parseFloat(nouveauSoldeCP.toFixed(2)), + source: 'calcul_gta' + } + ] + }; + + if (rttTypeId) { + syncPayload.compteurs.push({ + typeConge: 'RTT', + typeCongeId: rttTypeId, + total: parseFloat(acquisRTT.toFixed(2)), + solde: parseFloat(nouveauSoldeRTT.toFixed(2)), + source: 'calcul_gta' + }); + } + + console.log('📤 Envoi synchronisation vers GTA-RH...'); + + const syncResponse = await fetch(`${rhUrl}/api/syncCompteursFromGTA`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(syncPayload) + }); + + if (syncResponse.ok) { + const syncResult = await syncResponse.json(); + console.log('✅ Synchronisation GTA-RH réussie:', syncResult.message); + } else { + console.warn('⚠️ Synchronisation GTA-RH échouée:', syncResponse.status); + } + } catch (syncError) { + console.error('❌ Erreur synchronisation GTA-RH:', syncError.message); + // On continue même si la sync échoue + } + + // ═══════════════════════════════════════════════════════════ + // 4️⃣ RELIRE LA BASE POUR AFFICHER LES VRAIES VALEURS + // ═══════════════════════════════════════════════════════════ + + console.log('\n📖 Lecture base après synchronisation...'); + + // Ancienneté const ancienneteMs = today - new Date(dateEntree || today); const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44)); @@ -2187,7 +2979,7 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { description: user.description, typeContrat: typeContrat, societeId: user.SocieteId, - societeNom: user.societe_nom || 'Non défini', + societeNom: user.societeNom || 'Non défini', dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null, ancienneteMois: ancienneteMois, ancienneteAnnees: Math.floor(ancienneteMois / 12), @@ -2199,250 +2991,146 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { cpN1: null, cpN: null, rttN: null, - rttN1: null, recupN: null, totalDisponible: { cp: 0, rtt: 0, recup: 0, total: 0 } }; - const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); + // ✅ CP N-1 (Report) - LECTURE BASE + const [cpN1Data] = await conn.query(` + SELECT Annee, Total, Solde, SoldeReporte + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [userId, cpType[0].Id, previousYear] + ); - // ==================================== - // 1️⃣ CP N-1 (Report) - CALCUL CONSOMMÉ = ACQUIS - SOLDE - // ==================================== - if (cpType.length > 0) { - const [cpN1] = await conn.query(` - SELECT Annee, Total, Solde, SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, cpType[0].Id, previousYear]); + if (cpN1Data.length > 0) { + const totalAcquis = parseFloat(cpN1Data[0].Total) || 0; + const soldeReporte = parseFloat(cpN1Data[0].Solde) || 0; + const pris = Math.max(0, totalAcquis - soldeReporte); - if (cpN1.length > 0) { - const totalAcquis = parseFloat(cpN1[0].Total || 0); - const soldeReporte = parseFloat(cpN1[0].Solde || 0); + counters.cpN1 = { + annee: previousYear, + exercice: `${previousYear}-${previousYear + 1}`, + reporte: parseFloat(totalAcquis.toFixed(2)), + pris: parseFloat(pris.toFixed(2)), + solde: parseFloat(soldeReporte.toFixed(2)), + pourcentageUtilise: totalAcquis > 0 ? parseFloat((pris / totalAcquis * 100).toFixed(1)) : 0 + }; - // ⭐ CALCUL : Consommé = Acquis - Solde - const pris = Math.max(0, totalAcquis - soldeReporte); + counters.totalDisponible.cp += counters.cpN1.solde; + console.log('✅ CP N-1 BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeReporte + 'j'); + } - counters.cpN1 = { - annee: previousYear, - exercice: `${previousYear}-${previousYear + 1}`, - reporte: parseFloat(totalAcquis.toFixed(2)), - pris: parseFloat(pris.toFixed(2)), - solde: parseFloat(soldeReporte.toFixed(2)), - pourcentageUtilise: totalAcquis > 0 ? parseFloat(((pris / totalAcquis) * 100).toFixed(1)) : 0 - }; - counters.totalDisponible.cp += counters.cpN1.solde; + // ✅ CP N - LECTURE BASE + const [cpNData] = await conn.query(` + SELECT Total, Solde, SoldeReporte + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [userId, cpType[0].Id, currentYear] + ); - console.log(`✅ CP N-1: Acquis=${totalAcquis}j, Solde=${soldeReporte}j → Consommé=${pris}j`); - } else { - counters.cpN1 = { - annee: previousYear, - exercice: `${previousYear}-${previousYear + 1}`, - reporte: 0, - pris: 0, - solde: 0, - pourcentageUtilise: 0 - }; - } - - // ==================================== - // 2️⃣ CP N (Exercice en cours) - CALCUL CONSOMMÉ = ACQUIS - SOLDE - // ==================================== - const [compteurCPN] = await conn.query(` - SELECT Solde, SoldeReporte, Total - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, cpType[0].Id, currentYear]); - - let soldeActuelCP = 0; - let totalAcquis = 0; - let cpPris = 0; - - if (compteurCPN.length > 0) { - const soldeBDD = parseFloat(compteurCPN[0].Solde || 0); - const soldeReporte = parseFloat(compteurCPN[0].SoldeReporte || 0); - totalAcquis = parseFloat(compteurCPN[0].Total || 0); - soldeActuelCP = Math.max(0, soldeBDD - soldeReporte); - - // ⭐ CALCUL : Consommé = Acquis - (Solde - Report) - cpPris = Math.max(0, totalAcquis - soldeActuelCP); - - console.log(` CP N - Total=${totalAcquis}j, Solde BDD=${soldeBDD}j, Report=${soldeReporte}j → Solde N=${soldeActuelCP}j, Consommé=${cpPris}j`); - } else { - const acquisCP = calculerAcquisitionCP(today, dateEntree); - soldeActuelCP = acquisCP; - totalAcquis = acquisCP; - cpPris = 0; - console.log(` CP N - Pas de compteur BDD → Calcul: ${acquisCP}j`); - } - - // Calculer l'anticipé disponible - const [anticipeUtiliseCP] = 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 dd.TypeDeduction = 'N Anticip' - AND dc.Statut != 'Refusée' - `, [userId, cpType[0].Id, currentYear]); - - const cpAnticipeUtilise = parseFloat(anticipeUtiliseCP[0]?.totalConsomme || 0); - const cpAnticipeMax = Math.max(0, 25 - totalAcquis); - const cpAnticipeDisponible = Math.max(0, cpAnticipeMax - cpAnticipeUtilise); - - console.log(` CP - Acquis: ${totalAcquis.toFixed(2)}j`); - console.log(` CP - Consommé: ${cpPris.toFixed(2)}j`); - console.log(` CP - Solde: ${soldeActuelCP.toFixed(2)}j`); + if (cpNData.length > 0) { + const totalAcquis = parseFloat(cpNData[0].Total) || 0; + const soldeBDD = parseFloat(cpNData[0].Solde) || 0; + const soldeReporteBDD = parseFloat(cpNData[0].SoldeReporte) || 0; + const soldeReel = Math.max(0, soldeBDD - soldeReporteBDD); + const pris = Math.max(0, totalAcquis - soldeReel); counters.cpN = { annee: currentYear, exercice: getExerciceCP(today), totalAnnuel: 25.00, - moisTravailles: parseFloat(getMoisTravaillesCP(today, dateEntree).toFixed(2)), - acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)), acquis: parseFloat(totalAcquis.toFixed(2)), - pris: parseFloat(cpPris.toFixed(2)), // ⭐ CONSOMMÉ CALCULÉ - solde: parseFloat(soldeActuelCP.toFixed(2)), - tauxAcquisition: parseFloat((getMoisTravaillesCP(today, dateEntree) / 12 * 100).toFixed(1)), - pourcentageUtilise: totalAcquis > 0 ? parseFloat((cpPris / totalAcquis * 100).toFixed(1)) : 0, - joursRestantsAAcquerir: parseFloat((25 - totalAcquis).toFixed(2)), - anticipe: { - acquisPrevu: parseFloat(cpAnticipeMax.toFixed(2)), - pris: parseFloat(cpAnticipeUtilise.toFixed(2)), - disponible: parseFloat(cpAnticipeDisponible.toFixed(2)), - depassement: cpAnticipeUtilise > cpAnticipeMax ? parseFloat((cpAnticipeUtilise - cpAnticipeMax).toFixed(2)) : 0 - } + pris: parseFloat(pris.toFixed(2)), + solde: parseFloat(soldeReel.toFixed(2)), + pourcentageUtilise: totalAcquis > 0 ? parseFloat((pris / totalAcquis * 100).toFixed(1)) : 0 }; - counters.totalDisponible.cp += counters.cpN.solde + cpAnticipeDisponible; + counters.totalDisponible.cp += counters.cpN.solde; + console.log('✅ CP N BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeReel + 'j'); } - // ==================================== - // 3️⃣ RTT N - CALCUL CONSOMMÉ = ACQUIS - SOLDE - // ==================================== - const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); - + // ✅ RTT N - LECTURE BASE if (rttType.length > 0 && user.role !== 'Apprenti') { - const [compteurRTT] = await conn.query(` - SELECT Solde, Total + const [rttNData] = await conn.query(` + SELECT Total, Solde FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, rttType[0].Id, currentYear]); + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [userId, rttTypeId, currentYear] + ); - let soldeActuelRTT = 0; - let totalAcquis = 0; - let rttPris = 0; + if (rttNData.length > 0) { + const totalAcquis = parseFloat(rttNData[0].Total) || 0; + const soldeBDD = parseFloat(rttNData[0].Solde) || 0; + const pris = Math.max(0, totalAcquis - soldeBDD); - const rttData = await calculerAcquisitionRTT(conn, userId, today); - const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat); + const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat); - if (compteurRTT.length > 0) { - soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0); - totalAcquis = parseFloat(compteurRTT[0].Total || 0); + counters.rttN = { + annee: currentYear, + typeContrat: typeContrat, + totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)), + acquis: parseFloat(totalAcquis.toFixed(2)), + pris: parseFloat(pris.toFixed(2)), + solde: parseFloat(soldeBDD.toFixed(2)), + pourcentageUtilise: totalAcquis > 0 ? parseFloat((pris / totalAcquis * 100).toFixed(1)) : 0 + }; - // ⭐ CALCUL : Consommé = Acquis - Solde - rttPris = Math.max(0, totalAcquis - soldeActuelRTT); - - console.log(` RTT - Acquis: ${totalAcquis}j, Solde: ${soldeActuelRTT}j → Consommé: ${rttPris}j`); - } else { - soldeActuelRTT = rttData.acquisition; - totalAcquis = rttData.acquisition; - rttPris = 0; - console.log(` RTT - Pas de compteur BDD → Calcul: ${rttData.acquisition}j`); + counters.totalDisponible.rtt += counters.rttN.solde; + console.log('✅ RTT BASE: Acquis=' + totalAcquis + 'j, Solde=' + soldeBDD + 'j'); } - - counters.rttN = { - annee: currentYear, - typeContrat: typeContrat, - totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)), - moisTravailles: rttData.moisTravailles, - acquisitionMensuelle: parseFloat(rttConfig.acquisitionMensuelle.toFixed(6)), - acquis: parseFloat(totalAcquis.toFixed(2)), - pris: parseFloat(rttPris.toFixed(2)), // ⭐ CONSOMMÉ CALCULÉ - solde: parseFloat(soldeActuelRTT.toFixed(2)), - tauxAcquisition: parseFloat((rttData.moisTravailles / 12 * 100).toFixed(1)), - pourcentageUtilise: totalAcquis > 0 ? parseFloat((rttPris / totalAcquis * 100).toFixed(1)) : 0, - joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - totalAcquis).toFixed(2)) - }; - - counters.totalDisponible.rtt += counters.rttN.solde; } - counters.rttN1 = { - annee: previousYear, - reporte: 0, - pris: 0, - solde: 0, - pourcentageUtilise: 0, - message: "Les RTT ne sont pas reportables d'une année sur l'autre" - }; - - // ==================================== - // 4️⃣ RÉCUP - CALCUL CONSOMMÉ = ACQUIS - SOLDE - // ==================================== - const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); - + // ✅ Récup - LECTURE BASE + const [recupType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Récupération']); if (recupType.length > 0) { - const [compteurRecup] = await conn.query(` - SELECT Solde FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, recupType[0].Id, currentYear]); - - const soldeRecup = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0; - - // Récupérer accumulations depuis DeductionDetails - const [accumRecup] = await conn.query(` - SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAccum + const [recupData] = await conn.query(` + SELECT + COALESCE(SUM(CASE WHEN dd.TypeDeduction IN ('Accum Récup', 'Accum Recup') THEN dd.JoursUtilises ELSE 0 END), 0) as acquis, + COALESCE(SUM(CASE WHEN dd.TypeDeduction = 'Récup Dose' THEN dd.JoursUtilises ELSE 0 END), 0) as pris FROM DeductionDetails dd JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id - WHERE dc.CollaborateurADId = ? - AND dd.TypeCongeId = ? - AND dd.Annee = ? - AND dd.TypeDeduction IN ('Accum Récup', 'Accum Recup') - AND dc.Statut != 'Refusée' - `, [userId, recupType[0].Id, currentYear]); + WHERE dc.CollaborateurADId = ? AND dd.TypeCongeId = ? AND dd.Annee = ? AND dc.Statut != 'Refusé'`, + [userId, recupType[0].Id, currentYear] + ); - const acquis = parseFloat(accumRecup[0]?.totalAccum || 0); + if (recupData.length > 0) { + const acquis = parseFloat(recupData[0].acquis) || 0; + const pris = parseFloat(recupData[0].pris) || 0; + const solde = Math.max(0, acquis - pris); - // ⭐ CALCUL : Consommé = Acquis - Solde - const pris = Math.max(0, acquis - soldeRecup); + counters.recupN = { + annee: currentYear, + acquis: parseFloat(acquis.toFixed(2)), + pris: parseFloat(pris.toFixed(2)), + solde: parseFloat(solde.toFixed(2)), + message: "Jours de récupération accumulés suite à du temps supplémentaire" + }; - counters.recupN = { - annee: currentYear, - acquis: parseFloat(acquis.toFixed(2)), - pris: parseFloat(pris.toFixed(2)), // ⭐ CONSOMMÉ CALCULÉ - solde: parseFloat(soldeRecup.toFixed(2)), - message: "Jours de récupération" - }; - - counters.totalDisponible.recup = counters.recupN.solde; - - console.log(`✅ Récup: Acquis=${acquis}j, Solde=${soldeRecup}j → Consommé=${pris}j`); + counters.totalDisponible.recup = counters.recupN.solde; + console.log('✅ RÉCUP BASE: Acquis=' + acquis + 'j, Solde=' + solde + 'j'); + } } - counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt + counters.totalDisponible.recup; - - console.log(`\n✅ TOTAL FINAL: ${counters.totalDisponible.total.toFixed(2)}j disponibles`); + // Total disponible + counters.totalDisponible.total = + counters.totalDisponible.cp + + counters.totalDisponible.rtt + + counters.totalDisponible.recup; conn.release(); - res.json({ - success: true, - message: 'Compteurs détaillés récupérés avec succès', - data: counters, - availableCP: counters.totalDisponible.cp, - availableRTT: counters.totalDisponible.rtt, - availableRecup: counters.totalDisponible.recup - }); + console.log('\n✅ Réponse envoyée au frontend'); + console.log(' CP disponible:', counters.totalDisponible.cp + 'j'); + console.log(' RTT disponible:', counters.totalDisponible.rtt + 'j'); + console.log(' RÉCUP disponible:', counters.totalDisponible.recup + 'j'); + console.log(' TOTAL disponible:', counters.totalDisponible.total + 'j'); + + res.json({ success: true, data: counters }); + } catch (error) { - console.error('Erreur getDetailedLeaveCounters:', error); - res.status(500).json({ - success: false, - message: 'Erreur serveur', - error: error.message - }); + console.error('❌ Erreur getDetailedLeaveCounters:', error); + res.status(500).json({ success: false, message: error.message }); } }); app.post('/api/updateCounters', async (req, res) => { @@ -2503,8 +3191,8 @@ async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId // Déduction dans la base await conn.query( `UPDATE CompteurConges - SET SoldeReporte = GREATEST(0, SoldeReporte - ?), - Solde = GREATEST(0, Solde - ?) + SET SoldeReporte = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END, + Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ?`, [aDeduireN1, aDeduireN1, compteurN1[0].Id] ); @@ -2543,7 +3231,7 @@ async function deductLeaveBalanceWithTracking(conn, collaborateurId, typeCongeId // Déduction dans la base await conn.query( `UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?) + SET Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ?`, [aDeduireN, compteurN[0].Id] ); @@ -2582,9 +3270,8 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { console.log(`Demande ID: ${demandeCongeId}`); console.log(`Collaborateur ID: ${collaborateurId}`); - // 1️⃣ Récupérer TOUTES les déductions (y compris Récup) const [deductions] = await conn.query( - `SELECT dd.*, tc.Nom as TypeNom + `SELECT dd.TypeCongeId, dd.Annee, dd.TypeDeduction, dd.JoursUtilises, tc.Nom as TypeNom FROM DeductionDetails dd JOIN TypeConge tc ON dd.TypeCongeId = tc.Id WHERE dd.DemandeCongeId = ? @@ -2606,9 +3293,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j (Année: ${Annee})`); - // ======================================== - // RÉCUP POSÉE - RESTAURATION - // ======================================== + // ⭐ NOUVEAU : Gestion des Récup posées if (TypeDeduction === 'Récup Posée') { console.log(`🔄 Restauration Récup posée: +${JoursUtilises}j`); @@ -2625,7 +3310,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { await conn.query( `UPDATE CompteurConges SET Solde = ?, - DerniereMiseAJour = NOW() + DerniereMiseAJour = GETDATE() WHERE Id = ?`, [nouveauSolde, compteur[0].Id] ); @@ -2637,15 +3322,11 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { joursRestores: JoursUtilises }); console.log(`✅ Récup restaurée: ${ancienSolde} → ${nouveauSolde}`); - } else { - console.warn(`⚠️ Compteur Récup non trouvé pour l'année ${Annee}`); } continue; } - // ======================================== - // N+1 ANTICIPÉ - RESTAURATION - // ======================================== + // 🔹 N+1 Anticipé - ⭐ RESTAURATION CORRECTE if (TypeDeduction === 'N+1 Anticipé') { console.log(`🔄 Restauration N+1 Anticipé: +${JoursUtilises}j`); @@ -2662,7 +3343,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { await conn.query( `UPDATE CompteurConges SET SoldeAnticipe = ?, - DerniereMiseAJour = NOW() + DerniereMiseAJour = GETDATE() WHERE Id = ?`, [nouveauSolde, compteur[0].Id] ); @@ -2675,11 +3356,11 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { }); console.log(`✅ N+1 Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`); } else { - // Créer le compteur N+1 s'il n'existe pas + // ⭐ Créer le compteur N+1 s'il n'existe pas await conn.query( `INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) - VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`, + VALUES (?, ?, ?, 0, 0, 0, ?, GETDATE())`, [collaborateurId, TypeCongeId, Annee, JoursUtilises] ); @@ -2694,9 +3375,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { continue; } - // ======================================== - // N ANTICIPÉ - RESTAURATION - // ======================================== + // 🔹 N Anticipé - ⭐ RESTAURATION CORRECTE if (TypeDeduction === 'N Anticipé') { console.log(`🔄 Restauration N Anticipé: +${JoursUtilises}j`); @@ -2713,7 +3392,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { await conn.query( `UPDATE CompteurConges SET SoldeAnticipe = ?, - DerniereMiseAJour = NOW() + DerniereMiseAJour = GETDATE() WHERE Id = ?`, [nouveauSolde, compteur[0].Id] ); @@ -2726,11 +3405,11 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { }); console.log(`✅ N Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`); } else { - // Créer le compteur s'il n'existe pas + // ⭐ Créer le compteur s'il n'existe pas (cas rare) await conn.query( `INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) - VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`, + VALUES (?, ?, ?, 0, 0, 0, ?, GETDATE())`, [collaborateurId, TypeCongeId, Annee, JoursUtilises] ); @@ -2745,10 +3424,8 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { continue; } - // ======================================== - // REPORTÉ N-1 - // ======================================== - if (TypeDeduction === 'Reporté N-1' || TypeDeduction === 'Report N-1' || TypeDeduction === 'Année N-1') { + // 🔹 Reporté N-1 + if (TypeDeduction === 'Reporté N-1' || TypeDeduction === 'Report N-1') { const [compteur] = await conn.query( `SELECT Id, SoldeReporte, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, @@ -2763,7 +3440,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { `UPDATE CompteurConges SET SoldeReporte = SoldeReporte + ?, Solde = Solde + ?, - DerniereMiseAJour = NOW() + DerniereMiseAJour = GETDATE() WHERE Id = ?`, [JoursUtilises, JoursUtilises, compteur[0].Id] ); @@ -2778,9 +3455,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { } } - // ======================================== - // ANNÉE N - // ======================================== + // 🔹 Année N else if (TypeDeduction === 'Année N') { const [compteur] = await conn.query( `SELECT Id, Solde FROM CompteurConges @@ -2795,7 +3470,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { await conn.query( `UPDATE CompteurConges SET Solde = Solde + ?, - DerniereMiseAJour = NOW() + DerniereMiseAJour = GETDATE() WHERE Id = ?`, [JoursUtilises, compteur[0].Id] ); @@ -2809,50 +3484,9 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { console.log(`✅ Année N restaurée: ${ancienSolde} → ${nouveauSolde}`); } } - - // ======================================== - // ACCUM RÉCUP (enlever de l'accumulation) - // ======================================== - else if (TypeDeduction === 'Accum Récup' || TypeDeduction === 'Accum Recup') { - console.log(`⚠️ Accumulation Récup détectée - À enlever: ${JoursUtilises}j`); - - const [compteur] = await conn.query( - `SELECT Id, Solde FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, TypeCongeId, Annee] - ); - - if (compteur.length > 0) { - const ancienSolde = parseFloat(compteur[0].Solde || 0); - const nouveauSolde = Math.max(0, ancienSolde - parseFloat(JoursUtilises)); - - await conn.query( - `UPDATE CompteurConges - SET Solde = ?, - DerniereMiseAJour = NOW() - WHERE Id = ?`, - [nouveauSolde, compteur[0].Id] - ); - - restorations.push({ - type: TypeNom, - annee: Annee, - typeDeduction: 'Annulation Accumulation', - joursRestores: JoursUtilises - }); - console.log(`✅ Accumulation annulée: ${ancienSolde} → ${nouveauSolde}`); - } - } } - // ⭐ SUPPRIMER LES DÉDUCTIONS - await conn.query( - 'DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', - [demandeCongeId] - ); - console.log('🗑️ Déductions supprimées de la base'); - - // ⭐ Recalculer les soldes anticipés + // ⭐ IMPORTANT : Recalculer les soldes anticipés après restauration console.log(`\n🔄 Recalcul des soldes anticipés...`); await updateSoldeAnticipe(conn, collaborateurId); @@ -2881,8 +3515,8 @@ app.get('/api/testProrata', async (req, res) => { const typeContrat = user.TypeContrat || '37h'; const today = new Date(); const moisCP = getMoisTravaillesCP(today, dateEntree); - const acquisCP = calculerAcquisitionCP(today, dateEntree); - const rttData = await calculerAcquisitionRTT(conn, userId, today); + const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree); + const rttData = await calculerAcquisitionRTT_Smart(conn, userId, today); conn.release(); res.json({ success: true, @@ -2927,8 +3561,8 @@ app.post('/api/fixAllCounters', async (req, res) => { for (const collab of collaborateurs) { const dateEntree = collab.DateEntree; const moisCP = getMoisTravaillesCP(today, dateEntree); - const acquisCP = calculerAcquisitionCP(today, dateEntree); - const rttData = await calculerAcquisitionRTT(conn, collab.id, today); + const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree); + const rttData = await calculerAcquisitionRTT_Smart(conn, collab.id, today); const acquisRTT = rttData.acquisition; 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']); @@ -2939,7 +3573,7 @@ app.post('/api/fixAllCounters', async (req, res) => { const ancienSolde = parseFloat(existingCP[0].Solde); const difference = acquisCP - ancienTotal; const nouveauSolde = Math.max(0, ancienSolde + difference); - await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisCP, nouveauSolde, existingCP[0].Id]); + await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, [acquisCP, nouveauSolde, existingCP[0].Id]); corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'CP', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisCP.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) }); } } @@ -2950,7 +3584,7 @@ app.post('/api/fixAllCounters', async (req, res) => { const ancienSolde = parseFloat(existingRTT[0].Solde); const difference = acquisRTT - ancienTotal; const nouveauSolde = Math.max(0, ancienSolde + difference); - await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, [acquisRTT, nouveauSolde, existingRTT[0].Id]); + await conn.query(`UPDATE CompteurConges SET Total = ?, Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?`, [acquisRTT, nouveauSolde, existingRTT[0].Id]); corrections.push({ collaborateur: `${collab.prenom} ${collab.nom}`, type: 'RTT', ancienTotal: ancienTotal.toFixed(2), nouveauTotal: acquisRTT.toFixed(2), ancienSolde: ancienSolde.toFixed(2), nouveauSolde: nouveauSolde.toFixed(2) }); } } @@ -3138,16 +3772,16 @@ app.get('/api/getEmploye', async (req, res) => { success: true, employee: { id: employee.id, - Nom: employee.Nom, - Prenom: employee.Prenom, - Email: employee.Email, - role: employee.role, - TypeContrat: employee.TypeContrat, + Nom: employee.Nom || 'Non défini', + Prenom: employee.Prenom || 'Non défini', + Email: employee.Email || 'Non défini', + role: employee.role || 'Collaborateur', + TypeContrat: employee.TypeContrat || '37h', DateEntree: employee.DateEntree, CampusId: employee.CampusId, SocieteId: employee.SocieteId, - service: employee.service, - societe_nom: employee.societe_nom, + service: employee.service || 'Non défini', + societe_nom: employee.societe_nom || 'Non défini', conges_restants: parseFloat(cpSolde.toFixed(2)), rtt_restants: parseFloat(rttSolde.toFixed(2)) } @@ -3209,8 +3843,48 @@ app.get('/api/getRequests', async (req, res) => { const userId = req.query.user_id; if (!userId) return res.json({ success: false, message: 'ID utilisateur manquant' }); - // ✅ REQUÊTE CORRIGÉE avec la table de liaison - const [rows] = await pool.query(` + // 🔍 Déterminer si c'est un UUID ou un ID numérique + const isUUID = userId.length > 10 && userId.includes('-'); + console.log(`📝 Type userId détecté: ${isUUID ? 'UUID (entraUserId)' : 'ID numérique'}`); + + let mainRequest = pool.request(); + let whereClause; + + if (isUUID) { + // Si UUID, chercher d'abord le CollaborateurADId + const lookupRequest = pool.request(); + lookupRequest.input('entraUserId', userId); + + const userLookup = await lookupRequest.query(` + SELECT id + FROM CollaborateurAD + WHERE entraUserId = @entraUserId + `); + + if (userLookup.recordset.length === 0) { + return res.json({ + success: false, + message: 'Utilisateur non trouvé', + requests: [], + total: 0 + }); + } + + const collaborateurId = userLookup.recordset[0].id; + console.log(`✅ CollaborateurADId trouvé: ${collaborateurId}`); + + // Créer une nouvelle request pour la requête principale + mainRequest.input('collaborateurId', collaborateurId); + whereClause = 'dc.CollaborateurADId = @collaborateurId'; + + } else { + // Si ID numérique, utiliser directement + mainRequest.input('userId', parseInt(userId)); + whereClause = '(dc.EmployeeId = @userId OR dc.CollaborateurADId = @userId)'; + } + + // ✅ REQUÊTE CORRIGÉE pour MSSQL avec la table de liaison + const result = await mainRequest.query(` SELECT dc.Id, dc.DateDebut, @@ -3221,15 +3895,27 @@ app.get('/api/getRequests', async (req, res) => { dc.CommentaireValidation, dc.Validateur, dc.DocumentJoint, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS TypeConges, - SUM(dct.NombreJours) as NombreJoursTotal + ( + SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom) + FROM ( + SELECT DISTINCT tc2.Nom + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id + ) AS DistinctTypes + ) AS TypeConges, + ( + SELECT SUM(dct2.NombreJours) + FROM DemandeCongeType dct2 + WHERE dct2.DemandeCongeId = dc.Id + ) AS NombreJoursTotal FROM DemandeConge dc - LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - WHERE (dc.EmployeeId = ? OR dc.CollaborateurADId = ?) - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CommentaireValidation, dc.Validateur, dc.DocumentJoint + WHERE ${whereClause} ORDER BY dc.DateDemande DESC - `, [userId, userId]); + `); + + const rows = result.recordset; + console.log(`📋 ${rows.length} demandes trouvées pour userId: ${userId}`); const requests = rows.map(row => { const workingDays = getWorkingDays(row.DateDebut, row.DateFin); @@ -3274,6 +3960,7 @@ app.get('/api/getRequests', async (req, res) => { }); } }); + app.get('/api/getAllTeamRequests', async (req, res) => { try { const managerId = req.query.SuperieurId; @@ -3288,33 +3975,246 @@ app.get('/api/getAllTeamRequests', async (req, res) => { app.get('/api/getPendingRequests', async (req, res) => { try { - const managerId = req.query.manager_id; - if (!managerId) return res.json({ success: false, message: 'ID manager manquant' }); - const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]); - if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' }); + const validatorId = req.query.validator_id; + + if (!validatorId) { + return res.json({ success: false, message: 'ID validateur manquant' }); + } + + const conn = await pool.getConnection(); + + // Récupérer les infos du validateur + const [managerRows] = await conn.query( + 'SELECT TOP 1 ServiceId, CampusId, role FROM CollaborateurAD WHERE id = ?', + [validatorId] + ); + + if (managerRows.length === 0) { + conn.release(); + return res.json({ success: false, message: 'Validateur non trouvé' }); + } + const serviceId = managerRows[0].ServiceId; - const [rows] = await pool.query(`SELECT dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, CONCAT(ca.prenom, ' ', ca.nom) as employee_name, ca.email as employee_email, GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') as types FROM DemandeConge dc JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) WHERE ca.ServiceId = ? AND dc.Statut = 'En attente' AND ca.id != ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, dc.Commentaire, dc.CollaborateurADId, ca.prenom, ca.nom, ca.email ORDER BY dc.DateDemande ASC`, [serviceId, managerId]); - const requests = rows.map(row => ({ id: row.Id, employee_id: row.CollaborateurADId, employee_name: row.employee_name, employee_email: row.employee_email, type: row.types, start_date: row.DateDebut, end_date: row.DateFin, date_display: row.DateDebut === row.DateFin ? formatDate(row.DateDebut) : `${formatDate(row.DateDebut)} - ${formatDate(row.DateFin)}`, days: getWorkingDays(row.DateDebut, row.DateFin), status: row.Statut, reason: row.Commentaire || '', submitted_at: row.DateDemande, submitted_display: formatDate(row.DateDemande) })); - res.json({ success: true, message: 'Demandes récupérées', requests, service_id: serviceId }); + const campusId = managerRows[0].CampusId; + const role = normalizeRole(managerRows[0].role); + + let requests; + + if (role === 'admin' || role === 'president' || role === 'rh') { + // Admin/President/RH : toutes les demandes en attente + [requests] = await conn.query(` + SELECT + dc.Id, + dc.CollaborateurADId, + dc.DateDebut, + dc.DateFin, + dc.NombreJours, + dc.Statut, + dc.Commentaire, + tc.Nom as TypeConge, + ca.prenom, + ca.nom, + s.Nom as service_name, + camp.Nom as campus_name + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id + JOIN TypeConge tc ON dc.TypeCongeId = tc.Id + LEFT JOIN Services s ON ca.ServiceId = s.Id + LEFT JOIN Campus camp ON ca.CampusId = camp.Id + WHERE dc.Statut = 'En attente' + ORDER BY dc.DateCreation DESC + `); + } else if (role === 'validateur' || role === 'directeur de campus') { + // Validateur/Directeur : leurs collaborateurs directs via hiérarchie + [requests] = await conn.query(` + SELECT DISTINCT + dc.Id, + dc.CollaborateurADId, + dc.DateDebut, + dc.DateFin, + dc.NombreJours, + dc.Statut, + dc.Commentaire, + tc.Nom as TypeConge, + ca.prenom, + ca.nom, + s.Nom as service_name, + camp.Nom as campus_name + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id + JOIN HierarchieValidationAD hv ON ca.id = hv.CollaborateurId + JOIN TypeConge tc ON dc.TypeCongeId = tc.Id + LEFT JOIN Services s ON ca.ServiceId = s.Id + LEFT JOIN Campus camp ON ca.CampusId = camp.Id + WHERE dc.Statut = 'En attente' + AND hv.SuperieurId = ? + ORDER BY dc.DateCreation DESC + `, [validatorId]); + } else { + conn.release(); + return res.json({ success: false, message: 'Rôle non autorisé pour validation' }); + } + + conn.release(); + + res.json({ + success: true, + message: 'Demandes récupérées', + requests, + service_id: serviceId, + campus_id: campusId + }); + } catch (error) { - res.status(500).json({ success: false, message: 'Erreur', error: error.message }); + console.error('❌ Erreur getPendingRequests:', error); + res.status(500).json({ + success: false, + message: 'Erreur', + error: error.message + }); } }); + +// Route getTeamMembers - Membres de l'équipe app.get('/api/getTeamMembers', async (req, res) => { try { const managerId = req.query.manager_id; if (!managerId) return res.json({ success: false, message: 'ID manager manquant' }); - const [managerRows] = await pool.query('SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', [managerId]); - if (managerRows.length === 0) return res.json({ success: false, message: 'Manager non trouvé' }); - const serviceId = managerRows[0].ServiceId; - const [members] = await pool.query(`SELECT c.id, c.nom, c.prenom, c.email, c.role, s.Nom as service_name, c.CampusId FROM CollaborateurAD c JOIN Services s ON c.ServiceId = s.Id WHERE c.ServiceId = ? AND c.id != ? ORDER BY c.prenom, c.nom`, [serviceId, managerId]); - res.json({ success: true, message: 'Équipe récupérée', team_members: members, service_id: serviceId }); + + const managerRequest = pool.request(); + managerRequest.input('managerId', managerId); + + const managerResult = await managerRequest.query(` + SELECT TOP 1 ServiceId, CampusId, role, email + FROM CollaborateurAD + WHERE id = @managerId + `); + + if (managerResult.recordset.length === 0) { + return res.json({ success: false, message: 'Manager non trouvé' }); + } + + const managerInfo = managerResult.recordset[0]; + const serviceId = managerInfo.ServiceId; + const campusId = managerInfo.CampusId; + const role = normalizeRole(managerInfo.role); + + console.log(`🔍 getTeamMembers - Manager: ${managerId}, Role: ${role}, Campus: ${campusId}`); + + let members; + + if (role === 'admin' || role === 'president' || role === 'rh') { + // CAS 1: Admin/President/RH - Vue globale + console.log("CAS 1: Admin/President/RH - Vue globale"); + + const membersRequest = pool.request(); + membersRequest.input('managerId', managerId); + + const membersResult = await membersRequest.query(` + SELECT + c.id, + c.nom, + c.prenom, + c.email, + c.role, + s.Nom as service_name, + c.CampusId, + camp.Nom as campus_name + FROM CollaborateurAD c + JOIN Services s ON c.ServiceId = s.Id + LEFT JOIN Campus camp ON c.CampusId = camp.Id + WHERE c.id != @managerId + AND (c.actif = 1 OR c.actif IS NULL) + ORDER BY c.prenom, c.nom + `); + + members = membersResult.recordset; + console.log(` ✅ ${members.length} collaborateur(s) au total`); + + } else if (role === 'validateur' || role === 'directeur de campus') { + // CAS 2: Validateur/Directeur - Collaborateurs directs via hiérarchie + console.log("CAS 2: Validateur/Directeur - Collaborateurs directs via hiérarchie"); + + const membersRequest = pool.request(); + membersRequest.input('managerId', managerId); + + const membersResult = await membersRequest.query(` + SELECT DISTINCT + c.id, + c.nom, + c.prenom, + c.email, + c.role, + s.Nom as service_name, + c.CampusId, + camp.Nom as campus_name + FROM CollaborateurAD c + JOIN HierarchieValidationAD hv ON c.id = hv.CollaborateurId + JOIN Services s ON c.ServiceId = s.Id + LEFT JOIN Campus camp ON c.CampusId = camp.Id + WHERE hv.SuperieurId = @managerId + AND (c.actif = 1 OR c.actif IS NULL) + ORDER BY c.prenom, c.nom + `); + + members = membersResult.recordset; + console.log(` ✅ ${members.length} collaborateur(s) sous ${managerId}`); + + } else if (role === 'collaborateur' || role === 'apprenti') { + // CAS 3: Collaborateur/Apprenti - Collègues du même service ET campus + console.log("CAS 3: Collaborateur/Apprenti - Collègues du même service et campus"); + + const membersRequest = pool.request(); + membersRequest.input('managerId', managerId); + membersRequest.input('serviceId', serviceId); + membersRequest.input('campusId', campusId); + + const membersResult = await membersRequest.query(` + SELECT + c.id, + c.nom, + c.prenom, + c.email, + c.role, + s.Nom as service_name, + c.CampusId, + camp.Nom as campus_name + FROM CollaborateurAD c + JOIN Services s ON c.ServiceId = s.Id + LEFT JOIN Campus camp ON c.CampusId = camp.Id + WHERE c.ServiceId = @serviceId + AND c.CampusId = @campusId + AND c.id != @managerId + AND (c.actif = 1 OR c.actif IS NULL) + ORDER BY c.prenom, c.nom + `); + + members = membersResult.recordset; + console.log(` ✅ ${members.length} collègue(s) trouvé(s)`); + + } else { + return res.json({ success: false, message: 'Rôle non autorisé' }); + } + + res.json({ + success: true, + team_members: members || [], + service_id: serviceId, + campus_id: campusId + }); + } catch (error) { - res.status(500).json({ success: false, message: 'Erreur', error: error.message }); + console.error('❌ Erreur getTeamMembers:', error); + res.status(500).json({ + success: false, + message: 'Erreur', + error: error.message + }); } }); + app.get('/api/getNotifications', async (req, res) => { try { const userIdParam = req.query.user_id; @@ -3492,7 +4392,7 @@ app.post('/api/saisirRecupJour', async (req, res) => { INSERT INTO DemandeConge (CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, NombreJours) - VALUES (?, ?, ?, ?, 'Validée', NOW(), ?, ?) + VALUES (?, ?, ?, ?, 'Validée', GETDATE(), ?, ?) `, [user_id, date, date, recupTypeId, commentaire || `Samedi travaillé - ${dateFormatted}`, nombre_heures]); const demandeId = result.insertId; @@ -3516,7 +4416,7 @@ app.post('/api/saisirRecupJour', async (req, res) => { UPDATE CompteurConges SET Total = Total + ?, Solde = Solde + ?, - DerniereMiseAJour = NOW() + DerniereMiseAJour = GETDATE() WHERE Id = ? `, [nombre_heures, nombre_heures, compteur[0].Id]); @@ -3525,7 +4425,7 @@ app.post('/api/saisirRecupJour', async (req, res) => { await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW()) + VALUES (?, ?, ?, ?, ?, 0, GETDATE()) `, [user_id, recupTypeId, currentYear, nombre_heures, nombre_heures]); console.log(`✅ Compteur créé: ${nombre_heures}j`); @@ -3542,7 +4442,7 @@ app.post('/api/saisirRecupJour', async (req, res) => { await conn.query(` INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu) - VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, NOW(), 0) + VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, GETDATE(), 0) `, [ user_id, `Samedi ${dateFormatted} enregistré : +${nombre_heures}j de récupération`, @@ -3661,11 +4561,11 @@ async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartit let budgetAnnuel = 0; if (typeCode === 'CP') { - acquisALaDate = calculerAcquisitionCP(dateDebutObj, dateEntree); + acquisALaDate = calculerAcquisitionCP_Smart(dateDebutObj, dateEntree); budgetAnnuel = 25; console.log(`💰 Acquisition CP à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`); } else if (typeCode === 'RTT' && !isApprenti) { - const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDebutObj); + const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, dateDebutObj); acquisALaDate = rttData.acquisition; budgetAnnuel = rttData.config.joursAnnuels; console.log(`💰 Acquisition RTT à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`); @@ -3779,193 +4679,193 @@ async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartit * Déduit les jours d'un compteur avec gestion de l'anticipation * Ordre de déduction : N-1 → N → N Anticip */ + +// ======================================== +// 💰 FONCTION DE DÉDUCTION AVEC ANTICIPATION (SANS ID MANUEL) +// ======================================== + async function deductLeaveBalanceWithAnticipation(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) { const dateDebutObj = new Date(dateDebut); const currentYear = dateDebutObj.getFullYear(); const previousYear = currentYear - 1; - let joursRestants = nombreJours; const deductions = []; - console.log(`\n💳 === DÉDUCTION AVEC ANTICIPATION ===`); + console.log(`💳 === DÉDUCTION AVEC ANTICIPATION ===`); console.log(` Collaborateur: ${collaborateurId}`); console.log(` Type congé: ${typeCongeId}`); console.log(` Jours à déduire: ${nombreJours}j`); console.log(` Date début: ${dateDebut}`); - // ==================================== - // 1️⃣ Déduire du REPORT N-1 (CP uniquement) - // ==================================== - const [compteurN1] = await conn.query(` - SELECT Id, Solde, SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, previousYear]); - - if (compteurN1.length > 0) { - const soldeN1 = parseFloat(compteurN1[0].Solde || 0); - const aDeduireN1 = Math.min(soldeN1, joursRestants); - - if (aDeduireN1 > 0) { - await conn.query(` - UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?), - SoldeReporte = GREATEST(0, SoldeReporte - ?), - DerniereMiseAJour = NOW() - WHERE Id = ? - `, [aDeduireN1, aDeduireN1, compteurN1[0].Id]); - - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Année N-1', ?) - `, [demandeCongeId, typeCongeId, previousYear, aDeduireN1]); - - deductions.push({ - annee: previousYear, - type: 'Report N-1', - joursUtilises: aDeduireN1, - soldeAvant: soldeN1 - }); - - joursRestants -= aDeduireN1; - console.log(` ✓ Déduit ${aDeduireN1.toFixed(2)}j du report N-1`); - } - } - - // ==================================== - // 2️⃣ Déduire du SOLDE N (acquis actuel) - // ==================================== - if (joursRestants > 0) { - const [compteurN] = await conn.query(` - SELECT Id, Solde, SoldeReporte, Total + // ===== ÉTAPE 1 : Déduire du REPORT N-1 ===== + try { + const [compteurN1] = await conn.query(` + SELECT Id, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, currentYear]); + `, [collaborateurId, typeCongeId, previousYear]); - if (compteurN.length > 0) { - const soldeTotal = parseFloat(compteurN[0].Solde || 0); - const soldeReporte = parseFloat(compteurN[0].SoldeReporte || 0); - const soldeN = Math.max(0, soldeTotal - soldeReporte); // Solde actuel sans le report + if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) { + const soldeN1 = parseFloat(compteurN1[0].SoldeReporte || 0); + const aDeduireN1 = Math.min(soldeN1, joursRestants); - const aDeduireN = Math.min(soldeN, joursRestants); - - if (aDeduireN > 0) { + if (aDeduireN1 > 0) { await conn.query(` UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?), - DerniereMiseAJour = NOW() + SET SoldeReporte = SoldeReporte - ?, + Solde = Solde - ?, + DerniereMiseAJour = GETDATE() WHERE Id = ? - `, [aDeduireN, compteurN[0].Id]); + `, [aDeduireN1, aDeduireN1, compteurN1[0].Id]); + // ⭐ SANS SPÉCIFIER L'ID await conn.query(` INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Année N', ?) - `, [demandeCongeId, typeCongeId, currentYear, aDeduireN]); + VALUES (?, ?, ?, ?, ?) + `, [demandeCongeId, typeCongeId, previousYear, 'Report N-1', aDeduireN1]); + + deductions.push({ + annee: previousYear, + type: 'Report N-1', + joursUtilises: aDeduireN1 + }); + + joursRestants -= aDeduireN1; + console.log(` ✅ Report N-1: ${aDeduireN1.toFixed(2)}j déduits - reste ${joursRestants}j`); + } + } + } catch (error) { + console.error('❌ Erreur déduction N-1:', error.message); + throw error; + } + + // ===== ÉTAPE 2 : Déduire du SOLDE N ===== + if (joursRestants > 0) { + try { + const [compteurN] = await conn.query(` + SELECT Id, Solde, SoldeReporte, Total + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, typeCongeId, currentYear]); + + if (compteurN.length > 0) { + const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0); + const aDeduireN = Math.min(soldeN, joursRestants); + + if (aDeduireN > 0) { + await conn.query(` + UPDATE CompteurConges + SET Solde = Solde - ?, + DerniereMiseAJour = GETDATE() + WHERE Id = ? + `, [aDeduireN, compteurN[0].Id]); + + // ⭐ SANS SPÉCIFIER L'ID + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, ?, ?) + `, [demandeCongeId, typeCongeId, currentYear, 'Année N', aDeduireN]); + + deductions.push({ + annee: currentYear, + type: 'Année N', + joursUtilises: aDeduireN + }); + + joursRestants -= aDeduireN; + console.log(` ✅ Solde N: ${aDeduireN.toFixed(2)}j déduits - reste ${joursRestants}j`); + } + } + } catch (error) { + console.error('❌ Erreur déduction N:', error.message); + throw error; + } + } + + // ===== ÉTAPE 3 : ANTICIPÉ ===== + if (joursRestants > 0) { + console.log(` 💳 Il reste ${joursRestants.toFixed(2)}j à déduire → Anticipé`); + + try { + const [collabInfo] = await conn.query(` + SELECT DateEntree, TypeContrat, role + FROM CollaborateurAD + WHERE id = ? + `, [collaborateurId]); + + const dateEntree = collabInfo[0]?.DateEntree || null; + + const [typeInfo] = await conn.query(` + SELECT Nom FROM TypeConge WHERE Id = ? + `, [typeCongeId]); + + const typeNom = typeInfo[0]?.Nom; + let budgetAnnuel = 0; + let acquisALaDate = 0; + + if (typeNom === 'Congé payé') { + acquisALaDate = calculerAcquisitionCP_Smart(dateDebutObj, dateEntree); + budgetAnnuel = 25; + } else if (typeNom === 'RTT') { + const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, dateDebutObj); + acquisALaDate = rttData.acquisition; + budgetAnnuel = rttData.config.joursAnnuels; + } + + const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate); + + const [anticipeUtilise] = await conn.query(` + SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total + FROM DeductionDetails dd + JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id + WHERE dc.CollaborateurADId = ? + AND dd.TypeCongeId = ? + AND dd.Annee = ? + AND dd.TypeDeduction = 'N Anticip' + AND dc.Statut != 'Refusée' + `, [collaborateurId, typeCongeId, currentYear]); + + const dejaPrisAnticipe = parseFloat(anticipeUtilise[0]?.total || 0); + const anticipeDisponible = Math.max(0, anticipableMax - dejaPrisAnticipe); + + console.log(` 💳 Anticipable max: ${anticipableMax.toFixed(2)}j`); + console.log(` 💳 Déjà pris: ${dejaPrisAnticipe.toFixed(2)}j`); + console.log(` 💳 Disponible: ${anticipeDisponible.toFixed(2)}j`); + + if (anticipeDisponible >= joursRestants) { + // ⭐ SANS SPÉCIFIER L'ID + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, ?, ?) + `, [demandeCongeId, typeCongeId, currentYear, 'N Anticip', joursRestants]); deductions.push({ annee: currentYear, - type: 'Année N', - joursUtilises: aDeduireN, - soldeAvant: soldeN + type: 'N Anticip', + joursUtilises: joursRestants }); - joursRestants -= aDeduireN; - console.log(` ✓ Déduit ${aDeduireN.toFixed(2)}j du solde N actuel`); + console.log(` ✅ Anticipé: ${joursRestants.toFixed(2)}j`); + joursRestants = 0; + } else { + return { + success: false, + joursDeduitsTotal: nombreJours - joursRestants, + joursNonDeduits: joursRestants, + details: deductions, + error: `Solde insuffisant (manque ${joursRestants.toFixed(2)}j)` + }; } + } catch (error) { + console.error('❌ Erreur anticipé:', error.message); + throw error; } } - // ==================================== - // 3️⃣ Déduire de l'ANTICIPÉ N (ce qui reste à acquérir) - // ==================================== - if (joursRestants > 0) { - console.log(` 💳 Il reste ${joursRestants.toFixed(2)}j à déduire → Utilisation de l'anticipé`); - - // Récupérer les infos pour calculer l'anticipé disponible - const [collabInfo] = await conn.query(` - SELECT DateEntree, TypeContrat, role - FROM CollaborateurAD - WHERE id = ? - `, [collaborateurId]); - - const dateEntree = collabInfo[0]?.DateEntree || null; - const typeContrat = collabInfo[0]?.TypeContrat || '37h'; - const isApprenti = collabInfo[0]?.role === 'Apprenti'; - - // Déterminer le type de congé - const [typeInfo] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]); - const typeNom = typeInfo[0]?.Nom || ''; - - let acquisALaDate = 0; - let budgetAnnuel = 0; - - if (typeNom === 'Congé payé') { - acquisALaDate = calculerAcquisitionCP(dateDebutObj, dateEntree); - budgetAnnuel = 25; - } else if (typeNom === 'RTT' && !isApprenti) { - const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDebutObj); - acquisALaDate = rttData.acquisition; - budgetAnnuel = rttData.config.joursAnnuels; - } - - // Calculer l'anticipé disponible - const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate); - - // Vérifier combien a déjà été pris en anticipé - const [anticipeUtilise] = await conn.query(` - SELECT COALESCE(SUM(dd.JoursUtilises), 0) as total - FROM DeductionDetails dd - JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id - WHERE dc.CollaborateurADId = ? - AND dd.TypeCongeId = ? - AND dd.Annee = ? - AND dd.TypeDeduction = 'N Anticip' - AND dc.Statut != 'Refusée' - AND dc.Id != ? - `, [collaborateurId, typeCongeId, currentYear, demandeCongeId]); - - const dejaPrisAnticipe = parseFloat(anticipeUtilise[0]?.total || 0); - const anticipeDisponible = Math.max(0, anticipableMax - dejaPrisAnticipe); - - console.log(` 💳 Anticipé max: ${anticipableMax.toFixed(2)}j`); - console.log(` 💳 Déjà pris: ${dejaPrisAnticipe.toFixed(2)}j`); - console.log(` 💳 Disponible: ${anticipeDisponible.toFixed(2)}j`); - - const aDeduireAnticipe = Math.min(anticipeDisponible, joursRestants); - - if (aDeduireAnticipe > 0) { - // Enregistrer la déduction anticipée - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'N Anticip', ?) - `, [demandeCongeId, typeCongeId, currentYear, aDeduireAnticipe]); - - // Mettre à jour SoldeAnticipe dans CompteurConges - await conn.query(` - UPDATE CompteurConges - SET SoldeAnticipe = GREATEST(0, ? - ?), - DerniereMiseAJour = NOW() - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [anticipeDisponible, aDeduireAnticipe, collaborateurId, typeCongeId, currentYear]); - - deductions.push({ - annee: currentYear, - type: 'N Anticip', - joursUtilises: aDeduireAnticipe, - soldeAvant: anticipeDisponible - }); - - joursRestants -= aDeduireAnticipe; - console.log(` ✓ Déduit ${aDeduireAnticipe.toFixed(2)}j de l'anticipé N`); - } else if (joursRestants > 0) { - console.error(` ❌ Impossible de déduire ${joursRestants.toFixed(2)}j : anticipé épuisé !`); - } - } - - console.log(` ✅ Déduction terminée - Total déduit: ${(nombreJours - joursRestants).toFixed(2)}j\n`); + console.log(` ✅ Déduction OK - Total: ${(nombreJours - joursRestants).toFixed(2)}j`); return { success: joursRestants === 0, @@ -3974,7 +4874,21 @@ async function deductLeaveBalanceWithAnticipation(conn, collaborateurId, typeCon details: deductions }; } - +// ======================================== +// 🔧 FONCTION HELPER - GÉNÉRATION D'ID +// ======================================== +// ✅ VERSION CORRIGÉE +async function getNextId(connection, tableName) { + try { + const [result] = await connection.query( + `SELECT ISNULL(MAX(Id), 0) + 1 AS NextId FROM ${tableName}` + ); + return result[0].NextId; + } catch (error) { + console.error(`❌ Erreur génération ID pour ${tableName}:`, error.message); + throw error; + } +} app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => { const conn = await pool.getConnection(); @@ -3988,10 +4902,32 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), console.log('📎 Fichiers médicaux reçus:', uploadedFiles.length); // ✅ Les données arrivent différemment avec FormData - const { DateDebut, DateFin, NombreJours, Email, Nom, Commentaire, statut } = req.body; + const DateDebut = req.body.DateDebut; + const DateFin = req.body.DateFin; + const NombreJours = parseFloat(req.body.NombreJours); + const Email = req.body.Email; + const Nom = req.body.Nom; + const Commentaire = req.body.Commentaire || ''; + const statut = req.body.statut || null; // ✅ Parser la répartition (elle arrive en string depuis FormData) - const Repartition = JSON.parse(req.body.Repartition || '[]'); + let Repartition; + try { + Repartition = JSON.parse(req.body.Repartition || '[]'); + } catch (parseError) { + console.error('❌ Erreur parsing Repartition:', parseError); + if (req.files) { + req.files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + }); + } + return res.status(400).json({ + success: false, + message: 'Erreur de format de la répartition' + }); + } if (!DateDebut || !DateFin || !Repartition || !Email || !Nom) { uploadedFiles.forEach(file => { @@ -4002,7 +4938,7 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), return res.json({ success: false, message: 'Données manquantes' }); } - // ✅ Validation : Si arrêt maladie, il faut au moins 1 fichier + // ✅ VALIDATION : Si arrêt maladie, il faut au moins 1 fichier const hasABS = Repartition.some(r => r.TypeConge === 'ABS'); if (hasABS && uploadedFiles.length === 0) { await conn.rollback(); @@ -4091,8 +5027,6 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]); const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti'; - // ⭐ CORRECTION : Passer la date de début pour détecter N+1 - // ✅ APRÈS (avec anticipation) const checkResult = await checkLeaveBalanceWithAnticipation( conn, collaborateurId, @@ -4100,7 +5034,6 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), DateDebut ); - // Adapter le format de la réponse if (!checkResult.valide) { uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); @@ -4108,7 +5041,6 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), await conn.rollback(); conn.release(); - // Construire le message d'erreur const messagesErreur = checkResult.insuffisants.map(ins => { return `${ins.type}: ${ins.joursNecessaires}j demandés mais seulement ${ins.soldeTotal.toFixed(2)}j disponibles (déficit: ${ins.deficit.toFixed(2)}j)`; }).join('\n'); @@ -4121,12 +5053,11 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), }); } - console.log('✅ Tous les soldes sont suffisants (incluant anticipation si nécessaire)\n'); } // ======================================== - // ÉTAPE 2 : CRÉER LA DEMANDE + // ÉTAPE 2 : CRÉER LA DEMANDE (AVEC GÉNÉRATION MANUELLE D'ID) // ======================================== console.log('\n📝 Création de la demande...'); @@ -4134,7 +5065,6 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), for (const rep of Repartition) { const code = rep.TypeConge; - // Ne pas inclure ABS et Formation dans les typeIds principaux if (code === 'ABS' || code === 'Formation') { continue; } @@ -4147,7 +5077,6 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), if (typeRow.length > 0) typeIds.push(typeRow[0].Id); } - // Si aucun type CP/RTT/Récup, prendre le premier type de la répartition if (typeIds.length === 0) { const firstType = Repartition[0]?.TypeConge; const name = firstType === 'Formation' ? 'Formation' : @@ -4167,17 +5096,30 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), } const typeCongeIdCsv = typeIds.join(','); - const currentDate = new Date().toISOString().slice(0, 19).replace('T', ' '); - // ✅ CRÉER LA DEMANDE - const [result] = await conn.query( - `INSERT INTO DemandeConge - (EmployeeId, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [isAD ? 0 : employeeId, collaborateurId, DateDebut, DateFin, typeCongeIdCsv, statutDemande, currentDate, Commentaire || '', '', NombreJours] - ); + // 🔥 GÉNÉRATION MANUELLE DE L'ID (CONTOURNEMENT IDENTITY) + const [maxIdResult] = await conn.query('SELECT ISNULL(MAX(Id), 0) + 1 AS NextId FROM DemandeConge'); + const demandeId = maxIdResult[0].NextId; + + console.log(`🆔 ID généré manuellement: ${demandeId}`); + + // 🔥 INSERT AVEC ID EXPLICITE + await conn.query(` + INSERT INTO DemandeConge + (Id, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours) + VALUES (?, ?, ?, ?, ?, ?, GETDATE(), ?, ?, ?) + `, [ + demandeId, + collaborateurId, + DateDebut, + DateFin, + typeCongeIdCsv, + statutDemande, + Commentaire || '', + '', + NombreJours + ]); - const demandeId = result.insertId; console.log(`✅ Demande créée avec ID ${demandeId} - Statut: ${statutDemande}`); // ======================================== @@ -4186,12 +5128,11 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), if (uploadedFiles.length > 0) { console.log('\n📎 Sauvegarde des fichiers médicaux...'); for (const file of uploadedFiles) { - await conn.query( - `INSERT INTO DocumentsMedicaux - (DemandeCongeId, NomFichier, CheminFichier, TypeMime, TailleFichier, DateUpload) - VALUES (?, ?, ?, ?, ?, NOW())`, - [demandeId, file.originalname, file.path, file.mimetype, file.size] - ); + await conn.query(` + INSERT INTO DocumentsMedicaux + (DemandeCongeId, NomFichier, CheminFichier, TypeMime, TailleFichier, DateUpload) + VALUES (?, ?, ?, ?, ?, GETDATE()) + `, [demandeId, file.originalname, file.path, file.mimetype, file.size]); console.log(` ✓ ${file.originalname}`); } } @@ -4199,6 +5140,10 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), // ======================================== // ÉTAPE 4 : Sauvegarder la répartition // ======================================== + // ======================================== + // ÉTAPE 4 : Sauvegarder la répartition + // ======================================== + // 5️⃣ Sauvegarder la répartition console.log('\n📊 Sauvegarde de la répartition en base...'); for (const rep of Repartition) { const code = rep.TypeConge; @@ -4214,21 +5159,23 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), ); if (typeRow.length > 0) { - await conn.query( - `INSERT INTO DemandeCongeType - (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) - VALUES (?, ?, ?, ?)`, - [ - demandeId, - typeRow[0].Id, - rep.NombreJours, - rep.PeriodeJournee || 'Journée entière' - ] - ); + // ⭐ GÉNÉRER L'ID MANUELLEMENT + const demandeCongeTypeId = await getNextId(conn, 'DemandeCongeType'); + + await conn.query(` + INSERT INTO DemandeCongeType + (Id, DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) + VALUES (?, ?, ?, ?, ?) + `, [ + demandeCongeTypeId, + demandeId, + typeRow[0].Id, + rep.NombreJours, + rep.PeriodeJournee || 'Journée entière' + ]); console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`); } } - // ======================================== // ÉTAPE 5 : Déduction des compteurs CP/RTT/RÉCUP (AVEC ANTICIPATION N+1) // ======================================== @@ -4248,10 +5195,10 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), if (recupType.length > 0) { await conn.query(` UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?), - DerniereMiseAJour = NOW() + SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, + DerniereMiseAJour = GETDATE() WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [rep.NombreJours, collaborateurId, recupType[0].Id, currentYear]); + `, [rep.NombreJours, rep.NombreJours, collaborateurId, recupType[0].Id, currentYear]); await conn.query(` INSERT INTO DeductionDetails @@ -4269,7 +5216,7 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); if (typeRow.length > 0) { - const result = await deductLeaveBalanceWithAnticipation( + const deductResult = await deductLeaveBalanceWithAnticipation( conn, collaborateurId, typeRow[0].Id, @@ -4279,8 +5226,8 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), ); console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`); - if (result.details && result.details.length > 0) { - result.details.forEach(d => { + if (deductResult.details && deductResult.details.length > 0) { + deductResult.details.forEach(d => { console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`); }); } @@ -4299,17 +5246,16 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`; if (isFormationOnly && isAD && collaborateurId) { - await conn.query( - `INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu) - VALUES (?, ?, ?, ?, ?, NOW(), 0)`, - [ - collaborateurId, - 'Success', - '✅ Formation validée automatiquement', - `Votre période de formation ${datesPeriode} a été validée automatiquement.`, - demandeId - ] - ); + await conn.query(` + INSERT INTO Notifications (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu) + VALUES (?, ?, ?, ?, ?, GETDATE(), 0) + `, [ + collaborateurId, + 'Success', + '✅ Formation validée automatiquement', + `Votre période de formation ${datesPeriode} a été validée automatiquement.`, + demandeId + ]); console.log('\n📬 Notification formation créée'); } @@ -4318,12 +5264,11 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), // ======================================== let managers = []; if (isAD) { - const [rows] = await conn.query( - `SELECT c.email FROM HierarchieValidationAD hv - JOIN CollaborateurAD c ON hv.SuperieurId = c.id - WHERE hv.CollaborateurId = ?`, - [collaborateurId] - ); + const [rows] = await conn.query(` + SELECT c.email FROM HierarchieValidationAD hv + JOIN CollaborateurAD c ON hv.SuperieurId = c.id + WHERE hv.CollaborateurId = ? + `, [collaborateurId]); managers = rows.map(r => r.email); } @@ -4358,7 +5303,6 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), }).join(' | '); if (isFormationOnly) { - // Email formation const subjectCollab = '✅ Formation enregistrée et validée'; const bodyCollab = `
@@ -4379,7 +5323,6 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), console.error('❌ Erreur email:', mailError.message); } } else { - // ⭐ EMAIL NORMAL (incluant Récup) const subjectCollab = '✅ Confirmation de réception de votre demande de congé'; const bodyCollab = `
@@ -4456,14 +5399,13 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), } }); - app.get('/api/download-medical/:documentId', async (req, res) => { try { const { documentId } = req.params; const conn = await pool.getConnection(); const [docs] = await conn.query( - 'SELECT * FROM DocumentsMedicaux WHERE Id = ?', + 'SELECT TOP 1 * FROM DocumentsMedicaux WHERE Id = ?', [documentId] ); @@ -4487,6 +5429,7 @@ app.get('/api/download-medical/:documentId', async (req, res) => { } }); + // Récupérer les documents d'une demande app.get('/api/medical-documents/:demandeId', async (req, res) => { try { @@ -4520,6 +5463,8 @@ app.get('/api/medical-documents/:demandeId', async (req, res) => { res.status(500).json({ success: false, message: 'Erreur serveur' }); } }); + + app.post('/api/validateRequest', async (req, res) => { const conn = await pool.getConnection(); try { @@ -4532,15 +5477,19 @@ app.post('/api/validateRequest', async (req, res) => { return res.json({ success: false, message: 'Données manquantes' }); } - const [validator] = await conn.query('SELECT Id, prenom, nom, email, CampusId FROM CollaborateurAD WHERE Id = ?', [validator_id]); + // Récupérer le validateur + const [validator] = await conn.query( + 'SELECT TOP 1 Id, prenom, nom, email, CampusId FROM CollaborateurAD WHERE Id = ?', + [validator_id] + ); if (validator.length === 0) { throw new Error('Validateur introuvable'); } - // Récupérer les informations de la demande + // Récupérer la demande const [requests] = await conn.query( - `SELECT + `SELECT TOP 1 dc.Id, dc.CollaborateurADId, dc.TypeCongeId, @@ -4556,8 +5505,7 @@ app.post('/api/validateRequest', async (req, res) => { FROM DemandeConge dc JOIN TypeConge tc ON dc.TypeCongeId = tc.Id LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id - WHERE dc.Id = ? - LIMIT 1`, + WHERE dc.Id = ?`, [request_id] ); @@ -4567,7 +5515,6 @@ app.post('/api/validateRequest', async (req, res) => { const request = requests[0]; - // ⭐ DÉBOGAGE : Afficher TOUTES les données console.log('\n=== DONNÉES RÉCUPÉRÉES ==='); console.log('request:', JSON.stringify(request, null, 2)); console.log('validator:', JSON.stringify(validator[0], null, 2)); @@ -4579,31 +5526,41 @@ app.post('/api/validateRequest', async (req, res) => { const newStatus = action === 'approve' ? 'Validée' : 'Refusée'; + // Si refus, restaurer les soldes if (action === 'reject' && request.CollaborateurADId) { console.log(`\n🔄 REFUS - Restauration des soldes...`); const restoration = await restoreLeaveBalance(conn, request_id, request.CollaborateurADId); console.log('Restauration:', restoration); } + // Mettre à jour le statut await conn.query( - `UPDATE DemandeConge SET Statut = ?, ValidateurId = ?, ValidateurADId = ?, DateValidation = NOW(), CommentaireValidation = ? WHERE Id = ?`, + `UPDATE DemandeConge + SET Statut = ?, + ValidateurId = ?, + ValidateurADId = ?, + DateValidation = GETDATE(), + CommentaireValidation = ? + WHERE Id = ?`, [newStatus, validator_id, validator_id, comment || '', request_id] ); + // Créer une notification const notifTitle = action === 'approve' ? 'Demande approuvée ✅' : 'Demande refusée ❌'; let notifMessage = `Votre demande a été ${action === 'approve' ? 'approuvée' : 'refusée'}`; if (comment) notifMessage += ` (Commentaire: ${comment})`; const notifType = action === 'approve' ? 'Success' : 'Error'; await conn.query( - 'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)', - [request.CollaborateurADId, notifTitle, notifMessage, notifType, request_id, nowFR()] + `INSERT INTO Notifications + (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) + VALUES (?, ?, ?, ?, ?, GETDATE(), 0)`, + [request.CollaborateurADId, notifTitle, notifMessage, notifType, request_id] ); await conn.commit(); - // ⭐ TEST EMAIL - // ⭐ ENVOI EMAIL AVEC TEMPLATE PROFESSIONNEL + // Envoyer email via Microsoft Graph console.log('\n📧 === TENTATIVE ENVOI EMAIL ==='); console.log('1. Récupération token...'); @@ -4629,116 +5586,49 @@ app.post('/api/validateRequest', async (req, res) => { ? '✅ Votre demande de congé a été approuvée' : '❌ Votre demande de congé a été refusée'; - const body = action === 'approve' - ? `
-
-

✅ Demande approuvée

-
-
-

Bonjour ${collaborateurNom},

-

Votre demande de congé a été approuvée par ${validateurNom}.

-
-

Type : ${request.TypeConge}

-

Période : ${datesPeriode}

-

Durée : ${request.NombreJours} jour(s)

- ${comment ? `

Commentaire : ${comment}

` : ''} -
-

Vous pouvez consulter votre demande dans votre espace personnel.

-
-
` - : `
-
-

❌ Demande refusée

-
-
-

Bonjour ${collaborateurNom},

-

Votre demande de congé a été refusée par ${validateurNom}.

-
-

Type : ${request.TypeConge}

-

Période : ${datesPeriode}

-

Durée : ${request.NombreJours} jour(s)

- ${comment ? `

Motif du refus : ${comment}

` : ''} -
-

Pour plus d'informations, contactez ${validateurNom}.

-
-
`; + const emailHtml = ` + + +
+

${subject}

+ +

Bonjour ${collaborateurNom},

+ +

Votre demande de ${request.TypeConge} pour ${request.NombreJours} jour(s) ${datesPeriode} a été ${action === 'approve' ? 'approuvée' : 'refusée'} par ${validateurNom}.

+ + ${comment ? `

Commentaire: ${comment}

` : ''} + +

Vous pouvez consulter les détails dans l'application GTA.

+ +
+ +

+ Ceci est un email automatique, merci de ne pas y répondre. +

+
+ + + `; - console.log('4. Sujet:', subject); - console.log('5. Appel sendMailGraph...'); - - try { - await sendMailGraph( - accessToken, - fromEmail, - request.collaborateur_email, - subject, - body - ); - console.log('✅✅✅ EMAIL ENVOYÉ AVEC SUCCÈS ! ✅✅✅'); - } catch (mailError) { - console.error('❌❌❌ ERREUR ENVOI EMAIL ❌❌❌'); - console.error('Message:', mailError.message); - console.error('Stack:', mailError.stack); - } - } else { - if (!accessToken) console.error('❌ Token manquant'); - if (!request.collaborateur_email) console.error('❌ Email collaborateur manquant'); - } - console.log('=== FIN ENVOI EMAIL ===\n'); - - // Notifier les clients SSE locaux - notifyCollabClients({ - type: 'demande-validated', - demandeId: parseInt(request_id), - statut: newStatus, - timestamp: new Date().toISOString() - }, request.CollaborateurADId); - - notifyCollabClients({ - type: 'demande-list-updated', - action: 'validation', - demandeId: parseInt(request_id), - timestamp: new Date().toISOString() - }); - - // Envoyer webhook au serveur RH - try { - await webhookManager.sendWebhook( - WEBHOOKS.RH_URL, - EVENTS.DEMANDE_VALIDATED, - { - demandeId: parseInt(request_id), - statut: newStatus, - collaborateurId: request.CollaborateurADId, - validateurId: validator_id, - commentaire: comment - } - ); - console.log('✅ Webhook envoyé au serveur RH'); - } catch (webhookError) { - console.error('❌ Erreur envoi webhook (non bloquant):', webhookError.message); + const emailSent = await sendMailGraph(accessToken, fromEmail, request.collaborateur_email, subject, emailHtml); + console.log('4. Email envoyé ?', emailSent ? 'OUI ✅' : 'NON ❌'); } res.json({ success: true, - message: `Demande ${action === 'approve' ? 'approuvée' : 'refusée'}`, - new_status: newStatus + message: `Demande ${action === 'approve' ? 'approuvée' : 'refusée'} avec succès` }); - console.log(`✅ Demande ${request_id} ${newStatus}\n`); - } catch (error) { await conn.rollback(); - console.error('\n❌ ERREUR lors de la validation:', error); - res.status(500).json({ - success: false, - message: error.message - }); + console.error('❌ Erreur validateRequest:', error); + res.status(500).json({ success: false, message: error.message }); } finally { conn.release(); } }); + app.get('/api/testRestoration', async (req, res) => { const conn = await pool.getConnection(); try { @@ -4888,7 +5778,7 @@ app.get('/api/getCampusBySociete', async (req, res) => { // ======================================== app.get('/api/getTeamLeaves', async (req, res) => { try { - let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query; + let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query; console.log(`🔍 Paramètres reçus: user_id=${userIdParam}, role=${roleParam}, selectedCampus=${selectedCampus}`); @@ -4896,11 +5786,12 @@ app.get('/api/getTeamLeaves', async (req, res) => { return res.json({ success: false, message: 'ID utilisateur manquant' }); } - const conn = await pool.getConnection(); - const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); console.log(`📝 Type ID détecté: ${isUUID ? 'UUID' : 'Numérique'}`); + const userRequest = pool.request(); + userRequest.input('userIdParam', userIdParam); + const userQuery = ` SELECT ca.id, @@ -4915,23 +5806,23 @@ app.get('/api/getTeamLeaves', async (req, res) => { LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Campus c ON ca.CampusId = c.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id - WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ? - LIMIT 1 + WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = @userIdParam `; - const [userRows] = await conn.query(userQuery, [userIdParam]); + const userResult = await userRequest.query(userQuery); - if (!userRows || userRows.length === 0) { - conn.release(); + if (!userResult.recordset || userResult.recordset.length === 0) { return res.json({ success: false, message: 'Collaborateur non trouvé' }); } - const userInfo = userRows[0]; + const userInfo = userResult.recordset[0]; const serviceId = userInfo.ServiceId; const campusId = userInfo.CampusId; const societeId = userInfo.SocieteId; const userEmail = userInfo.email; const campusNom = userInfo.campusNom; + const serviceNom = userInfo.serviceNom; + const societeNom = userInfo.societeNom; function normalizeRole(role) { if (!role) return null; @@ -4952,138 +5843,122 @@ app.get('/api/getTeamLeaves', async (req, res) => { console.log(` - ServiceId: ${serviceId}`); console.log(` - CampusId: ${campusId}`); console.log(` - CampusNom: ${campusNom}`); + console.log(` - ServiceNom: ${serviceNom}`); console.log(` - SocieteId: ${societeId}`); console.log(` - Role normalisé: ${role}`); - let query, params; const filters = {}; - - - // ======================================== - // CAS 1: PRESIDENT, ADMIN, RH - // ======================================== // ======================================== // CAS 1: PRESIDENT, ADMIN, RH, DIRECTEUR DE CAMPUS + // ⚠️ SANS VALIDATEUR - C'est la correction principale ! // ======================================== if (role === 'president' || role === 'admin' || role === 'rh' || role === 'directeur de campus') { console.log("CAS 1: President/Admin/RH/Directeur de Campus - Vue globale"); console.log(` Filtres reçus: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); - // ======================================== - // 🔧 LISTE COMPLÈTE DES FILTRES DISPONIBLES - // ======================================== - - // 1️⃣ SOCIÉTÉS (toutes disponibles) - const [societesList] = await conn.query(` - SELECT DISTINCT Nom - FROM Societe - ORDER BY Nom - `); - filters.societes = societesList.map(s => s.Nom); + // 1️⃣ SOCIÉTÉS + const societesRequest = pool.request(); + const societesResult = await societesRequest.query(` + SELECT DISTINCT Nom + FROM Societe + ORDER BY Nom + `); + filters.societes = societesResult.recordset.map(s => s.Nom); console.log('📊 Sociétés disponibles:', filters.societes); - // 2️⃣ CAMPUS (tous les campus, filtrés par société si nécessaire) + // 2️⃣ CAMPUS + let campusRequest = pool.request(); let campusQuery; - let campusParams = []; if (selectedSociete && selectedSociete !== 'all') { campusQuery = ` - SELECT DISTINCT c.Nom - FROM Campus c - JOIN CollaborateurAD ca ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE so.Nom = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY c.Nom - `; - campusParams = [selectedSociete]; + SELECT DISTINCT c.Nom + FROM Campus c + JOIN CollaborateurAD ca ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + WHERE so.Nom = @selectedSociete + AND (ca.actif = 1 OR ca.actif IS NULL) + ORDER BY c.Nom + `; + campusRequest.input('selectedSociete', selectedSociete); } else { campusQuery = ` - SELECT DISTINCT Nom - FROM Campus - ORDER BY Nom - `; + SELECT DISTINCT Nom + FROM Campus + ORDER BY Nom + `; } - const [campusList] = await conn.query(campusQuery, campusParams); - filters.campus = campusList.map(c => c.Nom); + const campusResult = await campusRequest.query(campusQuery); + filters.campus = campusResult.recordset.map(c => c.Nom); console.log('📊 Campus disponibles:', filters.campus); - // ⭐ NOUVEAU : Pour directeur de campus, envoyer son campus par défaut if (role === 'directeur de campus') { - filters.defaultCampus = campusNom; // Le campus du directeur + filters.defaultCampus = campusNom; console.log('🏢 Campus par défaut pour directeur:', campusNom); } - // 3️⃣ SERVICES (filtrés selon société + campus) + // 3️⃣ SERVICES let servicesQuery = ` - SELECT DISTINCT s.Nom - FROM Services s - JOIN CollaborateurAD ca ON ca.ServiceId = s.Id - `; + SELECT DISTINCT s.Nom + FROM Services s + JOIN CollaborateurAD ca ON ca.ServiceId = s.Id + `; - let servicesJoins = []; let servicesConditions = ['(ca.actif = 1 OR ca.actif IS NULL)']; - let servicesParams = []; + let servicesRequest = pool.request(); if (selectedSociete && selectedSociete !== 'all') { - servicesJoins.push('JOIN Societe so ON ca.SocieteId = so.Id'); - servicesConditions.push('so.Nom = ?'); - servicesParams.push(selectedSociete); + servicesQuery += '\nJOIN Societe so ON ca.SocieteId = so.Id'; + servicesConditions.push('so.Nom = @selectedSociete'); + servicesRequest.input('selectedSociete', selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { - servicesJoins.push('JOIN Campus c ON ca.CampusId = c.Id'); - servicesConditions.push('c.Nom = ?'); - servicesParams.push(selectedCampus); - } - - if (servicesJoins.length > 0) { - servicesQuery += '\n' + servicesJoins.join('\n'); + servicesQuery += '\nJOIN Campus c ON ca.CampusId = c.Id'; + servicesConditions.push('c.Nom = @selectedCampus'); + servicesRequest.input('selectedCampus', selectedCampus); } servicesQuery += `\nWHERE ${servicesConditions.join(' AND ')}\nORDER BY s.Nom`; - const [servicesList] = await conn.query(servicesQuery, servicesParams); - filters.services = servicesList.map(s => s.Nom); + const servicesResult = await servicesRequest.query(servicesQuery); + filters.services = servicesResult.recordset.map(s => s.Nom); - // ======================================== - // 🔧 LISTE DES EMPLOYÉS (avec filtres appliqués) - // ======================================== + // ⭐ LISTE DES EMPLOYÉS let employeesQuery = ` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - c.Nom AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE (ca.actif = 1 OR ca.actif IS NULL) - `; + SELECT + CONCAT(ca.prenom, ' ', ca.nom) AS fullname, + c.Nom AS campusnom, + so.Nom AS societenom, + s.Nom AS servicenom + FROM CollaborateurAD ca + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; let employeesConditions = []; - let employeesParams = []; + let employeesRequest = pool.request(); if (selectedSociete && selectedSociete !== 'all') { - employeesConditions.push('so.Nom = ?'); - employeesParams.push(selectedSociete); + employeesConditions.push('so.Nom = @selectedSociete'); + employeesRequest.input('selectedSociete', selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { - employeesConditions.push('c.Nom = ?'); - employeesParams.push(selectedCampus); + employeesConditions.push('c.Nom = @selectedCampus'); + employeesRequest.input('selectedCampus', selectedCampus); } else if (role === 'directeur de campus' && campusNom) { - // ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut - employeesConditions.push('c.Nom = ?'); - employeesParams.push(campusNom); + employeesConditions.push('c.Nom = @campusNom'); + employeesRequest.input('campusNom', campusNom); } if (selectedService && selectedService !== 'all') { - employeesConditions.push('s.Nom = ?'); - employeesParams.push(selectedService); + employeesConditions.push('s.Nom = @selectedService'); + employeesRequest.input('selectedService', selectedService); } if (employeesConditions.length > 0) { @@ -5092,9 +5967,9 @@ app.get('/api/getTeamLeaves', async (req, res) => { employeesQuery += ` ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom`; - const [employeesList] = await conn.query(employeesQuery, employeesParams); + const employeesResult = await employeesRequest.query(employeesQuery); - filters.employees = employeesList.map(e => ({ + filters.employees = employeesResult.recordset.map(e => ({ name: e.fullname, campus: e.campusnom, societe: e.societenom, @@ -5103,195 +5978,196 @@ app.get('/api/getTeamLeaves', async (req, res) => { console.log(`👥 Employés trouvés:`, filters.employees.length); - // ======================================== - // 🔧 QUERY DES CONGÉS (avec filtres appliqués) - // ======================================== + // QUERY DES CONGÉS let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`]; - let whereParams = []; + let queryRequest = pool.request(); if (selectedSociete && selectedSociete !== 'all') { - whereConditions.push('so.Nom = ?'); - whereParams.push(selectedSociete); + whereConditions.push('so.Nom = @selectedSociete'); + queryRequest.input('selectedSociete', selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { - whereConditions.push('c.Nom = ?'); - whereParams.push(selectedCampus); + whereConditions.push('c.Nom = @selectedCampus'); + queryRequest.input('selectedCampus', selectedCampus); } else if (role === 'directeur de campus' && campusNom) { - // ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut - whereConditions.push('c.Nom = ?'); - whereParams.push(campusNom); + whereConditions.push('c.Nom = @campusNom'); + queryRequest.input('campusNom', campusNom); } if (selectedService && selectedService !== 'all') { - whereConditions.push('s.Nom = ?'); - whereParams.push(selectedService); + whereConditions.push('s.Nom = @selectedService'); + queryRequest.input('selectedService', selectedService); } - query = ` - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, - CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, - CONCAT( - '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' - ), - ']' - ) AS detailsconges, - MAX(tc.CouleurHex) AS color, - dc.Statut AS statut, - s.Nom AS servicenom, - c.Nom AS campusnom, - so.Nom AS societenom, - dc.NombreJours AS nombrejoursouvres - 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 - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ${whereConditions.join(' AND ')} - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours - ORDER BY so.Nom, c.Nom, dc.DateDebut ASC - `; - params = whereParams; + const query = ` + SELECT + CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate, + CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate, + CONCAT(ca.prenom, ' ', ca.nom) AS employeename, + ( + SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom) + FROM (SELECT DISTINCT tc2.Nom + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes + ) AS type, + CONCAT( + '[', + STRING_AGG( + CONCAT( + '{"type":"', tc.Nom, + '","jours":', dct.NombreJours, + ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' + ), + ',' + ), + ']' + ) AS detailsconges, + MAX(tc.CouleurHex) AS color, + dc.Statut AS statut, + s.Nom AS servicenom, + c.Nom AS campusnom, + so.Nom AS societenom, + dc.NombreJours AS nombrejoursouvres + 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 + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + WHERE ${whereConditions.join(' AND ')} + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours + ORDER BY c.Nom, dc.DateDebut ASC + `; console.log(`🔍 Query finale WHERE:`, whereConditions.join(' AND ')); - console.log(`🔍 Params:`, whereParams); + + const leavesResult = await queryRequest.query(query); + const formattedLeaves = leavesResult.recordset.map(leave => ({ + ...leave + })); + + console.log(`✅ ${formattedLeaves.length} congés trouvés`); + + return res.json({ + success: true, + role: role, + leaves: formattedLeaves, + filters: filters + }); } - - // ======================================== - // CAS 3: COLLABORATEUR + // CAS 2: VALIDATEUR - Vue équipe via HierarchieValidationAD + // ⚠️ C'est ici que le validateur doit tomber ! // ======================================== - // Dans la route /getTeamLeaves, modifiez la section CAS 3: COLLABORATEUR - // ======================================== - // CAS 3: COLLABORATEUR - // ======================================== - // ======================================== - // CAS 3: COLLABORATEUR - // ======================================== - else if (role === 'collaborateur' || role === 'validateur' || role === 'apprenti') { - console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés"); + else if (role === 'validateur') { + console.log("CAS 2: Validateur - Vue de l'équipe via HierarchieValidationAD"); - const serviceNom = userInfo.serviceNom || 'Non défini'; - const campusNom = userInfo.campusNom || 'Non défini'; - const societeNom = userInfo.societeNom || 'Non défini'; + console.log(`📍 Validateur: Service=${serviceNom}, Campus=${campusNom}, Société=${societeNom}`); - console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); - - // ⭐ NOUVEAU : Si AUCUN filtre n'est envoyé (premier chargement), utiliser les valeurs par défaut - // Sinon, respecter EXACTEMENT ce que le frontend envoie (même "all") const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete; if (isFirstLoad) { - console.log('🎯 Premier chargement : initialisation avec service par défaut'); + console.log('🎯 Premier chargement validateur : initialisation avec valeurs par défaut'); selectedCampus = campusNom; selectedService = serviceNom; selectedSociete = societeNom; } - // Si le frontend envoie "all", on garde "all" (ne pas forcer les valeurs par défaut) - console.log(`📍 Filtres appliqués finaux: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); + console.log(`📍 Filtres appliqués: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); - // ⭐ Construire les listes de filtres disponibles - // 1️⃣ Sociétés disponibles (TOUTES) - const [societesList] = await conn.query(` - SELECT DISTINCT so.Nom - FROM Societe so - JOIN CollaborateurAD ca ON ca.SocieteId = so.Id - WHERE (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY so.Nom - `); - filters.societes = societesList.map(s => s.Nom); + // Sociétés disponibles + const societesRequest = pool.request(); + const societesResult = await societesRequest.query(` + SELECT DISTINCT so.Nom + FROM Societe so + JOIN CollaborateurAD ca ON ca.SocieteId = so.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + ORDER BY so.Nom + `); + filters.societes = societesResult.recordset.map(s => s.Nom); - // 2️⃣ Campus disponibles (filtrés par société si sélectionné) + // Campus disponibles let campusQuery = ` - SELECT DISTINCT c.Nom - FROM Campus c - JOIN CollaborateurAD ca ON ca.CampusId = c.Id - WHERE (ca.actif = 1 OR ca.actif IS NULL) - `; - let campusParams = []; + SELECT DISTINCT c.Nom + FROM Campus c + JOIN CollaborateurAD ca ON ca.CampusId = c.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; + let campusRequest = pool.request(); if (selectedSociete && selectedSociete !== 'all') { - campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`; - campusParams.push(selectedSociete); + campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = @selectedSociete)`; + campusRequest.input('selectedSociete', selectedSociete); } campusQuery += ` ORDER BY c.Nom`; - const [campusList] = await conn.query(campusQuery, campusParams); - filters.campus = campusList.map(c => c.Nom); + const campusResult = await campusRequest.query(campusQuery); + filters.campus = campusResult.recordset.map(c => c.Nom); - // 3️⃣ Services disponibles (filtrés par société + campus) + // Services disponibles let servicesQuery = ` - SELECT DISTINCT s.Nom - FROM Services s - JOIN CollaborateurAD ca ON ca.ServiceId = s.Id - WHERE (ca.actif = 1 OR ca.actif IS NULL) - `; - let servicesParams = []; + SELECT DISTINCT s.Nom + FROM Services s + JOIN CollaborateurAD ca ON ca.ServiceId = s.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; + let servicesRequest = pool.request(); if (selectedSociete && selectedSociete !== 'all') { - servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`; - servicesParams.push(selectedSociete); + servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = @selectedSociete)`; + servicesRequest.input('selectedSociete', selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { - servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = ? LIMIT 1)`; - servicesParams.push(selectedCampus); + servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = @selectedCampus)`; + servicesRequest.input('selectedCampus', selectedCampus); } servicesQuery += ` ORDER BY s.Nom`; - const [servicesList] = await conn.query(servicesQuery, servicesParams); - filters.services = servicesList.map(s => s.Nom); + const servicesResult = await servicesRequest.query(servicesQuery); + filters.services = servicesResult.recordset.map(s => s.Nom); - // ⭐ Envoyer les valeurs par défaut au frontend (pour initialisation) filters.defaultCampus = campusNom; filters.defaultService = serviceNom; filters.defaultSociete = societeNom; - // ⭐ LISTE DES EMPLOYÉS (avec filtres conditionnels) + // ⭐ LISTE DES EMPLOYÉS - UNIQUEMENT CEUX DE L'ÉQUIPE DU VALIDATEUR let employeesQuery = ` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - c.Nom AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE (ca.actif = 1 OR ca.actif IS NULL) - `; + SELECT + CONCAT(ca.prenom, ' ', ca.nom) AS fullname, + c.Nom AS campusnom, + so.Nom AS societenom, + s.Nom AS servicenom + FROM CollaborateurAD ca + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + JOIN HierarchieValidationAD h ON ca.id = h.CollaborateurId + WHERE h.SuperieurId = @userId + AND (ca.actif = 1 OR ca.actif IS NULL) + `; - let employeesParams = []; + let employeesRequest = pool.request(); + employeesRequest.input('userId', userInfo.id); let employeesConditions = []; - // ⭐ N'ajouter les filtres QUE si différents de "all" if (selectedSociete && selectedSociete !== 'all') { - employeesConditions.push('so.Nom = ?'); - employeesParams.push(selectedSociete); + employeesConditions.push('so.Nom = @selectedSociete'); + employeesRequest.input('selectedSociete', selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { - employeesConditions.push('c.Nom = ?'); - employeesParams.push(selectedCampus); + employeesConditions.push('c.Nom = @selectedCampus'); + employeesRequest.input('selectedCampus', selectedCampus); } if (selectedService && selectedService !== 'all') { - employeesConditions.push('s.Nom = ?'); - employeesParams.push(selectedService); + employeesConditions.push('s.Nom = @selectedService'); + employeesRequest.input('selectedService', selectedService); } if (employeesConditions.length > 0) { @@ -5300,9 +6176,221 @@ app.get('/api/getTeamLeaves', async (req, res) => { employeesQuery += ` ORDER BY s.Nom, ca.prenom, ca.nom`; - const [employeesList] = await conn.query(employeesQuery, employeesParams); + const employeesResult = await employeesRequest.query(employeesQuery); - filters.employees = employeesList.map(emp => ({ + filters.employees = employeesResult.recordset.map(emp => ({ + name: emp.fullname, + campus: emp.campusnom, + societe: emp.societenom, + service: emp.servicenom + })); + + console.log(`👥 Équipe du validateur: ${filters.employees.length} personnes`); + + // ⭐ QUERY DES CONGÉS - UNIQUEMENT LES COLLABORATEURS DU VALIDATEUR + let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') + AND ca.id IN ( + SELECT CollaborateurId FROM HierarchieValidationAD WHERE SuperieurId = @userId + )`; + + let queryRequest = pool.request(); + queryRequest.input('userId', userInfo.id); + let congesConditions = []; + + if (selectedSociete && selectedSociete !== 'all') { + congesConditions.push('so.Nom = @selectedSociete'); + queryRequest.input('selectedSociete', selectedSociete); + } + + if (selectedCampus && selectedCampus !== 'all') { + congesConditions.push('c.Nom = @selectedCampus'); + queryRequest.input('selectedCampus', selectedCampus); + } + + if (selectedService && selectedService !== 'all') { + congesConditions.push('s.Nom = @selectedService'); + queryRequest.input('selectedService', selectedService); + } + + if (congesConditions.length > 0) { + queryConditions += ` AND ${congesConditions.join(' AND ')}`; + } + + const query = ` + SELECT + CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate, + CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate, + CONCAT(ca.prenom, ' ', ca.nom) AS employeename, + ( + SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom) + FROM (SELECT DISTINCT tc2.Nom + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes + ) AS type, + CONCAT( + '[', + STRING_AGG( + CONCAT( + '{"type":"', tc.Nom, + '","jours":', dct.NombreJours, + ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' + ), + ',' + ), + ']' + ) AS detailsconges, + MAX(tc.CouleurHex) AS color, + dc.Statut AS statut, + s.Nom AS servicenom, + c.Nom AS campusnom, + so.Nom AS societenom, + dc.NombreJours AS nombrejoursouvres + 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 + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + ${queryConditions} + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours + ORDER BY s.Nom, dc.DateDebut ASC + `; + + console.log(`🔍 Query WHERE final validateur:`, queryConditions); + + const leavesResult = await queryRequest.query(query); + const formattedLeaves = leavesResult.recordset.map(leave => ({ + ...leave + })); + + console.log(`✅ ${formattedLeaves.length} congés trouvés pour l'équipe du validateur`); + + return res.json({ + success: true, + role: role, + leaves: formattedLeaves, + filters: filters + }); + } + + // ======================================== + // CAS 3: COLLABORATEUR / APPRENTI + // ======================================== + else if (role === 'collaborateur' || role === 'apprenti') { + console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés"); + + console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); + + const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete; + + if (isFirstLoad) { + console.log('🎯 Premier chargement : initialisation avec service par défaut'); + selectedCampus = campusNom; + selectedService = serviceNom; + selectedSociete = societeNom; + } + + console.log(`📍 Filtres appliqués finaux: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); + + // Sociétés disponibles + const societesRequest = pool.request(); + const societesResult = await societesRequest.query(` + SELECT DISTINCT so.Nom + FROM Societe so + JOIN CollaborateurAD ca ON ca.SocieteId = so.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + ORDER BY so.Nom + `); + filters.societes = societesResult.recordset.map(s => s.Nom); + + // Campus disponibles + let campusQuery = ` + SELECT DISTINCT c.Nom + FROM Campus c + JOIN CollaborateurAD ca ON ca.CampusId = c.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; + let campusRequest = pool.request(); + + if (selectedSociete && selectedSociete !== 'all') { + campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = @selectedSociete)`; + campusRequest.input('selectedSociete', selectedSociete); + } + + campusQuery += ` ORDER BY c.Nom`; + const campusResult = await campusRequest.query(campusQuery); + filters.campus = campusResult.recordset.map(c => c.Nom); + + // Services disponibles + let servicesQuery = ` + SELECT DISTINCT s.Nom + FROM Services s + JOIN CollaborateurAD ca ON ca.ServiceId = s.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; + let servicesRequest = pool.request(); + + if (selectedSociete && selectedSociete !== 'all') { + servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = @selectedSociete)`; + servicesRequest.input('selectedSociete', selectedSociete); + } + + if (selectedCampus && selectedCampus !== 'all') { + servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = @selectedCampus)`; + servicesRequest.input('selectedCampus', selectedCampus); + } + + servicesQuery += ` ORDER BY s.Nom`; + const servicesResult = await servicesRequest.query(servicesQuery); + filters.services = servicesResult.recordset.map(s => s.Nom); + + filters.defaultCampus = campusNom; + filters.defaultService = serviceNom; + filters.defaultSociete = societeNom; + + // ⭐ LISTE DES EMPLOYÉS + let employeesQuery = ` + SELECT + CONCAT(ca.prenom, ' ', ca.nom) AS fullname, + c.Nom AS campusnom, + so.Nom AS societenom, + s.Nom AS servicenom + FROM CollaborateurAD ca + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; + + let employeesRequest = pool.request(); + let employeesConditions = []; + + if (selectedSociete && selectedSociete !== 'all') { + employeesConditions.push('so.Nom = @selectedSociete'); + employeesRequest.input('selectedSociete', selectedSociete); + } + + if (selectedCampus && selectedCampus !== 'all') { + employeesConditions.push('c.Nom = @selectedCampus'); + employeesRequest.input('selectedCampus', selectedCampus); + } + + if (selectedService && selectedService !== 'all') { + employeesConditions.push('s.Nom = @selectedService'); + employeesRequest.input('selectedService', selectedService); + } + + if (employeesConditions.length > 0) { + employeesQuery += ` AND ${employeesConditions.join(' AND ')}`; + } + + employeesQuery += ` ORDER BY s.Nom, ca.prenom, ca.nom`; + + const employeesResult = await employeesRequest.query(employeesQuery); + + filters.employees = employeesResult.recordset.map(emp => ({ name: emp.fullname, campus: emp.campusnom, societe: emp.societenom, @@ -5311,94 +6399,114 @@ app.get('/api/getTeamLeaves', async (req, res) => { console.log(`👥 Employés trouvés: ${filters.employees.length}`); - // ⭐ QUERY DES CONGÉS (avec mêmes filtres conditionnels) + // QUERY DES CONGÉS let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`; - params = []; + let queryRequest = pool.request(); let congesConditions = []; - // ⭐ N'ajouter les filtres QUE si différents de "all" if (selectedSociete && selectedSociete !== 'all') { - congesConditions.push('so.Nom = ?'); - params.push(selectedSociete); + congesConditions.push('so.Nom = @selectedSociete'); + queryRequest.input('selectedSociete', selectedSociete); } if (selectedCampus && selectedCampus !== 'all') { - congesConditions.push('c.Nom = ?'); - params.push(selectedCampus); + congesConditions.push('c.Nom = @selectedCampus'); + queryRequest.input('selectedCampus', selectedCampus); } if (selectedService && selectedService !== 'all') { - congesConditions.push('s.Nom = ?'); - params.push(selectedService); + congesConditions.push('s.Nom = @selectedService'); + queryRequest.input('selectedService', selectedService); } if (congesConditions.length > 0) { queryConditions += ` AND ${congesConditions.join(' AND ')}`; } - query = ` - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, - CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, - CONCAT( - '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' - ), - ']' - ) AS detailsconges, - MAX(tc.CouleurHex) AS color, - dc.Statut AS statut, - s.Nom AS servicenom, - c.Nom AS campusnom, - so.Nom AS societenom, - dc.NombreJours AS nombrejoursouvres - 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 - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - ${queryConditions} - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours - ORDER BY s.Nom, dc.DateDebut ASC - `; + const query = ` + SELECT + CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate, + CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate, + CONCAT(ca.prenom, ' ', ca.nom) AS employeename, + ( + SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom) + FROM (SELECT DISTINCT tc2.Nom + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes + ) AS type, + CONCAT( + '[', + STRING_AGG( + CONCAT( + '{"type":"', tc.Nom, + '","jours":', dct.NombreJours, + ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' + ), + ',' + ), + ']' + ) AS detailsconges, + MAX(tc.CouleurHex) AS color, + dc.Statut AS statut, + s.Nom AS servicenom, + c.Nom AS campusnom, + so.Nom AS societenom, + dc.NombreJours AS nombrejoursouvres + 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 + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + ${queryConditions} + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours + ORDER BY s.Nom, dc.DateDebut ASC + `; console.log(`🔍 Query WHERE final:`, queryConditions); - console.log(`🔍 Params:`, params); + + const leavesResult = await queryRequest.query(query); + const formattedLeaves = leavesResult.recordset.map(leave => ({ + ...leave + })); + + console.log(`✅ ${formattedLeaves.length} congés trouvés`); + + return res.json({ + success: true, + role: role, + leaves: formattedLeaves, + filters: filters + }); } - - // ======================================== - // CAS 4: AUTRES RÔLES + // CAS 4: AUTRES RÔLES (Fallback) // ======================================== else { - console.log("CAS 4: Autres rôles"); + console.log("CAS 4: Autres rôles - Fallback service/campus"); if (!serviceId) { - conn.release(); return res.json({ success: false, message: 'ServiceId manquant' }); } - const [checkService] = await conn.query(`SELECT Nom FROM Services WHERE Id = ?`, [serviceId]); - const serviceNom = checkService.length > 0 ? checkService[0].Nom : "Inconnu"; - const isAdminFinancier = serviceNom === "Administratif & Financier"; + const checkServiceRequest = pool.request(); + checkServiceRequest.input('serviceId', serviceId); + const checkServiceResult = await checkServiceRequest.query(`SELECT Nom FROM Services WHERE Id = @serviceId`); + const serviceNomCheck = checkServiceResult.recordset.length > 0 ? checkServiceResult.recordset[0].Nom : "Inconnu"; + const isAdminFinancier = serviceNomCheck === "Administratif & Financier"; if (isAdminFinancier) { - const [employeesList] = await conn.query(` - SELECT DISTINCT + // Service multi-campus + const employeesRequest = pool.request(); + employeesRequest.input('serviceId', serviceId); + const employeesResult = await employeesRequest.query(` + SELECT CONCAT(ca.prenom, ' ', ca.nom) AS fullname, c.Nom AS campusnom, so.Nom AS societenom, @@ -5407,33 +6515,42 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.ServiceId = ? + WHERE ca.ServiceId = @serviceId AND (ca.actif = 1 OR ca.actif IS NULL) ORDER BY ca.prenom, ca.nom - `, [serviceId]); + `); - filters.employees = employeesList.map(e => ({ + filters.employees = employeesResult.recordset.map(e => ({ name: e.fullname, campus: e.campusnom, societe: e.societenom, service: e.servicenom })); - query = ` + const queryRequest = pool.request(); + queryRequest.input('serviceId', serviceId); + + const query = ` SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, + CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate, + CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate, CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, + ( + SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom) + FROM (SELECT DISTINCT tc2.Nom + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes + ) AS type, CONCAT( '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' + STRING_AGG( + CONCAT( + '{"type":"', tc.Nom, + '","jours":', dct.NombreJours, + ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' + ), + ',' ), ']' ) AS detailsconges, @@ -5451,15 +6568,32 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - AND ca.ServiceId = ? + AND ca.ServiceId = @serviceId GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours ORDER BY c.Nom, dc.DateDebut ASC `; - params = [serviceId]; + + const leavesResult = await queryRequest.query(query); + const formattedLeaves = leavesResult.recordset.map(leave => ({ + ...leave + })); + + console.log(`✅ ${formattedLeaves.length} congés trouvés`); + + return res.json({ + success: true, + role: role, + leaves: formattedLeaves, + filters: filters + }); } else { - const [employeesList] = await conn.query(` - SELECT DISTINCT + // Service + Campus + const employeesRequest = pool.request(); + employeesRequest.input('serviceId', serviceId); + employeesRequest.input('campusId', campusId); + const employeesResult = await employeesRequest.query(` + SELECT CONCAT(ca.prenom, ' ', ca.nom) AS fullname, c.Nom AS campusnom, so.Nom AS societenom, @@ -5468,34 +6602,44 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN Services s ON ca.ServiceId = s.Id JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.ServiceId = ? - AND ca.CampusId = ? + WHERE ca.ServiceId = @serviceId + AND ca.CampusId = @campusId AND (ca.actif = 1 OR ca.actif IS NULL) ORDER BY ca.prenom, ca.nom - `, [serviceId, campusId]); + `); - filters.employees = employeesList.map(e => ({ + filters.employees = employeesResult.recordset.map(e => ({ name: e.fullname, campus: e.campusnom, societe: e.societenom, service: e.servicenom })); - query = ` + const queryRequest = pool.request(); + queryRequest.input('serviceId', serviceId); + queryRequest.input('campusId', campusId); + + const query = ` SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, + CONVERT(VARCHAR(10), dc.DateDebut, 23) AS startdate, + CONVERT(VARCHAR(10), dc.DateFin, 23) AS enddate, CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, + ( + SELECT STRING_AGG(Nom, ', ') WITHIN GROUP (ORDER BY Nom) + FROM (SELECT DISTINCT tc2.Nom + FROM DemandeCongeType dct2 + JOIN TypeConge tc2 ON dct2.TypeCongeId = tc2.Id + WHERE dct2.DemandeCongeId = dc.Id) AS DistinctTypes + ) AS type, CONCAT( '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' + STRING_AGG( + CONCAT( + '{"type":"', tc.Nom, + '","jours":', dct.NombreJours, + ',"periode":"', COALESCE(dct.PeriodeJournee, 'Journée entière'), '"}' + ), + ',' ), ']' ) AS detailsconges, @@ -5513,67 +6657,28 @@ app.get('/api/getTeamLeaves', async (req, res) => { JOIN Campus c ON ca.CampusId = c.Id JOIN Societe so ON ca.SocieteId = so.Id WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - AND ca.ServiceId = ? - AND ca.CampusId = ? + AND ca.ServiceId = @serviceId + AND ca.CampusId = @campusId GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours ORDER BY c.Nom, dc.DateDebut ASC `; - params = [serviceId, campusId]; + + const leavesResult = await queryRequest.query(query); + const formattedLeaves = leavesResult.recordset.map(leave => ({ + ...leave + })); + + console.log(`✅ ${formattedLeaves.length} congés trouvés`); + + return res.json({ + success: true, + role: role, + leaves: formattedLeaves, + filters: filters + }); } } - const [leavesRows] = await conn.query(query, params); - - const formattedLeaves = leavesRows.map(leave => ({ - ...leave - })); - - console.log(`✅ ${formattedLeaves.length} congés trouvés`); - - if (formattedLeaves.length === 0 && (role === 'collaborateur' || role === 'collaboratrice')) { - console.log('🔍 DEBUG: Aucun congé trouvé, vérification...'); - - const [debugLeaves] = await conn.query(` - SELECT COUNT(*) as total - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - JOIN Services s ON ca.ServiceId = s.Id - WHERE s.Nom = ? - AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - `, [userInfo.serviceNom]); - - console.log(`🔍 Total congés dans le service "${userInfo.serviceNom}":`, debugLeaves[0].total); - - const [debugCollabs] = await conn.query(` - SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.ServiceId, s.Nom as ServiceNom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - WHERE s.Nom = ? - `, [userInfo.serviceNom]); - - console.log(`🔍 Collaborateurs dans "${userInfo.serviceNom}":`, debugCollabs); - } - - console.log(`✅ Filtres:`, { - campus: filters.campus?.length || 0, - societes: filters.societes?.length || 0, - services: filters.services?.length || 0, - employees: filters.employees?.length || 0 - }); - - if (formattedLeaves.length > 0) { - console.log('📝 Exemple de congé formaté:', formattedLeaves[0]); - } - - conn.release(); - - res.json({ - success: true, - role: role, - leaves: formattedLeaves, - filters: filters - }); - } catch (error) { console.error("❌ Erreur getTeamLeaves:", error); res.status(500).json({ @@ -5584,198 +6689,383 @@ app.get('/api/getTeamLeaves', async (req, res) => { } }); +// ======================================== +// SYNCHRONISATION ENTRA ID CORRIGÉE +// À remplacer dans server.js à partir de la ligne ~3700 +// ======================================== - -// ================================================ -// ROUTE DE SYNCHRONISATION INITIALE (CORRIGÉE) -// ================================================ - - -// ✅ APRÈS - Version CORRIGÉE -// ✅ VERSION COMPLÈTE ET CORRIGÉE app.post('/api/initial-sync', async (req, res) => { + let errorCount = 0; + const syncResults = { + processed: 0, + inserted: 0, + updated: 0, + deactivated: 0, + errors: [] + }; + try { - // 1. Obtenir le token Admin + console.log('\n🔄 === DÉBUT SYNCHRONISATION ENTRA ID ==='); + + // 1️⃣ Obtenir le token Admin const accessToken = await getGraphToken(); - if (!accessToken) return res.json({ success: false, message: 'Impossible obtenir token Microsoft Graph' }); + if (!accessToken) { + return res.json({ + success: false, + message: '❌ Impossible d\'obtenir le token Microsoft Graph' + }); + } + console.log('✅ Token Microsoft Graph obtenu'); // ============================================================================= // SCÉNARIO 1 : Synchronisation unitaire (Un seul utilisateur spécifique) // ============================================================================= if (req.body.userPrincipalName || req.body.mail) { - const userEmail = req.body.mail || req.body.userPrincipalName; + const userEmail = (req.body.mail || req.body.userPrincipalName).toLowerCase().trim(); const entraUserId = req.body.id; - console.log(`🔄 Synchronisation utilisateur unique : ${userEmail}`); + console.log(`\n🔄 Synchronisation utilisateur unique : ${userEmail}`); - // Insertion ou Mise à jour - await pool.query(` - INSERT INTO CollaborateurAD - (entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW(), '37h') - ON DUPLICATE KEY UPDATE - prenom=VALUES(prenom), - nom=VALUES(nom), - email=VALUES(email), - service=VALUES(service), - description=VALUES(description), - Actif=1, -- On réactive si l'utilisateur revient - entraUserId=VALUES(entraUserId) - `, [ - entraUserId, - req.body.givenName || 'Prénom', - req.body.surname || 'Nom', - userEmail, - req.body.department || '', - req.body.jobTitle || null, - 'Collaborateur', - 1 // SocieteId par défaut (ex: ENSUP) - ]); - - // Récupération des données fraîches pour renvoyer au front - const [userRows] = await pool.query(` - SELECT - ca.id as localUserId, - ca.entraUserId, - ca.prenom, - ca.nom, - ca.email, - ca.role, - s.Nom as service, - ca.TypeContrat as typeContrat, - ca.DateEntree as dateEntree, - ca.description, - ca.CampusId, - ca.SocieteId, - so.Nom as societe_nom - FROM CollaborateurAD ca - LEFT JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.email = ? - `, [userEmail]); - - if (userRows.length === 0) { - return res.json({ success: false, message: 'Erreur : Utilisateur synchronisé mais introuvable en base.' }); + // ⭐ VALIDATION : Email requis + if (!userEmail || userEmail === '') { + return res.json({ + success: false, + message: '❌ Email utilisateur manquant ou invalide' + }); } - const userData = userRows[0]; - console.log(`✅ Utilisateur synchronisé avec succès : ${userData.email}`); + // ⭐ VALIDATION : Format email + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(userEmail)) { + return res.json({ + success: false, + message: `❌ Format d'email invalide : ${userEmail}` + }); + } - return res.json({ - success: true, - message: 'Utilisateur synchronisé', - localUserId: userData.localUserId, - role: userData.role, - service: userData.service, - typeContrat: userData.typeContrat, - dateEntree: userData.dateEntree, - societeId: userData.SocieteId, - user: userData - }); + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + + // Vérifier si l'utilisateur existe déjà + const [existing] = await conn.query( + 'SELECT id, email, Actif FROM CollaborateurAD WHERE LOWER(email) = ?', + [userEmail] + ); + + if (existing.length > 0) { + // MISE À JOUR + await conn.query(` + UPDATE CollaborateurAD + SET + entraUserId = ?, + prenom = ?, + nom = ?, + service = ?, + description = ?, + Actif = 1 + WHERE LOWER(email) = ? + `, [ + entraUserId || existing[0].entraUserId, + req.body.givenName || 'Prénom', + req.body.surname || 'Nom', + req.body.department || '', + req.body.jobTitle || null, + userEmail + ]); + + console.log(` ✅ Utilisateur mis à jour : ${userEmail}`); + syncResults.updated++; + + } else { + // INSERTION + await conn.query(` + INSERT INTO CollaborateurAD + (entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat) + VALUES (?, ?, ?, ?, ?, ?, 'Collaborateur', 1, 1, GETDATE(), '37h') + `, [ + entraUserId, + req.body.givenName || 'Prénom', + req.body.surname || 'Nom', + userEmail, + req.body.department || '', + req.body.jobTitle || null + ]); + + console.log(` ✅ Nouvel utilisateur créé : ${userEmail}`); + syncResults.inserted++; + } + + // Récupération des données fraîches + const [userRows] = await conn.query(` + SELECT + ca.id as localUserId, + ca.entraUserId, + ca.prenom, + ca.nom, + ca.email, + ca.role, + s.Nom as service, + ca.TypeContrat as typeContrat, + ca.DateEntree as dateEntree, + ca.description, + ca.CampusId, + ca.SocieteId, + so.Nom as societe_nom + FROM CollaborateurAD ca + LEFT JOIN Services s ON ca.ServiceId = s.Id + LEFT JOIN Societe so ON ca.SocieteId = so.Id + WHERE LOWER(ca.email) = ? + `, [userEmail]); + + await conn.commit(); + + if (userRows.length === 0) { + await conn.rollback(); + throw new Error('Utilisateur synchronisé mais introuvable en base'); + } + + const userData = userRows[0]; + console.log(`✅ Synchronisation réussie : ${userData.email}`); + + return res.json({ + success: true, + message: 'Utilisateur synchronisé avec succès', + localUserId: userData.localUserId, + role: userData.role, + service: userData.service, + typeContrat: userData.typeContrat, + dateEntree: userData.dateEntree, + societeId: userData.SocieteId, + user: userData + }); + + } catch (syncError) { + await conn.rollback(); + console.error('❌ Erreur sync unitaire:', syncError); + return res.json({ + success: false, + message: `❌ Erreur synchronisation: ${syncError.message}` + }); + } finally { + conn.release(); + } } // ============================================================================= // SCÉNARIO 2 : Full Sync (Tous les membres du groupe Azure) // ============================================================================= - console.log('🔄 Démarrage Full Sync des membres du groupe...'); + console.log('\n🔄 === FULL SYNC - Tous les membres du groupe ==='); - // A. Récupérer le nom du groupe (pour info) + // A. Récupérer le nom du groupe const groupResponse = await axios.get( `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, { headers: { Authorization: `Bearer ${accessToken}` } } ); const groupName = groupResponse.data.displayName; + console.log(`📋 Groupe : ${groupName}`); - // B. Récupérer TOUS les membres avec pagination (boucle while) + // B. Récupérer TOUS les membres avec pagination let allAzureMembers = []; - let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle&$top=999`; + let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle,accountEnabled&$top=999`; + console.log('📥 Récupération des membres...'); while (nextLink) { - const membersResponse = await axios.get(nextLink, { headers: { Authorization: `Bearer ${accessToken}` } }); + const membersResponse = await axios.get(nextLink, { + headers: { Authorization: `Bearer ${accessToken}` } + }); allAzureMembers = allAzureMembers.concat(membersResponse.data.value); - nextLink = membersResponse.data['@odata.nextLink']; // Lien vers la page suivante (si existe) - } + nextLink = membersResponse.data['@odata.nextLink']; - console.log(`📋 ${allAzureMembers.length} utilisateurs trouvés dans le groupe Azure "${groupName}".`); - - const azureEmails = new Set(); // Liste blanche des emails actifs - let usersInserted = 0; - - // C. Traitement de chaque membre Azure - for (const m of allAzureMembers) { - if (!m.mail) continue; // Ignorer ceux sans email - - azureEmails.add(m.mail.toLowerCase()); - - await pool.query(` - INSERT INTO CollaborateurAD ( - entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW(), '37h') - ON DUPLICATE KEY UPDATE - prenom=VALUES(prenom), - nom=VALUES(nom), - service=VALUES(service), - entraUserId=VALUES(entraUserId), - Actif = 1 -- On s'assure qu'il est actif - `, [ - m.id, - m.givenName || 'Prénom', - m.surname || 'Nom', - m.mail, - m.department || '', - m.jobTitle || null, - 'Collaborateur', - 1, // SocieteId par défaut - ]); - - usersInserted++; - } - - // D. Désactivation des fantômes (Ceux en base locale mais ABSENTS d'Azure) - const activeEmailsArray = Array.from(azureEmails); - let deactivatedCount = 0; - - if (activeEmailsArray.length > 0) { - // Création dynamique des placeholders (?, ?, ?) - const placeholders = activeEmailsArray.map(() => '?').join(','); - - // On désactive tous ceux qui NE SONT PAS dans la liste activeEmailsArray - const [resultDeactivate] = await pool.query(` - UPDATE CollaborateurAD - SET Actif = 0 - WHERE Email IS NOT NULL - AND LOWER(Email) NOT IN (${placeholders}) - AND Actif = 1 -- On ne modifie que ceux qui étaient actifs - `, activeEmailsArray); - - deactivatedCount = resultDeactivate.affectedRows; - } - - console.log(`✅ Full sync terminée avec succès.`); - console.log(` - ${usersInserted} utilisateurs synchronisés/actifs`); - console.log(` - ${deactivatedCount} utilisateurs désactivés (partis)`); - - res.json({ - success: true, - message: 'Synchronisation globale terminée', - groupe_sync: groupName, - stats: { - total_azure: allAzureMembers.length, - processed: usersInserted, - deactivated: deactivatedCount + if (nextLink) { + console.log(` 📄 ${allAzureMembers.length} membres récupérés... (suite)`); } + } + + console.log(`✅ ${allAzureMembers.length} membres trouvés dans Entra ID`); + + // C. Filtrer et valider les emails + const validMembers = allAzureMembers.filter(m => { + // Ignorer si pas d'email + if (!m.mail || m.mail.trim() === '') { + console.log(` ⚠️ Ignoré (pas d'email) : ${m.givenName} ${m.surname} (${m.id})`); + return false; + } + + // Ignorer si compte désactivé + if (m.accountEnabled === false) { + console.log(` ⚠️ Ignoré (compte désactivé) : ${m.mail}`); + return false; + } + + // Valider format email + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(m.mail)) { + console.log(` ⚠️ Ignoré (format email invalide) : ${m.mail}`); + return false; + } + + return true; }); + console.log(`✅ ${validMembers.length} membres valides à synchroniser`); + + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + + // D. Construire la liste des emails valides + const azureEmails = new Set(); + validMembers.forEach(m => { + azureEmails.add(m.mail.toLowerCase().trim()); + }); + + console.log('\n📝 Traitement des utilisateurs...'); + + // E. Traitement de chaque membre valide + for (const m of validMembers) { + try { + const emailClean = m.mail.toLowerCase().trim(); + syncResults.processed++; + + // Vérifier si l'utilisateur existe déjà + const [existing] = await conn.query( + 'SELECT id, email, entraUserId, Actif FROM CollaborateurAD WHERE LOWER(email) = ?', + [emailClean] + ); + + if (existing.length > 0) { + // MISE À JOUR si changements + await conn.query(` + UPDATE CollaborateurAD + SET + entraUserId = ?, + prenom = ?, + nom = ?, + service = ?, + description = ?, + Actif = 1 + WHERE LOWER(email) = ? + `, [ + m.id, + m.givenName || existing[0].prenom || 'Prénom', + m.surname || existing[0].nom || 'Nom', + m.department || '', + m.jobTitle || null, + emailClean + ]); + + syncResults.updated++; + console.log(` ✓ Mis à jour : ${emailClean}`); + + } else { + // INSERTION nouveau + await conn.query(` + INSERT INTO CollaborateurAD + (entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat) + VALUES (?, ?, ?, ?, ?, ?, 'Collaborateur', 1, 1, GETDATE(), '37h') + `, [ + m.id, + m.givenName || 'Prénom', + m.surname || 'Nom', + emailClean, + m.department || '', + m.jobTitle || null + ]); + + syncResults.inserted++; + console.log(` ✓ Créé : ${emailClean}`); + } + + } catch (userError) { + errorCount++; + syncResults.errors.push({ + email: m.mail, + error: userError.message + }); + console.error(` ❌ Erreur ${m.mail}:`, userError.message); + // Continuer avec les autres + } + } + + // F. ⭐ DÉSACTIVATION SÉCURISÉE + console.log('\n🔍 Désactivation des comptes obsolètes...'); + + if (azureEmails.size > 0) { + const activeEmailsArray = Array.from(azureEmails); + const placeholders = activeEmailsArray.map(() => '?').join(','); + + // ⭐ REQUÊTE PROTÉGÉE : Ne désactive que les comptes qui : + // 1. Ont un email valide + // 2. Ne sont pas dans Entra ID + // 3. Ne sont pas RH/Admin/President + // 4. Sont actuellement actifs + const [resultDeactivate] = await conn.query(` + UPDATE CollaborateurAD + SET Actif = 0 + WHERE + Email IS NOT NULL + AND Email != '' + AND Email NOT LIKE '%@noemail.local' + AND LOWER(Email) NOT IN (${placeholders}) + AND Actif = 1 + AND role NOT IN ('RH', 'Admin', 'President') + `, activeEmailsArray); + + syncResults.deactivated = resultDeactivate.affectedRows; + console.log(` ✓ ${syncResults.deactivated} compte(s) désactivé(s)`); + } + + await conn.commit(); + + // G. Logging final + console.log('\n📊 === RÉSUMÉ SYNCHRONISATION ==='); + console.log(` Groupe Azure: ${groupName}`); + console.log(` Total membres Entra: ${allAzureMembers.length}`); + console.log(` Membres valides: ${validMembers.length}`); + console.log(` Traités: ${syncResults.processed}`); + console.log(` Créés: ${syncResults.inserted}`); + console.log(` Mis à jour: ${syncResults.updated}`); + console.log(` Désactivés: ${syncResults.deactivated}`); + console.log(` Erreurs: ${errorCount}`); + + res.json({ + success: true, + message: 'Synchronisation globale terminée', + groupe_sync: groupName, + stats: { + total_azure: allAzureMembers.length, + membres_valides: validMembers.length, + processed: syncResults.processed, + inserted: syncResults.inserted, + updated: syncResults.updated, + deactivated: syncResults.deactivated, + errors: errorCount, + error_details: syncResults.errors.length > 0 ? syncResults.errors : undefined + } + }); + + } catch (error) { + await conn.rollback(); + throw error; + } finally { + conn.release(); + } + } catch (error) { - console.error('❌ Erreur critique lors de la synchronisation:', error); + console.error('\n❌ === ERREUR CRITIQUE SYNCHRONISATION ==='); + console.error('Message:', error.message); + console.error('Stack:', error.stack); + res.status(500).json({ success: false, message: 'Erreur lors de la synchronisation', - error: error.message + error: error.message, + stats: syncResults }); } }); - +'' // ======================================== // NOUVELLES ROUTES ADMINISTRATION RTT @@ -5912,8 +7202,9 @@ app.post('/api/updateConfigurationRTT', async (req, res) => { app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, res) => { let connection; try { - console.log('📥 Body reçu:', req.body); - console.log('📎 Fichiers reçus:', req.files); + console.log('\n📥 === MODIFICATION DEMANDE ==='); + console.log('Body reçu:', req.body); + console.log('Fichiers reçus:', req.files?.length || 0); const { requestId, @@ -5928,8 +7219,35 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, accessToken } = req.body; + // ⭐ PARSER LA RÉPARTITION (CRITIQUE) + let Repartition; + try { + Repartition = JSON.parse(req.body.Repartition || '[]'); + console.log('📊 Répartition parsée:', JSON.stringify(Repartition, null, 2)); + } catch (parseError) { + console.error('❌ Erreur parsing Repartition:', parseError); + if (req.files) { + req.files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + }); + } + return res.status(400).json({ + success: false, + message: 'Erreur de format de la répartition' + }); + } + // Validation if (!requestId || !leaveType || !startDate || !endDate || !businessDays || !userId) { + if (req.files) { + req.files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + }); + } return res.status(400).json({ success: false, message: '❌ Données manquantes' @@ -5939,7 +7257,8 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, connection = await pool.getConnection(); await connection.beginTransaction(); - console.log('\n✏️ === MODIFICATION DEMANDE ==='); + const uploadedFiles = req.files || []; + console.log(`Demande ID: ${requestId}, User ID: ${userId}`); // 1️⃣ RÉCUPÉRER LA DEMANDE ORIGINALE @@ -5950,6 +7269,13 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, if (originalRequest.length === 0) { await connection.rollback(); + if (req.files) { + req.files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + }); + } return res.status(404).json({ success: false, message: '❌ Demande introuvable ou non autorisée' @@ -5961,6 +7287,57 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, console.log(`📋 Demande originale: Statut=${oldStatus}`); + // ⭐ VALIDATION : Si arrêt maladie, il faut au moins 1 fichier + const hasABS = Repartition.some(r => r.TypeConge === 'ABS'); + if (hasABS && uploadedFiles.length === 0) { + await connection.rollback(); + return res.json({ + success: false, + message: 'Un justificatif médical est obligatoire pour un arrêt maladie' + }); + } + + // ⭐ VALIDATION DE LA RÉPARTITION + console.log('📊 Validation répartition...'); + console.log('Nombre de jours total:', businessDays); + console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2)); + + // Ne compter que CP, RTT ET RÉCUP dans la répartition + const sommeRepartition = Repartition.reduce((sum, r) => { + if (r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup') { + return sum + parseFloat(r.NombreJours || 0); + } + return sum; + }, 0); + + console.log('Somme répartition CP+RTT+Récup:', sommeRepartition.toFixed(2)); + + // Validation : La somme doit correspondre au total + const hasCountableLeave = Repartition.some(r => + r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup' + ); + + if (hasCountableLeave && Math.abs(sommeRepartition - businessDays) > 0.01) { + console.error('❌ ERREUR : Répartition incohérente !'); + + if (req.files) { + req.files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + }); + } + + await connection.rollback(); + + return res.json({ + success: false, + message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${businessDays}j)` + }); + } + + console.log('✅ Validation répartition OK'); + // 2️⃣ REMBOURSER L'ANCIENNE DEMANDE (via DeductionDetails) let restorationStats = { count: 0, details: [] }; @@ -5982,7 +7359,7 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, if (compteur.length > 0) { const newSolde = parseFloat(compteur[0].Solde) + parseFloat(d.JoursUtilises); await connection.query( - 'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?', + 'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', [newSolde, compteur[0].Id] ); restorationStats.count++; @@ -5997,7 +7374,7 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, } } - // 3️⃣ METTRE À JOUR LA DEMANDE + // 3️⃣ METTRE À JOUR LA DEMANDE (⭐ SQL SERVER : GETDATE + FORMAT) console.log('📝 Mise à jour de la demande...'); // Si elle était validée, on la repasse en "En attente" @@ -6011,155 +7388,204 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, Commentaire = ?, NombreJours = ?, Statut = ?, - DateValidation = NOW(), - CommentaireValidation = CONCAT( - COALESCE(CommentaireValidation, ''), - '\n[Modifiée le ', - DATE_FORMAT(NOW(), '%d/%m/%Y à %H:%i'), + DateValidation = GETDATE(), + CommentaireValidation = COALESCE(CommentaireValidation, '') + + CHAR(10) + '[Modifiée le ' + + FORMAT(GETDATE(), 'dd/MM/yyyy à HH:mm', 'fr-FR') + ']' - ) WHERE Id = ?`, [leaveType, startDate, endDate, reason || '', businessDays, newStatus, requestId] ); - // 4️⃣ CALCULER ET APPLIQUER LA NOUVELLE RÉPARTITION - let newRepartition = []; + console.log(`✅ Demande ${requestId} modifiée - Statut: ${newStatus}`); - if (parseInt(leaveType) !== 3) { // Pas pour Arrêt maladie - console.log('📊 Calcul de la nouvelle répartition...'); + // 4️⃣ SUPPRIMER L'ANCIENNE RÉPARTITION DANS DemandeCongeType + await connection.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]); - const currentYear = new Date().getFullYear(); - const previousYear = currentYear - 1; - let joursRestants = parseFloat(businessDays); + // 5️⃣ Sauvegarder la nouvelle répartition AVEC GÉNÉRATION MANUELLE D'ID + console.log('\n📊 Sauvegarde de la nouvelle répartition en base...'); + for (const rep of Repartition) { + const code = rep.TypeConge; + const name = code === 'CP' ? 'Congé payé' : + code === 'RTT' ? 'RTT' : + code === 'ABS' ? 'Congé maladie' : + code === 'Formation' ? 'Formation' : + code === 'Récup' ? 'Récupération' : code; - // A. CONGÉ PAYÉ : N-1 → N → Anticipé - if (parseInt(leaveType) === 1) { - // Essayer N-1 - const [compteurN1] = await connection.query( - 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, leaveType, previousYear] - ); + const [typeRow] = await connection.query( + 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', + [name] + ); - if (compteurN1.length > 0 && compteurN1[0].Solde > 0 && joursRestants > 0) { - const disponibleN1 = parseFloat(compteurN1[0].Solde); - const aPrendreN1 = Math.min(disponibleN1, joursRestants); + if (typeRow.length > 0) { + // ⭐ GÉNÉRER L'ID MANUELLEMENT + const demandeCongeTypeId = await getNextId(connection, 'DemandeCongeType'); - await connection.query( - 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?', - [aPrendreN1, compteurN1[0].Id] - ); - - await connection.query(` - INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction) - VALUES (?, ?, ?, ?, ?) - `, [requestId, leaveType, previousYear, aPrendreN1, 'Année N-1']); - - newRepartition.push({ - typeCongeId: leaveType, - annee: previousYear, - jours: aPrendreN1, - typeDeduction: 'Année N-1' - }); - - joursRestants -= aPrendreN1; - console.log(` ✅ Déduit ${aPrendreN1}j de N-1`); - } - - // Essayer N - if (joursRestants > 0) { - const [compteurN] = await connection.query( - 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, leaveType, currentYear] - ); - - if (compteurN.length > 0) { - const disponibleN = parseFloat(compteurN[0].Solde); - const aPrendreN = Math.min(disponibleN, joursRestants); - - await connection.query( - 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?', - [aPrendreN, compteurN[0].Id] - ); - - await connection.query(` - INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction) - VALUES (?, ?, ?, ?, ?) - `, [requestId, leaveType, currentYear, aPrendreN, 'Année N']); - - newRepartition.push({ - typeCongeId: leaveType, - annee: currentYear, - jours: aPrendreN, - typeDeduction: 'Année N' - }); - - joursRestants -= aPrendreN; - console.log(` ✅ Déduit ${aPrendreN}j de N`); - } - } - - // Anticipé (si encore des jours restants) - if (joursRestants > 0) { - const [compteurN] = await connection.query( - 'SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, leaveType, currentYear] - ); - - if (compteurN.length > 0) { - await connection.query( - 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?', - [joursRestants, compteurN[0].Id] - ); - - await connection.query(` - INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction, IsAnticipe) - VALUES (?, ?, ?, ?, ?, 1) - `, [requestId, leaveType, currentYear, joursRestants, 'N Anticip']); - - newRepartition.push({ - typeCongeId: leaveType, - annee: currentYear, - jours: joursRestants, - typeDeduction: 'N Anticip' - }); - - console.log(` ⚠️ Déduit ${joursRestants}j en ANTICIPÉ`); - } - } + await connection.query(` + INSERT INTO DemandeCongeType + (Id, DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) + VALUES (?, ?, ?, ?, ?) + `, [ + demandeCongeTypeId, + requestId, + typeRow[0].Id, + rep.NombreJours, + rep.PeriodeJournee || 'Journée entière' + ]); + console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`); } + } - // B. RTT : Uniquement année N - else if (parseInt(leaveType) === 2) { - const [compteurRTT] = await connection.query( - 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, leaveType, currentYear] - ); + // 6️⃣ CALCULER ET APPLIQUER LA NOUVELLE DÉDUCTION + let newRepartition = []; + const currentYear = new Date().getFullYear(); + const previousYear = currentYear - 1; - if (compteurRTT.length > 0) { - const disponible = parseFloat(compteurRTT[0].Solde); + const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation'; - if (disponible < joursRestants) { - throw new Error(`Solde RTT insuffisant: ${disponible.toFixed(2)}j disponibles, ${joursRestants}j demandés`); + if (!isFormationOnly) { + console.log('📉 Déduction des compteurs...'); + + for (const rep of Repartition) { + if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') { + console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`); + continue; + } + + // ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP + if (rep.TypeConge === 'Récup') { + const [recupType] = await connection.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); + + if (recupType.length > 0) { + await connection.query(` + UPDATE CompteurConges + SET Solde = CASE WHEN Solde - ? < 0 THEN 0 ELSE Solde - ? END, + DerniereMiseAJour = GETDATE() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [rep.NombreJours, rep.NombreJours, userId, recupType[0].Id, currentYear]); + + await connection.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Récup Posée', ?) + `, [requestId, recupType[0].Id, currentYear, rep.NombreJours]); + + newRepartition.push({ + typeCongeId: recupType[0].Id, + annee: currentYear, + jours: rep.NombreJours, + typeDeduction: 'Récup Posée' + }); + + console.log(` ✓ Récup: ${rep.NombreJours}j déduits`); + } + continue; + } + + // ⭐ CP et RTT : déduction normale + const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT'; + const [typeRow] = await connection.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); + + if (typeRow.length > 0) { + let joursRestants = parseFloat(rep.NombreJours); + + // A. Essayer N-1 (CP uniquement) + if (rep.TypeConge === 'CP') { + const [compteurN1] = await connection.query( + 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, typeRow[0].Id, previousYear] + ); + + if (compteurN1.length > 0 && compteurN1[0].Solde > 0) { + const disponibleN1 = parseFloat(compteurN1[0].Solde); + const aPrendreN1 = Math.min(disponibleN1, joursRestants); + + await connection.query( + 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', + [aPrendreN1, compteurN1[0].Id] + ); + + await connection.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Année N-1', ?) + `, [requestId, typeRow[0].Id, previousYear, aPrendreN1]); + + newRepartition.push({ + typeCongeId: typeRow[0].Id, + annee: previousYear, + jours: aPrendreN1, + typeDeduction: 'Année N-1' + }); + + joursRestants -= aPrendreN1; + console.log(` ✓ ${name} N-1: ${aPrendreN1}j déduits (reste: ${joursRestants}j)`); + } } - await connection.query( - 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?', - [joursRestants, compteurRTT[0].Id] - ); + // B. Essayer N + if (joursRestants > 0) { + const [compteurN] = await connection.query( + 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, typeRow[0].Id, currentYear] + ); - await connection.query(` - INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction) - VALUES (?, ?, ?, ?, ?) - `, [requestId, leaveType, currentYear, joursRestants, 'Année N']); + if (compteurN.length > 0) { + const disponibleN = parseFloat(compteurN[0].Solde); + const aPrendreN = Math.min(disponibleN, joursRestants); - newRepartition.push({ - typeCongeId: leaveType, - annee: currentYear, - jours: joursRestants, - typeDeduction: 'Année N' - }); + await connection.query( + 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', + [aPrendreN, compteurN[0].Id] + ); - console.log(` ✅ Déduit ${joursRestants}j RTT de N`); + await connection.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Année N', ?) + `, [requestId, typeRow[0].Id, currentYear, aPrendreN]); + + newRepartition.push({ + typeCongeId: typeRow[0].Id, + annee: currentYear, + jours: aPrendreN, + typeDeduction: 'Année N' + }); + + joursRestants -= aPrendreN; + console.log(` ✓ ${name} N: ${aPrendreN}j déduits (reste: ${joursRestants}j)`); + } + } + + // C. Si il reste des jours → Anticipé + if (joursRestants > 0) { + const [compteurN] = await connection.query( + 'SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, typeRow[0].Id, currentYear] + ); + + if (compteurN.length > 0) { + await connection.query( + 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', + [joursRestants, compteurN[0].Id] + ); + + await connection.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'N Anticip', ?) + `, [requestId, typeRow[0].Id, currentYear, joursRestants]); + + newRepartition.push({ + typeCongeId: typeRow[0].Id, + annee: currentYear, + jours: joursRestants, + typeDeduction: 'N Anticip' + }); + + console.log(` ⚠️ ${name} Anticipé: ${joursRestants}j`); + } + } } } } @@ -6167,7 +7593,7 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, await connection.commit(); console.log(`✅ Demande ${requestId} modifiée avec succès`); - // 5️⃣ ENVOI DES EMAILS (Asynchrone, ne bloque pas la réponse) + // 7️⃣ ENVOI DES EMAILS (Asynchrone) const graphToken = await getGraphToken(); if (graphToken) { @@ -6221,9 +7647,6 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req,
` : ''}

Merci de valider ou refuser cette demande dans l'application.

-
-

📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.

-
` @@ -6260,9 +7683,6 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req,

Elle est maintenant en attente de validation.

-
-

📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.

-
` @@ -6380,9 +7800,13 @@ app.post('/api/deleteRequest', async (req, res) => { // 1️⃣ Vérifier que la demande existe const [existingRequest] = await conn.query( - `SELECT d.*, - DATE_FORMAT(d.DateDebut, '%Y-%m-%d') as DateDebut, - DATE_FORMAT(d.DateFin, '%Y-%m-%d') as DateFin + `SELECT + d.Id, + d.DateDebut, + d.DateFin, + d.Statut, + d.NombreJours, + d.CollaborateurADId FROM DemandeConge d WHERE d.Id = ? AND d.CollaborateurADId = ?`, [requestId, userId] @@ -6399,22 +7823,33 @@ app.post('/api/deleteRequest', async (req, res) => { const request = existingRequest[0]; const requestStatus = request.Statut; - const dateDebut = new Date(request.DateDebut); + + // ⭐ CORRECTION 1 : Déclarer `aujourdhui` et `dateDebut` AVANT de les utiliser const aujourdhui = new Date(); aujourdhui.setHours(0, 0, 0, 0); - dateDebut.setHours(0, 0, 0, 0); - console.log(`📋 Demande: Statut=${requestStatus}, Date début=${dateDebut.toLocaleDateString('fr-FR')}`); + const dateDebut = request.DateDebut ? new Date(request.DateDebut) : null; + if (dateDebut) { + dateDebut.setHours(0, 0, 0, 0); + } - // ❌ BLOQUER SI DATE DÉJÀ PASSÉE - if (dateDebut <= aujourdhui && requestStatus === 'Validée') { - await conn.rollback(); - conn.release(); - return res.status(400).json({ - success: false, - message: '❌ Impossible d\'annuler : la date de début est déjà passée ou c\'est aujourd\'hui', - dateDebut: formatDateWithoutUTC(dateDebut) - }); + if (!dateDebut || isNaN(dateDebut.getTime())) { + console.warn('⚠️ Date invalide, on autorise l\'annulation'); + // Ne pas bloquer l'annulation si la date est invalide + } else { + dateDebut.setHours(0, 0, 0, 0); + console.log(`📋 Demande: Statut=${requestStatus}, Date début=${dateDebut.toLocaleDateString('fr-FR')}`); + + // ❌ BLOQUER SI DATE DÉJÀ PASSÉE + if (dateDebut <= aujourdhui && requestStatus === 'Validée') { + await conn.rollback(); + conn.release(); + return res.status(400).json({ + success: false, + message: '❌ Impossible d\'annuler : la date de début est déjà passée ou c\'est aujourd\'hui', + dateDebut: dateDebut.toISOString().split('T')[0] + }); + } } // 2️⃣ RÉCUPÉRER LA RÉPARTITION (pour l'email) @@ -6449,7 +7884,7 @@ app.post('/api/deleteRequest', async (req, res) => { const newSolde = parseFloat(c.Solde) + parseFloat(d.JoursUtilises); await conn.query( - 'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?', + 'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = GETDATE() WHERE Id = ?', [newSolde, c.Id] ); @@ -6478,13 +7913,11 @@ app.post('/api/deleteRequest', async (req, res) => { await conn.query( `UPDATE DemandeConge SET Statut = 'Annulée', - DateValidation = NOW(), - CommentaireValidation = CONCAT( - COALESCE(CommentaireValidation, ''), - '\n[Annulée par le collaborateur le ', - DATE_FORMAT(NOW(), '%d/%m/%Y à %H:%i'), + DateValidation = GETDATE(), + CommentaireValidation = COALESCE(CommentaireValidation, '') + + CHAR(10) + '[Annulée par le collaborateur le ' + + FORMAT(GETDATE(), 'dd/MM/yyyy à HH:mm', 'fr-FR') + ']' - ) WHERE Id = ?`, [requestId] ); @@ -6509,8 +7942,15 @@ app.post('/api/deleteRequest', async (req, res) => { ? `${collabInfo[0].prenom} ${collabInfo[0].nom}` : userName; - const dateDebutFormatted = new Date(request.DateDebut).toLocaleDateString('fr-FR'); - const dateFinFormatted = new Date(request.DateFin).toLocaleDateString('fr-FR'); + // ⭐ CORRECTION : Formater correctement les dates pour l'email + const dateDebutFormatted = request.DateDebut + ? new Date(request.DateDebut).toLocaleDateString('fr-FR') + : 'Date inconnue'; + + const dateFinFormatted = request.DateFin + ? new Date(request.DateFin).toLocaleDateString('fr-FR') + : 'Date inconnue'; + const datesPeriode = dateDebutFormatted === dateFinFormatted ? dateDebutFormatted : `du ${dateDebutFormatted} au ${dateFinFormatted}`; @@ -6722,11 +8162,11 @@ app.get('/api/exportCompteurs', async (req, res) => { const dateEntree = collab.DateEntree; const dateReference = new Date(dateRef); - const acquisCP = calculerAcquisitionCP(dateReference, dateEntree); + const acquisCP = calculerAcquisitionCP_Smart(dateReference, dateEntree); let acquisRTT = 0; if (collab.role !== 'Apprenti') { - const rttData = await calculerAcquisitionRTT(conn, collab.id, dateReference); + const rttData = await calculerAcquisitionRTT_Smart(conn, collab.id, dateReference); acquisRTT = rttData.acquisition; } @@ -6870,7 +8310,7 @@ async function checkSoldesDisponiblesMixte(conn, collaborateurId, repartition, d const dateEntree = collabInfo[0]?.DateEntree; // ⭐ CALCULER L'ACQUISITION JUSQU'À LA DATE DEMANDÉE - const acquisALaDate = calculerAcquisitionCP(dateDemandeObj, dateEntree); + const acquisALaDate = calculerAcquisitionCP_Smart(dateDemandeObj, dateEntree); console.log('💰 Acquisition CP à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j'); @@ -6940,7 +8380,7 @@ async function checkSoldesDisponiblesMixte(conn, collaborateurId, repartition, d } // ⭐ CALCUL RTT (utiliser la fonction existante) - const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDemandeObj); + const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, dateDemandeObj); const acquisALaDate = rttData.acquisition; console.log('💰 Acquisition RTT à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j'); @@ -7073,7 +8513,7 @@ async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe // Anticipation N const finExerciceN = new Date(currentYear + 1, 4, 31); // 31 mai N+1 - const acquisTotaleN = calculerAcquisitionCP(finExerciceN, dateEntree); + const acquisTotaleN = calculerAcquisitionCP_Smart(finExerciceN, dateEntree); const soldeAnticipeN = Math.max(0, acquisTotaleN - totalAcquisN); console.log(' N-1:', soldeN1); @@ -7091,7 +8531,7 @@ async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe dateCalculN1 = new Date(dateEntree); } - const acquisTotaleN1 = calculerAcquisitionCP(finExerciceN1, dateCalculN1); + const acquisTotaleN1 = calculerAcquisitionCP_Smart(finExerciceN1, dateCalculN1); soldeAnticipeN1 = acquisTotaleN1; console.log(' Anticipé N+1:', soldeAnticipeN1); @@ -7101,43 +8541,43 @@ async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe } async function getSoldesRTT(conn, collaborateurId, typeContrat, dateEntree) { - const currentYear = new Date().getFullYear(); - - const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`); - const typeCongeId = rttType[0].Id; - - const compteursN = await conn.query( - `SELECT Solde, Total FROM CompteurConges + const currentYear = new Date().getFullYear(); + + const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`); + const typeCongeId = rttType[0].Id; + + const compteursN = await conn.query( + `SELECT Solde, Total FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, typeCongeId, currentYear] - ); - - const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) : 0; - const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0; - - // Calcul anticipation N - const finAnneeN = new Date(currentYear, 11, 31); // 31 déc N - const rttDataTotalN = await calculerAcquisitionRTT(conn, collaborateurId, finAnneeN); - const soldeAnticipeN = Math.max(0, rttDataTotalN.acquisition - totalAcquisN); - - return { soldeN, soldeAnticipeN }; + [collaborateurId, typeCongeId, currentYear] + ); + + const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) : 0; + const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0; + + // Calcul anticipation N + const finAnneeN = new Date(currentYear, 11, 31); // 31 déc N + const rttDataTotalN = await calculerAcquisitionRTT_Smart(conn, collaborateurId, finAnneeN); + const soldeAnticipeN = Math.max(0, rttDataTotalN.acquisition - totalAcquisN); + + return { soldeN, soldeAnticipeN }; } async function getSoldesRecup(conn, collaborateurId) { - const currentYear = new Date().getFullYear(); - - const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`); - if (recupType.length === 0) return 0; - - const typeCongeId = recupType[0].Id; - - const compteur = await conn.query( - `SELECT Solde FROM CompteurConges + const currentYear = new Date().getFullYear(); + + const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`); + if (recupType.length === 0) return 0; + + const typeCongeId = recupType[0].Id; + + const compteur = await conn.query( + `SELECT Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, typeCongeId, currentYear] - ); - - return compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; + [collaborateurId, typeCongeId, currentYear] + ); + + return compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; } app.get('/api/getAnticipationDisponible', async (req, res) => { @@ -7301,7 +8741,7 @@ async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nomb if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges - SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?) + SET SoldeAnticipe = CASE WHEN (SoldeAnticipe - ?) < 0 THEN 0 ELSE (SoldeAnticipe - ?) END WHERE Id = ? `, [aDeduire, compteurN_Anticipe[0].Id]); @@ -7339,7 +8779,7 @@ async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nomb if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?) + SET Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ? `, [aDeduire, compteurN[0].Id]); @@ -7377,8 +8817,8 @@ async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nomb if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges - SET SoldeReporte = GREATEST(0, SoldeReporte - ?), - Solde = GREATEST(0, Solde - ?) + SET SoldeReporte = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END, + Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ? `, [aDeduire, aDeduire, compteurN1[0].Id]); @@ -7418,8 +8858,8 @@ async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nomb if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges - SET SoldeReporte = GREATEST(0, SoldeReporte - ?), - Solde = GREATEST(0, Solde - ?) + SET SoldeReporte = CASE WHEN (SoldeReporte - ?) < 0 THEN 0 ELSE (SoldeReporte - ?) END, + Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ? `, [aDeduire, aDeduire, compteurN1[0].Id]); @@ -7456,7 +8896,7 @@ async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nomb if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?) + SET Solde = CASE WHEN (Solde - ?) < 0 THEN 0 ELSE (Solde - ?) END WHERE Id = ? `, [aDeduire, compteurN[0].Id]); @@ -7494,7 +8934,7 @@ async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nomb if (aDeduire > 0) { await conn.query(` UPDATE CompteurConges - SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?) + SET SoldeAnticipe = CASE WHEN (SoldeAnticipe - ?) < 0 THEN 0 ELSE (SoldeAnticipe - ?) END WHERE Id = ? `, [aDeduire, compteurN_Anticipe[0].Id]); @@ -7527,6 +8967,10 @@ async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nomb }; } +/** + * Met à jour les soldes anticipés pour un collaborateur + * Appelée après chaque mise à jour de compteur ou soumission de demande + */ /** * Met à jour les soldes anticipés pour un collaborateur * Appelée après chaque mise à jour de compteur ou soumission de demande @@ -7536,15 +8980,16 @@ async function updateSoldeAnticipe(conn, collaborateurId) { today.setHours(0, 0, 0, 0); const currentYear = today.getFullYear(); - console.log(`\n🔄 Mise à jour soldes anticipés pour collaborateur ${collaborateurId}`); + console.log(`🔄 Mise à jour soldes anticipés pour collaborateur ${collaborateurId}`); - const [collab] = await conn.query( - 'SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?', - [collaborateurId] - ); + const collab = await conn.query(` + SELECT DateEntree, TypeContrat, role + FROM CollaborateurAD + WHERE id = ? + `, [collaborateurId]); if (collab.length === 0) { - console.log(' ❌ Collaborateur non trouvé'); + console.log(`❌ Collaborateur non trouvé`); return; } @@ -7552,14 +8997,15 @@ async function updateSoldeAnticipe(conn, collaborateurId) { const typeContrat = collab[0].TypeContrat || '37h'; const isApprenti = collab[0].role === 'Apprenti'; - // ===== CP ANTICIPÉ ===== - const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); - + // ======================================== + // CP ANTICIPÉ + // ======================================== + const cpType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Congé payé']); if (cpType.length > 0) { const cpAnticipe = calculerAcquisitionCPAnticipee(today, dateEntree); // Vérifier si le compteur existe - const [compteurCP] = await conn.query(` + const compteurCP = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, cpType[0].Id, currentYear]); @@ -7568,31 +9014,31 @@ async function updateSoldeAnticipe(conn, collaborateurId) { await conn.query(` UPDATE CompteurConges SET SoldeAnticipe = ?, - DerniereMiseAJour = NOW() + DerniereMiseAJour = GETDATE() WHERE Id = ? `, [cpAnticipe, compteurCP[0].Id]); } else { // Créer le compteur s'il n'existe pas - const acquisCP = calculerAcquisitionCP(today, dateEntree); + const acquisCP = calculerAcquisitionCP_Smart(today, dateEntree); await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, ?, NOW()) + VALUES (?, ?, ?, ?, ?, 0, ?, GETDATE()) `, [collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP, cpAnticipe]); } - console.log(` ✓ CP Anticipé: ${cpAnticipe.toFixed(2)}j`); } - // ===== RTT ANTICIPÉ ===== + // ======================================== + // RTT ANTICIPÉ + // ======================================== if (!isApprenti) { - const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); - + const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['RTT']); if (rttType.length > 0) { const rttAnticipe = await calculerAcquisitionRTTAnticipee(conn, collaborateurId, today); // Vérifier si le compteur existe - const [compteurRTT] = await conn.query(` + const compteurRTT = await conn.query(` SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [collaborateurId, rttType[0].Id, currentYear]); @@ -7601,26 +9047,26 @@ async function updateSoldeAnticipe(conn, collaborateurId) { await conn.query(` UPDATE CompteurConges SET SoldeAnticipe = ?, - DerniereMiseAJour = NOW() + DerniereMiseAJour = GETDATE() WHERE Id = ? `, [rttAnticipe, compteurRTT[0].Id]); } else { // Créer le compteur s'il n'existe pas - const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); + const rttData = await calculerAcquisitionRTT_Smart(conn, collaborateurId, today); await conn.query(` INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, ?, NOW()) + VALUES (?, ?, ?, ?, ?, 0, ?, GETDATE()) `, [collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition, rttAnticipe]); } - console.log(` ✓ RTT Anticipé: ${rttAnticipe.toFixed(2)}j`); } } - console.log(` ✅ Soldes anticipés mis à jour\n`); + console.log(` ✅ Soldes anticipés mis à jour`); } + /** * GET /getSoldesAnticipes * Retourne les soldes actuels ET anticipés pour un collaborateur @@ -7664,7 +9110,7 @@ app.get('/api/getSoldesAnticipes', async (req, res) => { // ===== CP ===== const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); - + let cpData = { acquis: 0, solde: 0, @@ -7674,8 +9120,8 @@ app.get('/api/getSoldesAnticipes', async (req, res) => { if (cpType.length > 0) { // Acquisition actuelle - const acquisCP = calculerAcquisitionCP(dateReference, dateEntree); - + const acquisCP = calculerAcquisitionCP_Smart(dateReference, dateEntree); + // Anticipé const anticipeCP = calculerAcquisitionCPAnticipee(dateReference, dateEntree); @@ -7720,7 +9166,7 @@ app.get('/api/getSoldesAnticipes', async (req, res) => { if (rttType.length > 0) { // Acquisition actuelle - const rttCalc = await calculerAcquisitionRTT(conn, userId, dateReference); + const rttCalc = await calculerAcquisitionRTT_Smart(conn, userId, dateReference); const acquisRTT = rttCalc.acquisition; // Anticipé @@ -7836,11 +9282,11 @@ app.get('/api/getCongesAnticipes', async (req, res) => { // ======================================== // Acquisition actuelle - const acquisActuelleCP = calculerAcquisitionCP(today, dateEntree); + const acquisActuelleCP = calculerAcquisitionCP_Smart(today, dateEntree); // Acquisition prévue à la fin de l'exercice (31 mai N+1) const finExerciceCP = new Date(currentYear + 1, 4, 31); // 31 mai N+1 - const acquisTotaleCP = calculerAcquisitionCP(finExerciceCP, dateEntree); + const acquisTotaleCP = calculerAcquisitionCP_Smart(finExerciceCP, dateEntree); // Récupérer le solde actuel const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); @@ -7880,11 +9326,11 @@ app.get('/api/getCongesAnticipes', async (req, res) => { if (user.role !== 'Apprenti') { // Acquisition actuelle - const rttDataActuel = await calculerAcquisitionRTT(conn, userId, today); + const rttDataActuel = await calculerAcquisitionRTT_Smart(conn, userId, today); acquisActuelleRTT = rttDataActuel.acquisition; // Acquisition prévue à la fin de l'année - const rttDataTotal = await calculerAcquisitionRTT(conn, userId, finAnnee); + const rttDataTotal = await calculerAcquisitionRTT_Smart(conn, userId, finAnnee); acquisTotaleRTT = rttDataTotal.acquisition; // Récupérer le solde actuel @@ -7914,10 +9360,6 @@ app.get('/api/getCongesAnticipes', async (req, res) => { conn.release(); - // ======================================== - // RÉPONSE - // ======================================== - res.json({ success: true, user: { @@ -7991,10 +9433,10 @@ function calculerAcquisitionCPAnticipee(dateReference = new Date(), dateEntree = finExercice.setHours(0, 0, 0, 0); // 2️⃣ Calculer l'acquisition actuelle - const acquisActuelle = calculerAcquisitionCP(d, dateEntree); + const acquisActuelle = calculerAcquisitionCP_Smart(d, dateEntree); // 3️⃣ Calculer l'acquisition totale à fin d'exercice - const acquisTotaleFinExercice = calculerAcquisitionCP(finExercice, dateEntree); + const acquisTotaleFinExercice = calculerAcquisitionCP_Smart(finExercice, dateEntree); // 4️⃣ Anticipée = Totale - Actuelle (plafonnée à 25) const acquisAnticipee = Math.min(25, acquisTotaleFinExercice) - acquisActuelle; @@ -8032,14 +9474,14 @@ async function calculerAcquisitionRTTAnticipee(conn, collaborateurId, dateRefere const config = await getConfigurationRTT(conn, annee, typeContrat); // 4️⃣ Calculer l'acquisition actuelle - const rttActuel = await calculerAcquisitionRTT(conn, collaborateurId, d); + const rttActuel = await calculerAcquisitionRTT_Smart(conn, collaborateurId, d); const acquisActuelle = rttActuel.acquisition; // 5️⃣ Calculer l'acquisition totale à fin d'année (31/12) const finAnnee = new Date(annee, 11, 31); finAnnee.setHours(0, 0, 0, 0); - const rttFinAnnee = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee); + const rttFinAnnee = await calculerAcquisitionRTT_Smart(conn, collaborateurId, finAnnee); const acquisTotaleFinAnnee = rttFinAnnee.acquisition; // 6️⃣ Anticipée = Totale - Actuelle (plafonnée au max annuel) @@ -8169,8 +9611,6 @@ async function hasCompteRenduAccess(userId) { return false; } } - - // Récupérer les jours du mois // GET - Récupérer les données du compte-rendu app.get('/api/getCompteRenduActivites', async (req, res) => { @@ -8231,8 +9671,6 @@ app.get('/api/getCompteRenduActivites', async (req, res) => { res.status(500).json({ success: false, message: error.message }); } }); - -// POST - Sauvegarder un jour avec AUTO-VERROUILLAGE // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE app.post('/api/saveCompteRenduJour', async (req, res) => { const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body; @@ -8258,11 +9696,11 @@ app.post('/api/saveCompteRenduJour', async (req, res) => { // Vérifier si le JOUR est déjà verrouillé (pas le mois entier) const [jourExistant] = await conn.query( - 'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?', + 'SELECT Id, Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?', [user_id, date] ); - if (jourExistant[0]?.Verrouille && !rh_override) { + if (jourExistant.length > 0 && jourExistant[0].Verrouille && !rh_override) { await conn.rollback(); conn.release(); return res.json({ success: false, message: 'Ce jour est verrouillé - Contactez les RH pour modification' }); @@ -8277,45 +9715,65 @@ app.post('/api/saveCompteRenduJour', async (req, res) => { } } - // Insérer ou mettre à jour le jour ET LE VERROUILLER - await conn.query(` - INSERT INTO CompteRenduActivites - (CollaborateurADId, Annee, Mois, JourDate, JourTravaille, - ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos, - DateSaisie, SaisiePar, Verrouille) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE) - ON DUPLICATE KEY UPDATE - JourTravaille = VALUES(JourTravaille), - ReposQuotidienRespect = VALUES(ReposQuotidienRespect), - ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect), - CommentaireRepos = VALUES(CommentaireRepos), - SaisiePar = VALUES(SaisiePar), - Verrouille = TRUE - `, [user_id, annee, mois, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, user_id]); + // ⭐ FIX: Utiliser IF EXISTS pattern au lieu de ON DUPLICATE KEY UPDATE + if (jourExistant.length > 0) { + // UPDATE + await conn.query(` + UPDATE CompteRenduActivites + SET JourTravaille = ?, + ReposQuotidienRespect = ?, + ReposHebdomadaireRespect = ?, + CommentaireRepos = ?, + SaisiePar = ?, + Verrouille = 1 + WHERE CollaborateurADId = ? AND JourDate = ? + `, [jour_travaille, repos_quotidien, repos_hebdo, commentaire, user_id, user_id, date]); + } else { + // INSERT + await conn.query(` + INSERT INTO CompteRenduActivites + (CollaborateurADId, Annee, Mois, JourDate, JourTravaille, + ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos, + DateSaisie, SaisiePar, Verrouille) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, GETDATE(), ?, 1) + `, [user_id, annee, mois, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, user_id]); + } // Mettre à jour les statistiques mensuelles (SANS verrouiller le mois) const [stats] = await conn.query(` SELECT COUNT(*) as nbJours, - SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien, - SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo + SUM(CASE WHEN ReposQuotidienRespect = 0 THEN 1 ELSE 0 END) as nbNonRespectQuotidien, + SUM(CASE WHEN ReposHebdomadaireRespect = 0 THEN 1 ELSE 0 END) as nbNonRespectHebdo FROM CompteRenduActivites WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? - AND JourTravaille = TRUE + AND JourTravaille = 1 `, [user_id, annee, mois]); - await conn.query(` - INSERT INTO CompteRenduMensuel - (CollaborateurADId, Annee, Mois, NbJoursTravailles, - NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo, - Statut, DateValidation) - VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW()) - ON DUPLICATE KEY UPDATE - NbJoursTravailles = VALUES(NbJoursTravailles), - NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien), - NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo), - DateValidation = NOW() - `, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]); + // ⭐ FIX: Vérifier si le mensuel existe + const [mensuelExistant] = await conn.query(` + SELECT Id FROM CompteRenduMensuel + WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? + `, [user_id, annee, mois]); + + if (mensuelExistant.length > 0) { + await conn.query(` + UPDATE CompteRenduMensuel + SET NbJoursTravailles = ?, + NbJoursNonRespectsReposQuotidien = ?, + NbJoursNonRespectsReposHebdo = ?, + DateValidation = GETDATE() + WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? + `, [stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo, user_id, annee, mois]); + } else { + await conn.query(` + INSERT INTO CompteRenduMensuel + (CollaborateurADId, Annee, Mois, NbJoursTravailles, + NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo, + Statut, DateValidation) + VALUES (?, ?, ?, ?, ?, ?, 'En cours', GETDATE()) + `, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]); + } await conn.commit(); conn.release(); @@ -8357,33 +9815,40 @@ app.post('/api/saveCompteRenduMasse', async (req, res) => { // Vérifier si déjà verrouillé const [jourExistant] = await conn.query( - 'SELECT Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?', + 'SELECT Id, Verrouille FROM CompteRenduActivites WHERE CollaborateurADId = ? AND JourDate = ?', [user_id, jour.date] ); - if (jourExistant[0]?.Verrouille && !rh_override) { + if (jourExistant.length > 0 && jourExistant[0].Verrouille && !rh_override) { blocked++; continue; } - await conn.query(` - INSERT INTO CompteRenduActivites - (CollaborateurADId, Annee, Mois, JourDate, JourTravaille, - ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos, - DateSaisie, SaisiePar, Verrouille) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, TRUE) - ON DUPLICATE KEY UPDATE - JourTravaille = VALUES(JourTravaille), - ReposQuotidienRespect = VALUES(ReposQuotidienRespect), - ReposHebdomadaireRespect = VALUES(ReposHebdomadaireRespect), - CommentaireRepos = VALUES(CommentaireRepos), - SaisiePar = VALUES(SaisiePar), - Verrouille = TRUE - `, [ - user_id, annee, mois, jour.date, - jour.jour_travaille, jour.repos_quotidien, jour.repos_hebdo, - jour.commentaire || null, user_id - ]); + // ⭐ FIX: Utiliser IF EXISTS pattern au lieu de ON DUPLICATE KEY UPDATE + if (jourExistant.length > 0) { + // UPDATE + await conn.query(` + UPDATE CompteRenduActivites + SET JourTravaille = ?, + ReposQuotidienRespect = ?, + ReposHebdomadaireRespect = ?, + CommentaireRepos = ?, + SaisiePar = ?, + Verrouille = 1 + WHERE CollaborateurADId = ? AND JourDate = ? + `, [jour.jour_travaille, jour.repos_quotidien, jour.repos_hebdo, + jour.commentaire || null, user_id, user_id, jour.date]); + } else { + // INSERT + await conn.query(` + INSERT INTO CompteRenduActivites + (CollaborateurADId, Annee, Mois, JourDate, JourTravaille, + ReposQuotidienRespect, ReposHebdomadaireRespect, CommentaireRepos, + DateSaisie, SaisiePar, Verrouille) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, GETDATE(), ?, 1) + `, [user_id, annee, mois, jour.date, jour.jour_travaille, + jour.repos_quotidien, jour.repos_hebdo, jour.commentaire || null, user_id]); + } count++; } @@ -8392,25 +9857,37 @@ app.post('/api/saveCompteRenduMasse', async (req, res) => { const [stats] = await conn.query(` SELECT COUNT(*) as nbJours, - SUM(CASE WHEN ReposQuotidienRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectQuotidien, - SUM(CASE WHEN ReposHebdomadaireRespect = FALSE THEN 1 ELSE 0 END) as nbNonRespectHebdo + SUM(CASE WHEN ReposQuotidienRespect = 0 THEN 1 ELSE 0 END) as nbNonRespectQuotidien, + SUM(CASE WHEN ReposHebdomadaireRespect = 0 THEN 1 ELSE 0 END) as nbNonRespectHebdo FROM CompteRenduActivites WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? - AND JourTravaille = TRUE + AND JourTravaille = 1 `, [user_id, annee, mois]); - await conn.query(` - INSERT INTO CompteRenduMensuel - (CollaborateurADId, Annee, Mois, NbJoursTravailles, - NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo, - Statut, DateValidation) - VALUES (?, ?, ?, ?, ?, ?, 'En cours', NOW()) - ON DUPLICATE KEY UPDATE - NbJoursTravailles = VALUES(NbJoursTravailles), - NbJoursNonRespectsReposQuotidien = VALUES(NbJoursNonRespectsReposQuotidien), - NbJoursNonRespectsReposHebdo = VALUES(NbJoursNonRespectsReposHebdo), - DateValidation = NOW() - `, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]); + // ⭐ FIX: Vérifier si le mensuel existe + const [mensuelExistant] = await conn.query(` + SELECT Id FROM CompteRenduMensuel + WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? + `, [user_id, annee, mois]); + + if (mensuelExistant.length > 0) { + await conn.query(` + UPDATE CompteRenduMensuel + SET NbJoursTravailles = ?, + NbJoursNonRespectsReposQuotidien = ?, + NbJoursNonRespectsReposHebdo = ?, + DateValidation = GETDATE() + WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? + `, [stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo, user_id, annee, mois]); + } else { + await conn.query(` + INSERT INTO CompteRenduMensuel + (CollaborateurADId, Annee, Mois, NbJoursTravailles, + NbJoursNonRespectsReposQuotidien, NbJoursNonRespectsReposHebdo, + Statut, DateValidation) + VALUES (?, ?, ?, ?, ?, ?, 'En cours', GETDATE()) + `, [user_id, annee, mois, stats[0].nbJours, stats[0].nbNonRespectQuotidien, stats[0].nbNonRespectHebdo]); + } await conn.commit(); conn.release(); @@ -8482,7 +9959,7 @@ app.post('/api/verrouillerCompteRendu', async (req, res) => { await conn.query(` UPDATE CompteRenduMensuel SET Verrouille = TRUE, - DateModification = NOW() + DateModification = GETDATE() WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? `, [user_id, annee, mois]); @@ -8517,7 +9994,7 @@ app.post('/api/deverrouillerCompteRendu', async (req, res) => { await conn.query(` UPDATE CompteRenduMensuel SET Verrouille = FALSE, - DateDeverrouillage = NOW(), + DateDeverrouillage = GETDATE(), DeverrouillePar = ? WHERE CollaborateurADId = ? AND Annee = ? AND Mois = ? `, [rh_user_id, user_id, annee, mois]); @@ -8608,21 +10085,494 @@ app.get('/api/exportCompteRenduPDF', async (req, res) => { res.status(500).json({ success: false, message: error.message }); } }); +app.post('/api/addUserFromEntra', async (req, res) => { + try { + const { email } = req.body; + if (!email) { + return res.json({ success: false, message: 'Email requis' }); + } + console.log(`\n🔍 Recherche utilisateur Entra ID: ${email}`); + // 1️⃣ Obtenir le token + const accessToken = await getGraphToken(); + if (!accessToken) { + return res.json({ success: false, message: 'Impossible d\'obtenir le token Microsoft' }); + } + // 2️⃣ Rechercher l'utilisateur dans Entra ID + const searchUrl = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(email)}?$select=id,givenName,surname,mail,userPrincipalName,department,jobTitle,accountEnabled`; + let userData; + try { + const response = await axios.get(searchUrl, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + userData = response.data; + } catch (error) { + return res.json({ + success: false, + message: `Utilisateur ${email} non trouvé dans Entra ID` + }); + } + // 3️⃣ Vérifier si compte actif + if (userData.accountEnabled === false) { + return res.json({ + success: false, + message: 'Ce compte est désactivé dans Entra ID' + }); + } -// ======================================== -// DÉMARRAGE DU SERVEUR -// ======================================== + // 4️⃣ Vérifier s'il existe déjà en base + const conn = await pool.getConnection(); -app.listen(PORT, '0.0.0.0', () => { - console.log('✅ ✅ ✅ SERVEUR PRINCIPAL DÉMARRÉ ✅ ✅ ✅'); - console.log(`📡 Port: ${PORT}`); - console.log(`🗄️ Base: ${dbConfig.database}@${dbConfig.host}`); - console.log(`⏰ Cron jobs: activés`); - console.log(`🌐 CORS origins: ${JSON.stringify(dbConfig)}`); + const [existing] = await conn.query( + 'SELECT id, email FROM CollaborateurAD WHERE LOWER(email) = ?', + [email.toLowerCase()] + ); + + if (existing.length > 0) { + conn.release(); + return res.json({ + success: false, + message: 'Cet utilisateur existe déjà en base', + userId: existing[0].id + }); + } + + // 5️⃣ Insérer l'utilisateur (SANS DateEntree pour éviter GETDATE()) + await conn.query(` + INSERT INTO CollaborateurAD + (entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, TypeContrat) + VALUES (?, ?, ?, ?, ?, ?, 'Collaborateur', 1, 1, '37h') + `, [ + userData.id, + userData.givenName || 'Prénom', + userData.surname || 'Nom', + email.toLowerCase(), + userData.department || '', + userData.jobTitle || '' + ]); + + // 6️⃣ Récupérer l'utilisateur créé + const [newUser] = await conn.query( + 'SELECT id, prenom, nom, email FROM CollaborateurAD WHERE LOWER(email) = ?', + [email.toLowerCase()] + ); + + conn.release(); + + console.log(`✅ Utilisateur créé: ${newUser[0].prenom} ${newUser[0].nom} (ID: ${newUser[0].id})`); + + res.json({ + success: true, + message: 'Utilisateur ajouté avec succès', + user: newUser[0] + }); + + } catch (error) { + console.error('❌ Erreur addUserFromEntra:', error); + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +async function syncEntraIdUsers() { + const syncResults = { + processed: 0, + inserted: 0, + updated: 0, + deactivated: 0, + errors: [] + }; + + try { + console.log('\n🔄 === DÉBUT SYNCHRONISATION ENTRA ID ==='); + + // 1️⃣ Obtenir le token + const accessToken = await getGraphToken(); + if (!accessToken) { + console.error('❌ Impossible d\'obtenir le token'); + return syncResults; + } + console.log('✅ Token obtenu'); + + // 2️⃣ Récupérer le groupe + const groupResponse = await axios.get( + `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, + { headers: { Authorization: `Bearer ${accessToken}` } } + ); + const groupName = groupResponse.data.displayName; + console.log(`📋 Groupe : ${groupName}`); + + // 3️⃣ Récupérer tous les membres avec pagination + let allAzureMembers = []; + let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle,officeLocation,accountEnabled&$top=999`; + + console.log('📥 Récupération des membres...'); + while (nextLink) { + const membersResponse = await axios.get(nextLink, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + allAzureMembers = allAzureMembers.concat(membersResponse.data.value); + nextLink = membersResponse.data['@odata.nextLink']; + + if (nextLink) { + console.log(` 📄 ${allAzureMembers.length} membres récupérés...`); + } + } + + console.log(`✅ ${allAzureMembers.length} membres trouvés`); + + // 4️⃣ Filtrer les membres valides + const validMembers = allAzureMembers.filter(m => { + if (!m.mail || m.mail.trim() === '') return false; + if (m.accountEnabled === false) return false; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(m.mail); + }); + + console.log(`✅ ${validMembers.length} membres valides`); + + // 5️⃣ Traitement avec transaction + const transaction = new sql.Transaction(pool); + await transaction.begin(); + + try { + const azureEmails = new Set(); + validMembers.forEach(m => { + azureEmails.add(m.mail.toLowerCase().trim()); + }); + + console.log('\n📝 Traitement des utilisateurs...'); + + // 6️⃣ Pour chaque membre + for (const m of validMembers) { + try { + const emailClean = m.mail.toLowerCase().trim(); + syncResults.processed++; + + // ✅ CORRIGÉ : Utiliser entraUserId et Actif + const request = new sql.Request(transaction); + request.input('email', sql.NVarChar, emailClean); + + const result = await request.query(` + SELECT id, email, entraUserId, Actif + FROM CollaborateurAD + WHERE LOWER(email) = LOWER(@email) + `); + + if (result.recordset.length > 0) { + // ✅ MISE À JOUR avec bonnes colonnes + const updateRequest = new sql.Request(transaction); + updateRequest.input('entraUserId', sql.NVarChar, m.id); + updateRequest.input('prenom', sql.NVarChar, m.givenName || ''); + updateRequest.input('nom', sql.NVarChar, m.surname || ''); + updateRequest.input('service', sql.NVarChar, m.department || ''); + updateRequest.input('description', sql.NVarChar, m.jobTitle || ''); + updateRequest.input('email', sql.NVarChar, emailClean); + + await updateRequest.query(` + UPDATE CollaborateurAD + SET + entraUserId = @entraUserId, + prenom = @prenom, + nom = @nom, + service = @service, + description = @description, + Actif = 1 + WHERE LOWER(email) = LOWER(@email) + `); + + syncResults.updated++; + console.log(` ✓ Mis à jour : ${emailClean}`); + + } else { + // ✅ INSERTION avec bonnes colonnes + const insertRequest = new sql.Request(transaction); + insertRequest.input('entraUserId', sql.NVarChar, m.id); + insertRequest.input('prenom', sql.NVarChar, m.givenName || ''); + insertRequest.input('nom', sql.NVarChar, m.surname || ''); + insertRequest.input('email', sql.NVarChar, emailClean); + insertRequest.input('service', sql.NVarChar, m.department || ''); + insertRequest.input('description', sql.NVarChar, m.jobTitle || ''); + + await insertRequest.query(` + INSERT INTO CollaborateurAD + (entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, TypeContrat) + VALUES (@entraUserId, @prenom, @nom, @email, @service, @description, 'Collaborateur', 1, 1, '37h') + `); + + syncResults.inserted++; + console.log(` ✓ Créé : ${emailClean}`); + } + + } catch (userError) { + syncResults.errors.push({ + email: m.mail, + error: userError.message + }); + console.error(` ❌ Erreur ${m.mail}:`, userError.message); + } + } + + // 7️⃣ ✅ DÉSACTIVATION avec bonne colonne (Actif, pas dateMiseAJour) + console.log('\n🔍 Désactivation des comptes obsolètes...'); + + if (azureEmails.size > 0) { + const activeEmailsList = Array.from(azureEmails).map(e => `'${e}'`).join(','); + + const deactivateRequest = new sql.Request(transaction); + const deactivateResult = await deactivateRequest.query(` + UPDATE CollaborateurAD + SET Actif = 0 + WHERE + email IS NOT NULL + AND email != '' + AND LOWER(email) NOT IN (${activeEmailsList}) + AND Actif = 1 + `); + + syncResults.deactivated = deactivateResult.rowsAffected[0]; + console.log(` ✓ ${syncResults.deactivated} compte(s) désactivé(s)`); + } + + await transaction.commit(); + + console.log('\n📊 === RÉSUMÉ ==='); + console.log(` Groupe: ${groupName}`); + console.log(` Total Entra: ${allAzureMembers.length}`); + console.log(` Valides: ${validMembers.length}`); + console.log(` Traités: ${syncResults.processed}`); + console.log(` Créés: ${syncResults.inserted}`); + console.log(` Mis à jour: ${syncResults.updated}`); + console.log(` Désactivés: ${syncResults.deactivated}`); + console.log(` Erreurs: ${syncResults.errors.length}`); + + } catch (error) { + await transaction.rollback(); + throw error; + } + + } catch (error) { + console.error('\n❌ ERREUR SYNCHRONISATION:', error.message); + } + + return syncResults; +} +app.post('/api/sync-all', async (req, res) => { + try { + console.log('🚀 Sync complète manuelle...'); + const results = await syncEntraIdUsers(); + + res.json({ + success: true, + message: 'Sync terminée', + stats: results + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +// Route diagnostic +app.get('/api/diagnostic-sync', async (req, res) => { + try { + const totalDB = await pool.query( + 'SELECT COUNT(*) as total, SUM(CASE WHEN actif = 1 THEN 1 ELSE 0 END) as actifs FROM CollaborateurAD', + [] + ); + + const sansEmail = await pool.query( + 'SELECT COUNT(*) as total FROM CollaborateurAD WHERE email IS NULL OR email = \'\'', + [] + ); + + const derniers = await pool.query( + 'SELECT TOP 10 id, prenom, nom, email, CollaborateurADId, actif FROM CollaborateurAD ORDER BY id DESC', + [] + ); + + // Test Entra + let entraStatus = { connected: false }; + try { + const token = await getGraphToken(); + if (token) { + const groupResponse = await axios.get( + `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, + { headers: { Authorization: `Bearer ${token}` } } + ); + entraStatus = { + connected: true, + groupName: groupResponse.data.displayName + }; + } + } catch (err) { + entraStatus.error = err.message; + } + + res.json({ + success: true, + database: { + total: totalDB[0]?.total || 0, + actifs: totalDB[0]?.actifs || 0, + sansEmail: sansEmail[0]?.total || 0 + }, + entraId: entraStatus, + derniers_utilisateurs: derniers + }); + + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// GET - Compteur de demandes en attente pour le badge +app.get('/api/getPendingRequestsCount', async (req, res) => { + try { + const userId = req.query.user_id; + const userRole = req.query.role; + const userEmail = req.query.email; + + if (!userId) { + return res.json({ success: false, message: 'ID utilisateur manquant' }); + } + + const conn = await pool.getConnection(); + + let count = 0; + + // Normaliser le rôle + const role = normalizeRole(userRole); + + console.log(`🔍 Comptage demandes pour: ${userEmail}, Role: ${role}`); + + if (role === 'rh' || role === 'admin' || role === 'president' || role === 'directeur de campus') { + // Pour RH/Admin/President/Directeur : toutes les demandes en attente de leur périmètre + + const [userInfo] = await conn.query( + 'SELECT ServiceId, CampusId, SocieteId FROM CollaborateurAD WHERE id = ?', + [userId] + ); + + if (userInfo.length === 0) { + conn.release(); + return res.json({ success: false, message: 'Utilisateur non trouvé' }); + } + + const serviceId = userInfo[0].ServiceId; + const campusId = userInfo[0].CampusId; + + // ⭐ Vérifier accès transversal + const accesTransversal = getUserAccesTransversal(userEmail); + + if (accesTransversal) { + // Accès service multi-campus + const [result] = await conn.query(` + SELECT COUNT(DISTINCT dc.Id) as count + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN Services s ON ca.ServiceId = s.Id + WHERE dc.Statut = 'En attente' + AND s.Nom = ? + `, [accesTransversal.serviceNom]); + + count = result[0].count; + + } else if (role === 'directeur de campus') { + // Directeur de campus : toutes les demandes de son campus + const [result] = await conn.query(` + SELECT COUNT(DISTINCT dc.Id) as count + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + WHERE dc.Statut = 'En attente' + AND ca.CampusId = ? + `, [campusId]); + + count = result[0].count; + + } else { + // RH : son service sur son campus uniquement + const [result] = await conn.query(` + SELECT COUNT(DISTINCT dc.Id) as count + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + WHERE dc.Statut = 'En attente' + AND ca.ServiceId = ? + AND ca.CampusId = ? + `, [serviceId, campusId]); + + count = result[0].count; + } + + } else if (role === 'validateur' || role === 'validatrice') { + // Pour validateurs : demandes de leur service + const [userInfo] = await conn.query( + 'SELECT ServiceId, CampusId FROM CollaborateurAD WHERE id = ?', + [userId] + ); + + if (userInfo.length === 0) { + conn.release(); + return res.json({ success: false, message: 'Utilisateur non trouvé' }); + } + + const serviceId = userInfo[0].ServiceId; + const campusId = userInfo[0].CampusId; + + const [result] = await conn.query(` + SELECT COUNT(DISTINCT dc.Id) as count + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + WHERE dc.Statut = 'En attente' + AND ca.ServiceId = ? + AND ca.CampusId = ? + AND ca.id != ? + `, [serviceId, campusId, userId]); + + count = result[0].count; + } + + conn.release(); + + console.log(`✅ Nombre de demandes en attente: ${count}`); + + res.json({ + success: true, + count: count, + role: role + }); + + } catch (error) { + console.error('❌ Erreur getPendingRequestsCount:', error); + res.status(500).json({ + success: false, + message: 'Erreur serveur', + error: error.message + }); + } +}); + +app.listen(PORT, "0.0.0.0", async () => { + console.log("✅ =========================================="); + console.log(" SERVEUR PRINCIPAL DÉMARRÉ"); + console.log(" Port:", PORT); + console.log(` Base: ${dbConfig.database}@${dbConfig.server}`); + console.log("=========================================="); + // ⚡ Synchronisation Entra ID au démarrage (après 5 secondes) + setTimeout(async () => { + console.log("🚀 Lancement synchronisation Entra ID..."); + await syncEntraIdUsers(); + }, 5000); }); \ No newline at end of file diff --git a/project/src/App.jsx b/project/src/App.jsx index 0c402e0..3915bf1 100644 --- a/project/src/App.jsx +++ b/project/src/App.jsx @@ -39,7 +39,7 @@ function AppContent() { + } @@ -85,7 +85,7 @@ function AppContent() { + } diff --git a/project/src/AuthConfig.js b/project/src/AuthConfig.js index 8ab94fc..51e9154 100644 --- a/project/src/AuthConfig.js +++ b/project/src/AuthConfig.js @@ -1,34 +1,46 @@ // authConfig.js - const hostname = window.location.hostname; const protocol = window.location.protocol; -// Détection environnements (utile pour le debug) +// Détection environnements const isProduction = hostname === "mygta.ensup-adm.net"; -// --- API URL --- -// On utilise TOUJOURS /api car le proxy Vite (port 80) va rediriger vers le backend (port 3000) -// Cela évite les problèmes CORS et les problèmes de ports fermés (8000) +// ✅ EXPORT : API URL export const API_BASE_URL = "/api"; -// --- MSAL Config --- +// ✅ EXPORT : MSAL Config - OPTIMISÉ POUR MOBILE iOS export const msalConfig = { auth: { clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561", authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9", - - // En prod, on force l'URL sans slash final pour être propre - redirectUri: isProduction - ? "https://mygta.ensup-adm.net" + redirectUri: isProduction + ? "https://mygta.ensup-adm.net" + : `${protocol}//${hostname}`, + navigateToLoginRequestUrl: false, // ✅ false pour éviter double redirection + postLogoutRedirectUri: isProduction + ? "https://mygta.ensup-adm.net" : `${protocol}//${hostname}`, }, cache: { - cacheLocation: "sessionStorage", - storeAuthStateInCookie: false, + cacheLocation: "localStorage", + storeAuthStateInCookie: true, }, + system: { + allowRedirectInIframe: false, + allowNativeBroker: false, + loggerOptions: { + logLevel: "Verbose", + piiLoggingEnabled: false, + }, + windowHashTimeout: 25000, + iframeHashTimeout: 25000, + loadFrameTimeout: 25000, + tokenRenewalOffsetSeconds: 300, + asyncPopups: false, + } }; -// --- Permissions Graph --- +// ✅ EXPORT : Permissions Graph export const loginRequest = { scopes: [ "User.Read", @@ -37,11 +49,14 @@ export const loginRequest = { "GroupMember.Read.All", "Mail.Send", ], + prompt: "select_account", + responseMode: "fragment", }; +// ✅ Log de configuration au démarrage console.log("🔧 Config Auth:", { hostname, protocol, API_BASE_URL, - redirectUri: msalConfig.auth.redirectUri, -}); + redirectUri: msalConfig.auth.redirectUri +}); \ No newline at end of file diff --git a/project/src/components/EditLeaveRequestModal.jsx b/project/src/components/EditLeaveRequestModal.jsx index 73e0267..016124c 100644 --- a/project/src/components/EditLeaveRequestModal.jsx +++ b/project/src/components/EditLeaveRequestModal.jsx @@ -1,70 +1,101 @@ import React, { useState, useEffect } from 'react'; -import { X, Calendar, AlertCircle, Upload } from 'lucide-react'; +import { X, AlertCircle, Upload, FileText, Image as ImageIcon, Trash2 } from 'lucide-react'; const EditLeaveRequestModal = ({ + isOpen, onClose, request, + onRequestUpdated, availableLeaveCounters, - accessToken, userId, userEmail, - userRole, userName, - onRequestUpdated + accessToken }) => { - const [leaveType, setLeaveType] = useState(request.typeId || ''); - const [startDate, setStartDate] = useState(request.startDate || ''); - const [endDate, setEndDate] = useState(request.endDate || ''); - const [reason, setReason] = useState(request.reason || ''); - const [businessDays, setBusinessDays] = useState(request.days || 0); + // ======================================== + // ÉTATS + // ======================================== + const [selectedTypes, setSelectedTypes] = useState([]); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + const [reason, setReason] = useState(''); + const [businessDays, setBusinessDays] = useState(0); const [saturdayCount, setSaturdayCount] = useState(0); + + // Répartition manuelle (multi-types) + const [repartition, setRepartition] = useState({}); + + // Période par type (Matin/Après-midi/Journée entière) + const [periodeSelection, setPeriodeSelection] = useState({}); + + // Documents médicaux const [medicalDocuments, setMedicalDocuments] = useState([]); - const [errors, setErrors] = useState({}); + const [isDragging, setIsDragging] = useState(false); + + // Compteurs + const [countersData, setCountersData] = useState(null); + const [isLoadingCounters, setIsLoadingCounters] = useState(true); + + // UI const [isSubmitting, setIsSubmitting] = useState(false); const [submitMessage, setSubmitMessage] = useState({ type: '', text: '' }); + const [validationErrors, setValidationErrors] = useState([]); - // ⭐ Types de congés disponibles selon le rôle - const getLeaveTypes = () => { - const baseTypes = [ - { id: 1, name: 'Congé payé', key: 'CP', counter: availableLeaveCounters.availableCP }, - ]; + const availableTypes = [ + { id: 'CP', label: 'Congé payé', color: '#3b82f6' }, + { id: 'RTT', label: 'RTT', color: '#8b5cf6' }, + { id: 'Récup', label: 'Récupération', color: '#10b981' }, + { id: 'ABS', label: 'Arrêt maladie', color: '#ef4444' }, + { id: 'Formation', label: 'Formation', color: '#f59e0b' } + ]; - // Ajouter RTT sauf pour les apprentis - if (userRole !== 'Apprenti') { - baseTypes.push({ - id: 2, - name: 'RTT', - key: 'RTT', - counter: availableLeaveCounters.availableRTT - }); - } - - // Ajouter les types sans compteur - baseTypes.push( - { id: 3, name: 'Arrêt maladie', key: 'ABS', counter: null }, - { id: 5, name: 'Récupération (samedi)', key: 'Récup', counter: null } - ); - - // Ajouter Formation pour les apprentis - if (userRole === 'Apprenti') { - baseTypes.push({ id: 4, name: 'Formation', key: 'Formation', counter: null }); - } - - return baseTypes; - }; - - const leaveTypes = getLeaveTypes(); - - // ⭐ Calcul des jours ouvrés ET des samedis + // ======================================== + // INITIALISATION + // ======================================== useEffect(() => { - if (startDate && endDate) { - const result = calculateBusinessDaysAndSaturdays(startDate, endDate); - setBusinessDays(result.workingDays); - setSaturdayCount(result.saturdays); - } - }, [startDate, endDate]); + if (isOpen && request) { + console.log('📝 Initialisation EditModal avec request:', request); - const calculateBusinessDaysAndSaturdays = (start, end) => { + // Dates + setStartDate(request.startDate || ''); + setEndDate(request.endDate || ''); + setReason(request.reason || ''); + + // Types (mapping inverse) + const typeMapping = { + 'Congé payé': 'CP', + 'RTT': 'RTT', + 'Récupération': 'Récup', + 'Congé maladie': 'ABS', + 'Formation': 'Formation' + }; + + if (request.type) { + const types = request.type.split(', ').map(t => typeMapping[t] || t); + setSelectedTypes(types); + console.log('✅ Types initialisés:', types); + } + + // Calculer jours ouvrés + if (request.startDate && request.endDate) { + const days = calculateBusinessDays(request.startDate, request.endDate); + setBusinessDays(days.businessDays); + setSaturdayCount(days.saturdayCount); + } + } + }, [isOpen, request]); + + // Charger les compteurs + useEffect(() => { + if (isOpen && userId) { + loadCounters(); + } + }, [isOpen, userId]); + + // ======================================== + // FONCTIONS UTILITAIRES + // ======================================== + const calculateBusinessDays = (start, end) => { const startD = new Date(start); const endD = new Date(end); let workingDays = 0; @@ -75,96 +106,206 @@ const EditLeaveRequestModal = ({ const dayOfWeek = current.getDay(); if (dayOfWeek === 6) { saturdays++; - } else if (dayOfWeek !== 0) { // Pas dimanche + } else if (dayOfWeek !== 0) { workingDays++; } current.setDate(current.getDate() + 1); } - return { workingDays, saturdays }; + return { businessDays: workingDays, saturdayCount: saturdays }; }; - const validateForm = () => { - const newErrors = {}; + const loadCounters = async () => { + setIsLoadingCounters(true); + try { + const response = await fetch(`/api/getDetailedLeaveCounters?user_id=${userId}`); + const data = await response.json(); - if (!leaveType) { - newErrors.leaveType = 'Veuillez sélectionner un type de congé'; + if (data.success) { + setCountersData(data); + console.log('✅ Compteurs chargés:', data); + } + } catch (error) { + console.error('❌ Erreur chargement compteurs:', error); + } finally { + setIsLoadingCounters(false); + } + }; + + // ======================================== + // GESTION DES TYPES + // ======================================== + const handleTypeToggle = (typeId) => { + if (typeId === 'ABS' && selectedTypes.length > 0 && !selectedTypes.includes('ABS')) { + alert('⚠️ L\'arrêt maladie ne peut pas être combiné avec d\'autres types'); + return; } - if (!startDate) { - newErrors.startDate = 'La date de début est requise'; + if (selectedTypes.includes('ABS') && typeId !== 'ABS') { + alert('⚠️ L\'arrêt maladie ne peut pas être combiné avec d\'autres types'); + return; } - if (!endDate) { - newErrors.endDate = 'La date de fin est requise'; + setSelectedTypes(prev => { + if (prev.includes(typeId)) { + const newTypes = prev.filter(t => t !== typeId); + const newRep = { ...repartition }; + delete newRep[typeId]; + setRepartition(newRep); + + const newPeriodes = { ...periodeSelection }; + delete newPeriodes[typeId]; + setPeriodeSelection(newPeriodes); + + return newTypes; + } else { + return [...prev, typeId]; + } + }); + }; + + // ======================================== + // GESTION RÉPARTITION + // ======================================== + const handleRepartitionChange = (typeId, value) => { + const numValue = parseFloat(value) || 0; + const maxValue = businessDays; + + if (numValue > maxValue) { + alert(`Maximum ${maxValue} jours`); + return; } - if (startDate && endDate && new Date(startDate) > new Date(endDate)) { - newErrors.endDate = 'La date de fin doit être après la date de début'; - } + setRepartition(prev => ({ + ...prev, + [typeId]: numValue + })); + }; - // ⭐ Validation spécifique pour Récupération - const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType)); - if (selectedType?.key === 'Récup') { - if (saturdayCount === 0) { - newErrors.days = 'Une récupération nécessite au moins un samedi dans la période sélectionnée'; + const handlePeriodeChange = (typeId, periode) => { + setPeriodeSelection(prev => ({ + ...prev, + [typeId]: periode + })); + + // Calcul automatique si un seul type + if (selectedTypes.length === 1 && startDate === endDate) { + if (periode === 'Matin' || periode === 'Après-midi') { + setRepartition({ [typeId]: 0.5 }); + } else { + setRepartition({ [typeId]: businessDays }); } } - - // ⭐ Validation spécifique pour Arrêt maladie - if (selectedType?.key === 'ABS' && medicalDocuments.length === 0) { - newErrors.medical = 'Un justificatif médical est obligatoire pour un arrêt maladie'; - } - - // Vérification du solde disponible (CP et RTT uniquement) - if (selectedType && selectedType.counter !== null && businessDays > selectedType.counter) { - newErrors.days = `Solde insuffisant. Disponible : ${selectedType.counter} jour(s)`; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; }; - const handleFileUpload = (e) => { + // ======================================== + // GESTION FICHIERS MÉDICAUX + // ======================================== + const handleFileSelect = (e) => { const files = Array.from(e.target.files); - const validFiles = []; - const maxSize = 5 * 1024 * 1024; // 5MB + addFiles(files); + }; - for (const file of files) { - const validTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']; - if (!validTypes.includes(file.type)) { - setSubmitMessage({ - type: 'error', - text: `Le fichier "${file.name}" n'est pas un format valide.` - }); - continue; + const handleDrop = (e) => { + e.preventDefault(); + setIsDragging(false); + const files = Array.from(e.dataTransfer.files); + addFiles(files); + }; + + const addFiles = (files) => { + const validFiles = files.filter(file => { + const isValidType = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'].includes(file.type); + const isValidSize = file.size <= 5 * 1024 * 1024; + + if (!isValidType) { + alert(`❌ ${file.name}: Type non autorisé (PDF, JPG, PNG uniquement)`); + return false; } - if (file.size > maxSize) { - setSubmitMessage({ - type: 'error', - text: `Le fichier "${file.name}" est trop volumineux (max 5MB).` - }); - continue; + if (!isValidSize) { + alert(`❌ ${file.name}: Taille max 5MB`); + return false; } - validFiles.push(file); - } + return true; + }); setMedicalDocuments(prev => [...prev, ...validFiles]); - e.target.value = ''; }; - const removeDocument = (index) => { + const removeFile = (index) => { setMedicalDocuments(prev => prev.filter((_, i) => i !== index)); }; - const formatFileSize = (bytes) => { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + // ======================================== + // VALIDATION + // ======================================== + const validateForm = () => { + const errors = []; + + // Dates + if (!startDate || !endDate) { + errors.push('Les dates sont obligatoires'); + } else if (new Date(startDate) > new Date(endDate)) { + errors.push('La date de fin doit être après la date de début'); + } + + // Types + if (selectedTypes.length === 0) { + errors.push('Sélectionnez au moins un type de congé'); + } + + // Documents pour ABS + if (selectedTypes.includes('ABS') && medicalDocuments.length === 0) { + errors.push('Un justificatif médical est obligatoire pour un arrêt maladie'); + } + + // Répartition + if (selectedTypes.length > 1) { + const total = Object.values(repartition).reduce((sum, val) => sum + val, 0); + if (Math.abs(total - businessDays) > 0.01) { + errors.push(`La répartition (${total.toFixed(1)}j) ne correspond pas au total (${businessDays}j)`); + } + } + + // Compteurs (si chargés) + if (countersData?.data?.totalDisponible) { + const safeCounters = { + availableCP: countersData.data.cpN?.solde || 0, + availableRTT: countersData.data.rttN?.solde || 0, + availableRecup: countersData.data.recupN?.solde || 0 + }; + + selectedTypes.forEach(type => { + if (type === 'CP') { + const cpDemande = selectedTypes.length === 1 ? businessDays : (repartition[type] || 0); + if (cpDemande > safeCounters.availableCP) { + errors.push(`Solde CP insuffisant (${safeCounters.availableCP.toFixed(1)}j disponibles)`); + } + } + + if (type === 'RTT') { + const rttDemande = selectedTypes.length === 1 ? businessDays : (repartition[type] || 0); + if (rttDemande > safeCounters.availableRTT) { + errors.push(`Solde RTT insuffisant (${safeCounters.availableRTT.toFixed(1)}j disponibles)`); + } + } + + if (type === 'Récup') { + const recupDemande = selectedTypes.length === 1 ? businessDays : (repartition[type] || 0); + if (recupDemande > safeCounters.availableRecup) { + errors.push(`Solde Récup insuffisant (${safeCounters.availableRecup.toFixed(1)}j disponibles)`); + } + } + }); + } + + setValidationErrors(errors); + return errors.length === 0; }; + // ======================================== + // SOUMISSION + // ======================================== const handleSubmit = async (e) => { e.preventDefault(); @@ -178,57 +319,127 @@ const EditLeaveRequestModal = ({ try { const formDataToSend = new FormData(); - // ⭐ Ajouter tous les champs texte AVANT les fichiers + // ⭐ CHAMPS REQUIS PAR LE BACKEND formDataToSend.append('requestId', request.id.toString()); - formDataToSend.append('leaveType', leaveType.toString()); - formDataToSend.append('startDate', startDate); - formDataToSend.append('endDate', endDate); - formDataToSend.append('reason', reason || ''); formDataToSend.append('userId', userId.toString()); formDataToSend.append('userEmail', userEmail); formDataToSend.append('userName', userName); formDataToSend.append('accessToken', accessToken || ''); - // ⭐ Calcul des jours selon le type - const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType)); - const daysToSend = selectedType?.key === 'Récup' ? saturdayCount : businessDays; - formDataToSend.append('businessDays', daysToSend.toString()); + // ⭐ DATES + formDataToSend.append('DateDebut', startDate); + formDataToSend.append('DateFin', endDate); + formDataToSend.append('startDate', startDate); + formDataToSend.append('endDate', endDate); - // ⭐ Documents médicaux EN DERNIER + // ⭐ COMMENTAIRE + formDataToSend.append('Commentaire', reason || 'Aucun commentaire'); + formDataToSend.append('reason', reason || 'Aucun commentaire'); + + // ⭐ CALCUL NOMBRE DE JOURS TOTAL + let totalJoursToSend = businessDays; + + if (selectedTypes.length === 1 && startDate === endDate) { + const type = selectedTypes[0]; + const periode = periodeSelection[type]; + + if ((type === 'CP' || type === 'RTT' || type === 'Récup') && + (periode === 'Matin' || periode === 'Après-midi')) { + totalJoursToSend = 0.5; + } + } + + formDataToSend.append('NombreJours', totalJoursToSend.toString()); + formDataToSend.append('businessDays', totalJoursToSend.toString()); + + // ⭐ RÉPARTITION (CORRECTION ICI) + const repartitionArray = selectedTypes.map(type => { + let nombreJours; + let periodeJournee = 'Journée entière'; + + if (selectedTypes.length === 1) { + const periode = periodeSelection[type] || 'Journée entière'; + + if ((type === 'CP' || type === 'RTT' || type === 'Récup') && + startDate === endDate && + (periode === 'Matin' || periode === 'Après-midi')) { + nombreJours = 0.5; + periodeJournee = periode; + } else { + nombreJours = businessDays; + } + } else { + nombreJours = repartition[type] || 0; + periodeJournee = periodeSelection[type] || 'Journée entière'; + } + + return { + TypeConge: type, + NombreJours: nombreJours, + PeriodeJournee: ['CP', 'RTT', 'Récup'].includes(type) ? periodeJournee : 'Journée entière' + }; + }); + + // ⭐ STRINGIFIER LA RÉPARTITION (CRITIQUE POUR FORMDATA) + formDataToSend.append('Repartition', JSON.stringify(repartitionArray)); + + // ⭐ TYPE DE CONGÉ (pour compatibilité backend) + const leaveTypeMapping = { + 'CP': 1, + 'RTT': 2, + 'ABS': 3, + 'Formation': 4, + 'Récup': 5 + }; + const leaveTypeId = leaveTypeMapping[selectedTypes[0]] || 1; + formDataToSend.append('leaveType', leaveTypeId.toString()); + + // Documents médicaux EN DERNIER if (medicalDocuments.length > 0) { medicalDocuments.forEach((file) => { formDataToSend.append('medicalDocuments', file); }); } - // ⭐ DEBUG : Vérifier le contenu - console.log('📤 FormData à envoyer:'); + console.log('📤 Envoi modification demande...'); + console.log('📊 Répartition envoyée:', JSON.stringify(repartitionArray, null, 2)); + for (let pair of formDataToSend.entries()) { - console.log(pair[0], ':', pair[1]); + if (pair[0] !== 'medicalDocuments') { + console.log(pair[0], ':', pair[1]); + } } const response = await fetch('/api/updateRequest', { method: 'POST', - // ⭐ NE PAS mettre de Content-Type, le navigateur le fera automatiquement avec boundary body: formDataToSend }); + const responseText = await response.text(); + console.log('📥 Réponse brute:', responseText); + if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status} - ${responseText}`); } - const data = await response.json(); + let data; + try { + data = JSON.parse(responseText); + } catch (parseError) { + console.error('❌ Erreur parsing JSON:', parseError); + throw new Error('Réponse serveur invalide: ' + responseText); + } if (data.success) { setSubmitMessage({ type: 'success', - text: '✅ Demande modifiée avec succès ! Le manager a été informé par email.' + text: '✅ Demande modifiée avec succès !' }); setTimeout(() => { onRequestUpdated(); onClose(); - }, 2000); + }, 1500); } else { setSubmitMessage({ type: 'error', @@ -239,285 +450,333 @@ const EditLeaveRequestModal = ({ console.error('❌ Erreur:', error); setSubmitMessage({ type: 'error', - text: '❌ Une erreur est survenue. Veuillez réessayer.' + text: `❌ ${error.message || 'Une erreur est survenue'}` }); } finally { setIsSubmitting(false); } }; - const getMinDate = () => { - const today = new Date(); - return today.toISOString().split('T')[0]; - }; + // ======================================== + // RECALCUL AUTO JOURS OUVRÉS + // ======================================== + useEffect(() => { + if (startDate && endDate) { + const days = calculateBusinessDays(startDate, endDate); + setBusinessDays(days.businessDays); + setSaturdayCount(days.saturdayCount); - const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType)); + // Réinitialiser répartition si changement de dates + if (selectedTypes.length === 1) { + const type = selectedTypes[0]; + setRepartition({ [type]: days.businessDays }); + } + } + }, [startDate, endDate]); + + // ======================================== + // RENDER + // ======================================== + if (!isOpen) return null; return ( -
- {/* Overlay */} -
- - {/* Modal */} -
- {/* Header */} -
-

Modifier la demande

+
+
+ {/* HEADER */} +
+

+ ✏️ Modifier la demande +

- {/* Body */}
- {/* Message de statut */} - {submitMessage.text && ( -
- {submitMessage.text} -
- )} - - {/* Info - Demande originale */} -
-

- - Demande actuelle -

-
-

Type : {request.type}

-

Dates : {request.dateDisplay}

-

Jours : {request.days}

-
-
- - {/* Type de congé */} -
- - - {errors.leaveType && ( -

{errors.leaveType}

- )} -
- - {/* Dates */} + {/* DATES */}
-
- - setStartDate(e.target.value)} - min={getMinDate()} - className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.startDate ? 'border-red-500' : 'border-gray-300' - }`} - /> -
- {errors.startDate && ( -

{errors.startDate}

- )} + setStartDate(e.target.value)} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + required + />
-
- - setEndDate(e.target.value)} - min={startDate || getMinDate()} - className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.endDate ? 'border-red-500' : 'border-gray-300' - }`} - /> -
- {errors.endDate && ( -

{errors.endDate}

- )} + setEndDate(e.target.value)} + min={startDate} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + required + />
- {/* ⭐ Résumé de la période */} - {startDate && endDate && ( -
-

Résumé de la période :

-
-

{businessDays} jour(s) ouvré(s) (lundi-vendredi)

+ {/* RÉSUMÉ PÉRIODE */} + {businessDays > 0 && ( +
+
+ 📅 Période : + {businessDays} jour(s) ouvré(s) {saturdayCount > 0 && ( - <> -

• {saturdayCount} samedi(s) détecté(s)

- {selectedType?.key !== 'Récup' && ( -

- ⚠️ Les samedis seront ignorés (sélectionnez "Récupération" pour les inclure) -

- )} - {selectedType?.key === 'Récup' && ( -

- ✅ Récupération : {saturdayCount} samedi(s) seront comptabilisés -

- )} - + + {saturdayCount} samedi(s) )}
)} - {/* Nombre de jours */} - {(businessDays > 0 || saturdayCount > 0) && ( -
-

- Nombre de jours {selectedType?.key === 'Récup' ? '(samedis)' : 'ouvrés'} :{' '} - {selectedType?.key === 'Récup' ? saturdayCount : businessDays} -

- {errors.days && ( -

{errors.days}

- )} + {/* TYPES DE CONGÉ */} +
+ + + {isLoadingCounters ? ( +
+
+ Chargement des compteurs... +
+ ) : ( +
+ {availableTypes.map(type => { + const isSelected = selectedTypes.includes(type.id); + let counterDisplay = null; + + if (countersData?.data) { + if (type.id === 'CP') { + const solde = countersData.data.cpN?.solde || 0; + counterDisplay = `${solde.toFixed(1)}j`; + } else if (type.id === 'RTT') { + const solde = countersData.data.rttN?.solde || 0; + counterDisplay = `${solde.toFixed(1)}j`; + } else if (type.id === 'Récup') { + const solde = countersData.data.recupN?.solde || 0; + counterDisplay = `${solde.toFixed(1)}j`; + } + } + + return ( +
+ + {/* RÉPARTITION SI MULTI-TYPES */} + {selectedTypes.length > 1 && ( +
+

📊 Répartition des jours

+ {selectedTypes.map(type => ( +
+ + handleRepartitionChange(type, e.target.value)} + className="w-24 px-3 py-2 border rounded-lg" + /> + jour(s) + + {/* Période */} + {(type === 'CP' || type === 'RTT' || type === 'Récup') && ( +
+ {['Matin', 'Après-midi', 'Journée entière'].map(p => ( + + ))} +
+ )} +
+ ))}
)} - {/* ⭐ Upload documents médicaux pour Arrêt maladie */} - {selectedType?.key === 'ABS' && ( -
-