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 = `
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.
-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}.
-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. +
+Merci de valider ou refuser cette demande dans l'application.
-📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
-Elle est maintenant en attente de validation.
-📧 Cet email est envoyé automatiquement, merci de ne pas y répondre.
-