diff --git a/Dockerfile.frontend b/Dockerfile.frontend deleted file mode 100644 index 9494d37..0000000 --- a/Dockerfile.frontend +++ /dev/null @@ -1,32 +0,0 @@ -# Étape 1 : Construction de l'application -FROM node:18-alpine AS builder - -# Définir le répertoire de travail -WORKDIR /app - -# Copier le package.json et package-lock.json depuis le dossier 'project' -# Le contexte de construction est './project' donc Docker peut les trouver -COPY package.json ./ -COPY package-lock.json ./ - -# Installer les dépendances -RUN npm install - -# Copier le reste des fichiers du dossier 'project' -# Cela inclut le dossier 'src' et tout le reste -COPY . . - -# Lancer la compilation de l'application pour la production -RUN npm run build - -# Étape 2 : Servir l'application avec Nginx -FROM nginx:alpine - -# Copier les fichiers du build de l'étape précédente -COPY --from=builder /app/build /usr/share/nginx/html - -# Exposer le port 80 -EXPOSE 80 - -# Commande pour démarrer Nginx -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index a7b89be..54a13ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,36 @@ services: - frontend: - image: ouijdaneim/gta-frontend:latest + backend: + build: + context: ./project/public/Backend + dockerfile: DockerfileGTA.backend + container_name: gta-backend + hostname: backend ports: - - "3000:80" + - "8012:3000" + volumes: + - ./project/public/Backend/uploads:/app/uploads + networks: + - gta-network + restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" + + frontend: + build: + context: ./project + dockerfile: DockerfileGTA.frontend + container_name: gta-frontend + hostname: frontend + ports: + - "3013:80" + environment: + - VITE_API_URL=http://backend:3000 + networks: + - gta-network depends_on: - backend + restart: unless-stopped - backend: - image: ouijdaneim/gta-backend:latest - ports: - - "8000:80" \ No newline at end of file +networks: + gta-network: + driver: bridge \ No newline at end of file diff --git a/project/DockerfileGTA.frontend b/project/DockerfileGTA.frontend new file mode 100644 index 0000000..6d5281f --- /dev/null +++ b/project/DockerfileGTA.frontend @@ -0,0 +1,53 @@ +FROM node:18-alpine + +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json ./ + +# Install all dependencies +RUN npm ci --legacy-peer-deps + +# Copy source code +COPY . . + +# Create vite.config.js with correct proxy settings +RUN cat > vite.config.js << 'VITECONFIG' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + host: '0.0.0.0', + port: 80, + strictPort: true, + allowedHosts: ['mygta.ensup-adm.net', 'localhost'], + proxy: { + '/api': { + target: 'http://gta-backend:3000', + changeOrigin: true, + secure: false, + configure: (proxy, options) => { + proxy.on('error', (err, req, res) => { + console.log('Proxy error:', err); + }); + proxy.on('proxyReq', (proxyReq, req, res) => { + console.log('Proxying:', req.method, req.url, '-> http://gta-backend:3000'); + }); + } + } + } + } +}); +VITECONFIG + +EXPOSE 80 + +CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "80"] diff --git a/project/convert-cert-docker.ps1 b/project/convert-cert-docker.ps1 new file mode 100644 index 0000000..ff61371 --- /dev/null +++ b/project/convert-cert-docker.ps1 @@ -0,0 +1,16 @@ +# Variables +$PFX_PATH = "C:\Users\oimer\.aspnet\https\aspnetapp.pfx" +$PASSWORD = "tGTF2025" + +Write-Host "Conversion du certificat via Docker..." -ForegroundColor Yellow + +# Convertir en certificat (.crt) +docker run --rm -v C:\Users\oimer\.aspnet\https:/certs alpine/openssl pkcs12 -in /certs/aspnetapp.pfx -clcerts -nokeys -out /certs/aspnetapp.crt -passin pass:$PASSWORD + +# Convertir en clé privée (.key) +docker run --rm -v C:\Users\oimer\.aspnet\https:/certs alpine/openssl pkcs12 -in /certs/aspnetapp.pfx -nocerts -nodes -out /certs/aspnetapp.key -passin pass:$PASSWORD + +Write-Host "`n✓ Certificats convertis avec succès!" -ForegroundColor Green +Write-Host "Fichiers créés:" -ForegroundColor Cyan +Write-Host " - C:\Users\oimer\.aspnet\https\aspnetapp.crt" -ForegroundColor White +Write-Host " - C:\Users\oimer\.aspnet\https\aspnetapp.key" -ForegroundColor White \ No newline at end of file diff --git a/project/package-lock.json b/project/package-lock.json index cabb7e0..0617205 100644 --- a/project/package-lock.json +++ b/project/package-lock.json @@ -15,11 +15,13 @@ "crypto": "^1.0.1", "dotenv": "^17.2.3", "express": "^5.1.0", + "framer-motion": "^12.23.24", "lucide-react": "^0.344.0", "multer": "^2.0.2", "mysql2": "^3.15.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-joyride": "^2.9.3", "react-router-dom": "^7.7.1" }, "devDependencies": { @@ -1040,6 +1042,12 @@ "node": ">=12" } }, + "node_modules/@gilbarbara/deep-equal": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz", + "integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2000,6 +2008,16 @@ "undici-types": "~7.10.0" } }, + "node_modules/@types/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", + "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3301,6 +3319,12 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -3333,6 +3357,12 @@ } } }, + "node_modules/deep-diff": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", + "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==", + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3347,7 +3377,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3961,6 +3990,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -4405,6 +4461,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-lite": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-1.2.1.tgz", + "integrity": "sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5825,6 +5887,21 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6342,6 +6419,17 @@ "node": ">=8" } }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -6524,6 +6612,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -6660,6 +6765,55 @@ "react": "^18.3.1" } }, + "node_modules/react-floater": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz", + "integrity": "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "is-lite": "^0.8.2", + "popper.js": "^1.16.0", + "prop-types": "^15.8.1", + "tree-changes": "^0.9.1" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-floater/node_modules/@gilbarbara/deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==", + "license": "MIT" + }, + "node_modules/react-floater/node_modules/is-lite": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-0.8.2.tgz", + "integrity": "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==", + "license": "MIT" + }, + "node_modules/react-floater/node_modules/tree-changes": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.9.3.tgz", + "integrity": "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.1.1", + "is-lite": "^0.8.2" + } + }, + "node_modules/react-innertext": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", + "integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">=0.0.0 <=99", + "react": ">=0.0.0 <=99" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -6667,6 +6821,47 @@ "dev": true, "license": "MIT" }, + "node_modules/react-joyride": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.9.3.tgz", + "integrity": "sha512-1+Mg34XK5zaqJ63eeBhqdbk7dlGCFp36FXwsEvgpjqrtyywX2C6h9vr3jgxP0bGHCw8Ilsp/nRDzNVq6HJ3rNw==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "deep-diff": "^1.0.2", + "deepmerge": "^4.3.1", + "is-lite": "^1.2.1", + "react-floater": "^0.7.9", + "react-innertext": "^1.1.5", + "react-is": "^16.13.1", + "scroll": "^3.0.1", + "scrollparent": "^2.1.0", + "tree-changes": "^0.11.2", + "type-fest": "^4.27.0" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-joyride/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-joyride/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -6931,6 +7126,18 @@ "loose-envify": "^1.1.0" } }, + "node_modules/scroll": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scroll/-/scroll-3.0.1.tgz", + "integrity": "sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==", + "license": "MIT" + }, + "node_modules/scrollparent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz", + "integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==", + "license": "ISC" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -7694,6 +7901,16 @@ "node": ">=0.6" } }, + "node_modules/tree-changes": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.11.3.tgz", + "integrity": "sha512-r14mvDZ6tqz8PRQmlFKjhUVngu4VZ9d92ON3tp0EGpFBE6PAHOq8Bx8m8ahbNoGE3uI/npjYcJiqVydyOiYXag==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "is-lite": "^1.2.1" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -7704,9 +7921,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-detect": { "version": "4.0.8", diff --git a/project/package.json b/project/package.json index aa21140..16db53c 100644 --- a/project/package.json +++ b/project/package.json @@ -16,11 +16,13 @@ "crypto": "^1.0.1", "dotenv": "^17.2.3", "express": "^5.1.0", + "framer-motion": "^12.23.24", "lucide-react": "^0.344.0", "multer": "^2.0.2", "mysql2": "^3.15.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-joyride": "^2.9.3", "react-router-dom": "^7.7.1" }, "devDependencies": { diff --git a/project/public/Backend/DockerfileGTA.backend b/project/public/Backend/DockerfileGTA.backend new file mode 100644 index 0000000..5a14dfd --- /dev/null +++ b/project/public/Backend/DockerfileGTA.backend @@ -0,0 +1,24 @@ +FROM node:18-alpine + +# Install required tools +RUN apk add --no-cache curl mysql-client python3 make g++ + +WORKDIR /app + +# Copy package files first for better caching +COPY package*.json ./ + +# Install dependencies +RUN npm install --production + +# Copy application code +COPY . . + +# Create uploads directory +RUN mkdir -p /app/uploads/medical + +# Expose the port +EXPOSE 3000 + +# Start the server +CMD ["node", "server.js"] diff --git a/project/public/Backend/package.json b/project/public/Backend/package.json new file mode 100644 index 0000000..4f1eec3 --- /dev/null +++ b/project/public/Backend/package.json @@ -0,0 +1,26 @@ +{ + "name": "gta-backend", + "version": "1.0.0", + "description": "GTA Backend API", + "main": "server.js", + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "express": "^4.18.2", + "mysql2": "^3.6.5", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "multer": "^1.4.5-lts.1", + "@microsoft/microsoft-graph-client": "^3.0.7", + "@azure/identity": "^4.0.0", + "body-parser": "^1.20.2", + "axios": "^1.6.0", + "node-cron": "^3.0.3" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/project/public/Backend/server-test.js b/project/public/Backend/server-test.js new file mode 100644 index 0000000..3c85606 --- /dev/null +++ b/project/public/Backend/server-test.js @@ -0,0 +1,124 @@ +import express from 'express'; +import cors from 'cors'; +import mysql from 'mysql2/promise'; + +const app = express(); +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, +}; + +// ✅ CRÉER LE POOL ICI, AU NIVEAU GLOBAL +const pool = mysql.createPool(dbConfig); + +// Route test connexion base + comptage collaborateurs +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; + res.json({ + success: true, + message: 'Connexion à la base OK', + collaboratorCount, + }); + } catch (error) { + console.error('Erreur connexion base:', error); + res.status(500).json({ + success: false, + message: 'Erreur de connexion à la base', + error: error.message, + }); + } +}); + +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); + + // 1. Chercher user + const [users] = await conn.query('SELECT * FROM CollaborateurAD WHERE email = ?', [email]); + let userId; + let userRole; + + 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 || '' + ]); + + 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'; + } + console.log('✅ User créé/récupéré:', userId); + } + + res.json({ + success: true, + localUserId: userId, + role: userRole + }); + } 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, + error: error.message + }); + } +}); + +app.listen(PORT, () => { + console.log(`✅ ✅ ✅ SERVEUR TEST DÉMARRÉ SUR LE PORT ${PORT} ✅ ✅ ✅`); +}); \ No newline at end of file diff --git a/project/public/Backend/server.js b/project/public/Backend/server.js index 59ce8a5..a0ebb1d 100644 --- a/project/public/Backend/server.js +++ b/project/public/Backend/server.js @@ -21,24 +21,37 @@ const PORT = 3000; const webhookManager = new WebhookManager(WEBHOOKS.SECRET_KEY); const sseClientsCollab = new Set(); +process.on('uncaughtException', (err) => { + console.error('💥 ERREUR CRITIQUE NON CATCHÉE:', err); + console.error('Stack:', err.stack); + // On ne crash pas pour pouvoir déboguer +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('💥 PROMESSE REJETÉE NON GÉRÉE:', reason); + console.error('Promise:', promise); +}); app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'] + origin: ['http://localhost:3013', 'http://localhost:80', 'https://mygta.ensup-adm.net'], + credentials: true })); + 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' }; + function nowFR() { const d = new Date(); d.setHours(d.getHours() + 2); @@ -410,13 +423,88 @@ const notifyCollabClients = (event, targetUserId = null) => { } }; +const sseClients = new Set(); + +// 🔌 ROUTE SSE POUR LE CALENDRIER +app.get('/api/sse', (req, res) => { + const userId = req.query.user_id; + + if (!userId) { + return res.status(400).json({ error: 'user_id requis' }); + } + + console.log('🔌 Nouvelle connexion SSE:', userId); + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + + const sendEvent = (data) => { + try { + res.write(`data: ${JSON.stringify(data)}\n\n`); + } catch (error) { + console.error('❌ Erreur envoi SSE:', error); + } + }; + + const client = { id: userId, send: sendEvent }; + sseClients.add(client); + + console.log(`📊 Clients SSE connectés: ${sseClients.size}`); + + // Envoyer un heartbeat initial + sendEvent({ + type: 'ping', + message: 'Connexion établie', + timestamp: new Date().toISOString() + }); + + // Heartbeat toutes les 30 secondes + const heartbeat = setInterval(() => { + try { + sendEvent({ type: 'ping', timestamp: new Date().toISOString() }); + } catch (error) { + console.error('❌ Erreur heartbeat:', error); + clearInterval(heartbeat); + } + }, 30000); + + req.on('close', () => { + console.log('🔌 Déconnexion SSE:', userId); + clearInterval(heartbeat); + sseClients.delete(client); + console.log(`📊 Clients SSE connectés: ${sseClients.size}`); + }); +}); + +// 📢 FONCTION POUR NOTIFIER LES CLIENTS +const notifyClients = (event, userId = null) => { + console.log(`📢 Notification SSE: ${event.type}${userId ? ` pour ${userId}` : ''}`); + + sseClients.forEach(client => { + // Si userId est spécifié, envoyer seulement à ce client + if (userId && client.id !== userId) { + return; + } + + try { + client.send(event); + } catch (error) { + console.error('❌ Erreur envoi event:', error); + } + }); +}; + app.post('/api/webhook/receive', async (req, res) => { try { const signature = req.headers['x-webhook-signature']; const payload = req.body; - console.log('📥 Webhook reçu:', payload.event); + console.log('\n📥 === WEBHOOK REÇU (COLLABORATEURS) ==='); + console.log('Event:', payload.event); + console.log('Data:', JSON.stringify(payload.data, null, 2)); // Vérifier la signature if (!webhookManager.verifySignature(payload, signature)) { @@ -428,134 +516,149 @@ app.post('/api/webhook/receive', async (req, res) => { // Traiter selon le type d'événement switch (event) { - case EVENTS.DEMANDE_VALIDATED: - console.log(`📥 Validation reçue: Demande ${data.demandeId} - Statut: ${data.statut}`); + case EVENTS.COMPTEUR_UPDATED: + console.log('📊 WEBHOOK COMPTEUR_UPDATED REÇ'); + 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('Source:', data.source); - const conn = await pool.getConnection(); - try { - await conn.beginTransaction(); + // SI MODIFICATION RH OU RECALCUL, METTRE À JOUR LA BASE LOCALE + if ((data.source === 'rh' || data.source === 'recalcul') && + data.nouveauTotal !== undefined && + data.nouveauSolde !== undefined) { - // ⭐ GESTION DES COMPTEURS SELON LE STATUT - if (data.statut === 'Refusée' && data.collaborateurId) { - console.log(`❌ DEMANDE REFUSÉE - Restauration des soldes...`); + console.log('🔄 Synchronisation depuis RH (source:', data.source + ')...'); + console.log('Nouveau Total:', data.nouveauTotal + 'j'); + console.log('Nouveau Solde:', data.nouveauSolde + 'j'); - // Restaurer les soldes via la fonction existante - const restoration = await restoreLeaveBalance( - conn, - data.demandeId, - data.collaborateurId + 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] ); - console.log('✅ Restauration terminée:', restoration); + if (typeRow.length > 0) { + const typeCongeId = typeRow[0].Id; - } else if (data.statut === 'Validée') { - console.log(`✅ DEMANDE VALIDÉE - Les jours ont déjà été déduits à la création`); + // 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(); } - - // ⭐ CRÉER UNE NOTIFICATION EN BASE DE DONNÉES - const notifTitle = data.statut === 'Validée' - ? 'Demande approuvée ✅' - : 'Demande refusée ❌'; - - let notifMessage = `Votre demande a été ${data.statut === 'Validée' ? 'approuvée' : 'refusée'}`; - if (data.commentaire) { - notifMessage += ` (Commentaire: ${data.commentaire})`; - } - - const notifType = data.statut === 'Validée' ? 'Success' : 'Error'; - - // ✅ FIX : Remplacer NOW() par nowFR() - await conn.query( - 'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)', - [data.collaborateurId, notifTitle, notifMessage, notifType, data.demandeId, nowFR()] - ); - - console.log('✅ Notification créée en base de données'); - - await conn.commit(); - - } catch (error) { - await conn.rollback(); - console.error('❌ Erreur traitement webhook:', error); - throw error; - } finally { - conn.release(); } - // Notifier les clients SSE - notifyCollabClients({ - type: 'demande-validated-rh', - demandeId: data.demandeId, - statut: data.statut, - timestamp: new Date().toISOString() - }, data.collaborateurId); - - notifyCollabClients({ - type: 'demande-list-updated', - action: 'validation-rh', - demandeId: data.demandeId, - timestamp: new Date().toISOString() - }); - break; - - case EVENTS.COMPTEUR_UPDATED: - console.log(`🔄 Compteur mis à jour pour collaborateur ${data.collaborateurId}`); - + // NOTIFIER LE CLIENT SSE DU COLLABORATEUR notifyCollabClients({ type: 'compteur-updated', collaborateurId: data.collaborateurId, + typeConge: data.typeConge, + annee: data.annee, + typeUpdate: data.typeUpdate, + nouveauTotal: data.nouveauTotal, + nouveauSolde: data.nouveauSolde, + source: data.source, // Garder la source originale 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}`); + console.log(` Statut: ${data.statut}`); + console.log(` Type: ${data.typeConge}`); + console.log(` Couleur: ${data.couleurHex}`); + + // Notifier les clients SSE avec TOUTES les infos + notifyClients({ + type: 'demande-validated', + demandeId: data.demandeId, + statut: data.statut, + typeConge: data.typeConge, + couleurHex: data.couleurHex || '#d946ef', + date: data.date, + periode: data.periode, + collaborateurId: data.collaborateurId, + timestamp: new Date().toISOString() + }, data.collaborateurId); + + // Notifier les RH aussi + notifyClients({ + type: 'demande-list-updated', + action: 'validation-collab', + demandeId: data.demandeId, + statut: data.statut, + typeConge: data.typeConge, + couleurHex: data.couleurHex || '#d946ef', + timestamp: new Date().toISOString() + }); + + console.log(' 📢 Notifications SSE envoyées'); break; case EVENTS.DEMANDE_UPDATED: - console.log(`✏️ Demande ${data.demandeId} modifiée via RH`); - - // ⭐ CRÉER UNE NOTIFICATION POUR LA MODIFICATION - const connUpdate = await pool.getConnection(); - try { - // ✅ FIX : Remplacer NOW() par nowFR() - await connUpdate.query( - 'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)', - [data.collaborateurId, 'Demande modifiée ✏️', 'Votre demande a été modifiée par le service RH', 'Info', data.demandeId, nowFR()] - ); - console.log('✅ Notification modification créée'); - } catch (error) { - console.error('❌ Erreur création notification:', error); - } finally { - connUpdate.release(); - } + console.log('\n✏️ === WEBHOOK DEMANDE_UPDATED REÇU ==='); + console.log(` Demande: ${data.demandeId}`); + console.log(` Collaborateur: ${data.collaborateurId}`); notifyCollabClients({ type: 'demande-updated-rh', demandeId: data.demandeId, timestamp: new Date().toISOString() }, data.collaborateurId); + + console.log(' 📢 Notification modification envoyée'); break; case EVENTS.DEMANDE_DELETED: - console.log(`🗑️ Demande ${data.demandeId} supprimée via RH`); - - // ⭐ CRÉER UNE NOTIFICATION POUR LA SUPPRESSION - const connDelete = await pool.getConnection(); - try { - // ✅ FIX : Remplacer NOW() par nowFR() - await connDelete.query( - 'INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId, DateCreation, lu) VALUES (?, ?, ?, ?, ?, ?, 0)', - [data.collaborateurId, 'Demande supprimée 🗑️', 'Votre demande a été supprimée par le service RH', 'Warning', data.demandeId, nowFR()] - ); - console.log('✅ Notification suppression créée'); - } catch (error) { - console.error('❌ Erreur création notification:', error); - } finally { - connDelete.release(); - } + console.log('\n🗑️ === WEBHOOK DEMANDE_DELETED REÇU ==='); + console.log(` Demande: ${data.demandeId}`); + console.log(` Collaborateur: ${data.collaborateurId}`); notifyCollabClients({ type: 'demande-deleted-rh', demandeId: data.demandeId, timestamp: new Date().toISOString() }, data.collaborateurId); + + console.log(' 📢 Notification suppression envoyée'); break; default: @@ -570,6 +673,132 @@ 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); @@ -612,9 +841,14 @@ const LEAVE_RULES = { /** * Récupère la configuration RTT pour une année et un type de contrat donnés + * RÈGLES : + * - 37h : toujours 10 RTT/an (0.8333/mois) + * - Forfait jour 2025 : 10 RTT/an (0.8333/mois) + * - Forfait jour 2026+ : 12 RTT/an (1.0/mois) */ async function getConfigurationRTT(conn, annee, typeContrat = '37h') { try { + // D'abord chercher en base de données const [config] = await conn.query( `SELECT JoursAnnuels, AcquisitionMensuelle FROM ConfigurationRTT @@ -630,68 +864,225 @@ async function getConfigurationRTT(conn, annee, typeContrat = '37h') { }; } - // Valeurs par défaut si pas de config trouvée - console.warn(`⚠️ Pas de config RTT pour ${annee}/${typeContrat}, utilisation des valeurs par défaut`); - return typeContrat === 'forfait_jour' - ? { joursAnnuels: 12, acquisitionMensuelle: 1.0 } - : { joursAnnuels: 10, acquisitionMensuelle: 0.833333 }; + // Si pas en base, utiliser les règles par défaut + console.warn(`⚠️ Pas de config RTT en base pour ${annee}/${typeContrat}, utilisation des règles par défaut`); + + return getConfigurationRTTDefaut(annee, typeContrat); + } catch (error) { console.error('Erreur getConfigurationRTT:', error); - // Retour valeur par défaut en cas d'erreur - return { joursAnnuels: 10, acquisitionMensuelle: 0.833333 }; + return getConfigurationRTTDefaut(annee, typeContrat); } } +function getConfigurationRTTDefaut(annee, typeContrat) { + // 37h : toujours 10 RTT/an + if (typeContrat === '37h' || typeContrat === 'temps_partiel') { + return { + joursAnnuels: 10, + acquisitionMensuelle: 10 / 12 // 0.8333 + }; + } + + // Forfait jour : dépend de l'année + if (typeContrat === 'forfait_jour') { + if (annee <= 2025) { + // 2025 et avant : 10 RTT/an + return { + joursAnnuels: 10, + acquisitionMensuelle: 10 / 12 // 0.8333 + }; + } else { + // 2026 et après : 12 RTT/an + return { + joursAnnuels: 12, + acquisitionMensuelle: 12 / 12 // 1.0 + }; + } + } + + // Par défaut : 10 RTT/an + return { + joursAnnuels: 10, + acquisitionMensuelle: 10 / 12 + }; +} + /** - * Calcule l'acquisition RTT en tenant compte du type de contrat et de l'année + * Calcule l'acquisition RTT avec la formule Excel exacte */ async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) { - try { - const d = new Date(dateReference); - const annee = d.getFullYear(); + const d = new Date(dateReference); + d.setHours(0, 0, 0, 0); + const annee = d.getFullYear(); - // Récupérer le type de contrat et la date d'entrée du collaborateur - const [collabInfo] = await conn.query( - `SELECT TypeContrat, DateEntree, CampusId FROM CollaborateurAD WHERE id = ?`, - [collaborateurId] - ); + // 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é`); - } + if (collabInfo.length === 0) { + throw new Error(`Collaborateur ${collaborateurId} non trouvé`); + } - const typeContrat = collabInfo[0].TypeContrat || '37h'; - const dateEntree = collabInfo[0].DateEntree; - - // Récupérer la configuration RTT pour l'année et le type de contrat - const config = await getConfigurationRTT(conn, annee, typeContrat); - - // Calculer les mois travaillés dans l'année - const moisTravailles = getMoisTravaillesRTT(dateReference, dateEntree); - - // Calculer l'acquisition cumulée - const acquisition = moisTravailles * config.acquisitionMensuelle; + 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: Math.round(acquisition * 100) / 100, - moisTravailles: parseFloat(moisTravailles.toFixed(2)), - config: config, + acquisition: 0, + moisTravailles: 0, + config: { joursAnnuels: 0, acquisitionMensuelle: 0 }, typeContrat: typeContrat }; - } catch (error) { - console.error('Erreur calculerAcquisitionRTT:', error); - throw error; } + + // 3️⃣ Récupérer la configuration RTT (avec règles 2025/2026) + const config = await getConfigurationRTT(conn, annee, typeContrat); + + console.log(`📊 Config RTT ${annee}/${typeContrat}: ${config.joursAnnuels}j/an (${config.acquisitionMensuelle.toFixed(4)}/mois)`); + + // 4️⃣ Début d'acquisition = 01/01/N ou date d'entrée si postérieure + let dateDebutAcquis = new Date(annee, 0, 1); // 01/01/N + dateDebutAcquis.setHours(0, 0, 0, 0); + + if (dateEntree) { + const entree = new Date(dateEntree); + entree.setHours(0, 0, 0, 0); + + if (entree.getFullYear() === annee && entree > dateDebutAcquis) { + dateDebutAcquis = entree; + } + + if (entree.getFullYear() > annee) { + return { + acquisition: 0, + moisTravailles: 0, + config: config, + typeContrat: typeContrat + }; + } + } + + // 5️⃣ Calculer avec la formule Excel + const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, config.acquisitionMensuelle); + + // 6️⃣ Calculer les mois travaillés (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 + }; } /** - * Calcule l'acquisition CP (inchangé, mais pour cohérence) + * Calcule l'acquisition avec la formule Excel exacte : + * E1 * ((JOUR(FIN.MOIS(B1;0)) - JOUR(B1) + 1) / JOUR(FIN.MOIS(B1;0)) + * + DATEDIF(B1;B2;"m") - 1 + * + JOUR(B2) / JOUR(FIN.MOIS(B2;0))) */ -function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) { - const moisTravailles = getMoisTravaillesCP(dateReference, dateEntree); - const acquisition = moisTravailles * (25 / 12); +function calculerAcquisitionFormuleExcel(dateDebut, dateReference, coeffMensuel) { + const b1 = new Date(dateDebut); + const b2 = new Date(dateReference); + b1.setHours(0, 0, 0, 0); + b2.setHours(0, 0, 0, 0); + + // Si date référence avant date début + if (b2 < b1) { + return 0; + } + + // Si même mois et même année + if (b1.getFullYear() === b2.getFullYear() && b1.getMonth() === b2.getMonth()) { + const joursTotal = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate(); + const joursAcquis = b2.getDate() - b1.getDate() + 1; + return Math.round((joursAcquis / joursTotal) * coeffMensuel * 100) / 100; + } + + // 1️⃣ Fraction du PREMIER mois + const joursFinMoisB1 = new Date(b1.getFullYear(), b1.getMonth() + 1, 0).getDate(); + const jourB1 = b1.getDate(); + const fractionPremierMois = (joursFinMoisB1 - jourB1 + 1) / joursFinMoisB1; + + // 2️⃣ Mois COMPLETS entre + const moisComplets = dateDifMonths(b1, b2) - 1; + + // 3️⃣ Fraction du DERNIER mois + const joursFinMoisB2 = new Date(b2.getFullYear(), b2.getMonth() + 1, 0).getDate(); + const jourB2 = b2.getDate(); + const fractionDernierMois = jourB2 / joursFinMoisB2; + + // 4️⃣ Total + const totalMois = fractionPremierMois + Math.max(0, moisComplets) + fractionDernierMois; + const acquisition = totalMois * coeffMensuel; + return Math.round(acquisition * 100) / 100; } +/** + * Équivalent de DATEDIF(date1, date2, "m") en JavaScript + */ +function dateDifMonths(date1, date2) { + const d1 = new Date(date1); + const d2 = new Date(date2); + + let months = (d2.getFullYear() - d1.getFullYear()) * 12; + months += d2.getMonth() - d1.getMonth(); + + // Si le jour de d2 < jour de d1, on n'a pas encore complété le mois + if (d2.getDate() < d1.getDate()) { + months--; + } + + return Math.max(0, months); +} +/** + * Calcule l'acquisition CP avec la formule Excel exacte + */ +function calculerAcquisitionCP(dateReference = new Date(), dateEntree = null) { + const d = new Date(dateReference); + d.setHours(0, 0, 0, 0); + + const annee = d.getFullYear(); + const mois = d.getMonth() + 1; + + // 1️⃣ Déterminer le début de l'exercice CP (01/06) + let exerciceDebut; + if (mois >= 6) { + exerciceDebut = new Date(annee, 5, 1); // 01/06/N + } else { + exerciceDebut = new Date(annee - 1, 5, 1); // 01/06/N-1 + } + exerciceDebut.setHours(0, 0, 0, 0); + + // 2️⃣ Ajuster si date d'entrée postérieure + let dateDebutAcquis = new Date(exerciceDebut); + if (dateEntree) { + const entree = new Date(dateEntree); + entree.setHours(0, 0, 0, 0); + if (entree > exerciceDebut) { + dateDebutAcquis = entree; + } + } + + // 3️⃣ Calculer avec la formule Excel + const coeffCP = 25 / 12; // 2.0833 + const acquisition = calculerAcquisitionFormuleExcel(dateDebutAcquis, d, coeffCP); + + // 4️⃣ Plafonner à 25 jours + return Math.min(acquisition, 25); +} + // ======================================== // TÂCHES CRON @@ -982,8 +1373,10 @@ function getExerciceCP(date = new Date()) { function getMoisTravaillesCP(date = new Date(), dateEntree = null) { const d = new Date(date); d.setHours(0, 0, 0, 0); + const annee = d.getFullYear(); const mois = d.getMonth() + 1; + let debutExercice; if (mois >= 6) { debutExercice = new Date(annee, 5, 1); @@ -991,6 +1384,7 @@ function getMoisTravaillesCP(date = new Date(), dateEntree = null) { debutExercice = new Date(annee - 1, 5, 1); } debutExercice.setHours(0, 0, 0, 0); + if (dateEntree) { const entree = new Date(dateEntree); entree.setHours(0, 0, 0, 0); @@ -998,12 +1392,16 @@ function getMoisTravaillesCP(date = new Date(), dateEntree = null) { debutExercice = entree; } } + + // ✅ Calculer jusqu'à aujourd'hui const diffMs = d - debutExercice; const diffJours = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1; const moisTravailles = diffJours / 30.44; + return Math.max(0, Math.min(12, moisTravailles)); } + function getMoisTravaillesRTT(date = new Date(), dateEntree = null) { const d = new Date(date); d.setHours(0, 0, 0, 0); @@ -1150,6 +1548,8 @@ async function checkLeaveBalance(conn, collaborateurId, repartition) { return { valide: insuffisants.length === 0, details: verification, insuffisants }; } + + // ======================================== // MISE À JOUR DE updateMonthlyCounters // ======================================== @@ -1159,55 +1559,191 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null const currentYear = today.getFullYear(); const updates = []; - const [collabInfo] = await conn.query( - 'SELECT DateEntree, TypeContrat, CampusId FROM CollaborateurAD WHERE id = ?', - [collaborateurId] - ); - const dateEntree = collabInfo.length > 0 && collabInfo[0].DateEntree ? collabInfo[0].DateEntree : null; - const typeContrat = collabInfo.length > 0 && collabInfo[0].TypeContrat ? collabInfo[0].TypeContrat : '37h'; + // Récupérer les infos du collaborateur + const [collabInfo] = await conn.query(` + SELECT DateEntree, TypeContrat, CampusId, role + FROM CollaborateurAD WHERE id = ? + `, [collaborateurId]); - // ===== CP (inchangé) ===== + if (collabInfo.length === 0) { + throw new Error(`Collaborateur ${collaborateurId} non trouvé`); + } + + const dateEntree = collabInfo[0].DateEntree || null; + const typeContrat = collabInfo[0].TypeContrat || '37h'; + const isApprenti = collabInfo[0].role === 'Apprenti'; + + console.log(`\n📊 === Mise à jour compteurs 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 [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); + if (cpType.length > 0) { const cpTypeId = cpType[0].Id; - const [existingCP] = await conn.query( - `SELECT Id, Total, Solde, SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, cpTypeId, currentYear] - ); + + // 1️⃣ Récupérer le compteur existant + const [existingCP] = await conn.query(` + SELECT Id, Total, Solde, SoldeReporte + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, cpTypeId, currentYear]); if (existingCP.length > 0) { - const ancienTotal = parseFloat(existingCP[0].Total); - const ancienSolde = parseFloat(existingCP[0].Solde); + const ancienTotal = parseFloat(existingCP[0].Total || 0); + const ancienSolde = parseFloat(existingCP[0].Solde || 0); const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0); - const incrementTotal = acquisitionCP - ancienTotal; - const nouveauSolde = ancienSolde + incrementTotal; - await conn.query( - `UPDATE CompteurConges - SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() - WHERE Id = ?`, - [acquisitionCP, Math.max(0, nouveauSolde), existingCP[0].Id] - ); + 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`); + + // 3️⃣ Vérifier si le collaborateur a de l'anticipé utilisé + const [anticipeUtilise] = await conn.query(` + SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe, + MIN(dd.Id) as firstDeductionId, + MIN(dd.DemandeCongeId) as demandeId + 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 dd.JoursUtilises > 0 + `, [collaborateurId, cpTypeId, currentYear]); + + const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0); + + if (anticipePris > 0) { + // 4️⃣ Calculer le montant à rembourser + const aRembourser = Math.min(incrementAcquis, anticipePris); + + 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 + const [deductionsAnticipees] = await conn.query(` + SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises + 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 dd.JoursUtilises > 0 + ORDER BY dd.Id ASC + `, [collaborateurId, cpTypeId, currentYear]); + + let resteARembourser = aRembourser; + + for (const deduction of deductionsAnticipees) { + if (resteARembourser <= 0) break; + + 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 - ?) + 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 + WHERE DemandeCongeId = ? + AND TypeCongeId = ? + AND Annee = ? + AND TypeDeduction IN ('Année N', 'Anne N', 'Anne actuelle N') + `, [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) + VALUES (?, ?, ?, 'Année N', ?) + `, [deduction.DemandeCongeId, cpTypeId, currentYear, aDeduiteDeCetteDeduction]); + } + + resteARembourser -= aDeduiteDeCetteDeduction; + + console.log(` ✅ CP - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`); + } + + // Supprimer les déductions anticipées à zéro + await conn.query(` + DELETE FROM DeductionDetails + WHERE TypeCongeId = ? + AND Annee = ? + AND TypeDeduction = 'N Anticip' + AND JoursUtilises <= 0 + `, [cpTypeId, currentYear]); + } + } + + // 6️⃣ Recalculer le solde total (acquis + report - consommé) + const [consomme] = 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ée' + `, [collaborateurId, cpTypeId, currentYear]); + + 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() + WHERE Id = ? + `, [acquisitionCP, nouveauSolde, existingCP[0].Id]); updates.push({ type: 'CP', exercice: exerciceCP, acquisitionCumulee: acquisitionCP, - increment: incrementTotal, - nouveauSolde: Math.max(0, nouveauSolde) + increment: incrementAcquis, + nouveauSolde: nouveauSolde }); } else { - await conn.query( - `INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW())`, - [collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP] - ); + // Créer le compteur s'il n'existe pas + await conn.query(` + INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, NOW()) + `, [collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP]); + + console.log(` CP - Compteur créé: ${acquisitionCP.toFixed(2)}j`); updates.push({ type: 'CP', @@ -1219,64 +1755,189 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null } } - // ===== RTT (NOUVEAU avec gestion variable) ===== - const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); - const acquisitionRTT = rttData.acquisition; + // ====================================== + // RTT + // ====================================== + if (!isApprenti) { + const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); + const acquisitionRTT = rttData.acquisition; - const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); - if (rttType.length > 0) { - const rttTypeId = rttType[0].Id; - const [existingRTT] = await conn.query( - `SELECT Id, Total, Solde - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, rttTypeId, currentYear] - ); + const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); - if (existingRTT.length > 0) { - const ancienTotal = parseFloat(existingRTT[0].Total); - const ancienSolde = parseFloat(existingRTT[0].Solde); - const incrementTotal = acquisitionRTT - ancienTotal; - const nouveauSolde = ancienSolde + incrementTotal; + if (rttType.length > 0) { + const rttTypeId = rttType[0].Id; - await conn.query( - `UPDATE CompteurConges - SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() - WHERE Id = ?`, - [acquisitionRTT, Math.max(0, nouveauSolde), existingRTT[0].Id] - ); + // 1️⃣ Récupérer le compteur existant + const [existingRTT] = await conn.query(` + SELECT Id, Total, Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttTypeId, currentYear]); - updates.push({ - type: 'RTT', - annee: currentYear, - typeContrat: rttData.typeContrat, - config: `${rttData.config.joursAnnuels}j/an`, - moisTravailles: rttData.moisTravailles, - acquisitionCumulee: acquisitionRTT, - increment: incrementTotal, - nouveauSolde: Math.max(0, nouveauSolde) - }); - } else { - await conn.query( - `INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW())`, - [collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT] - ); + if (existingRTT.length > 0) { + const ancienTotal = parseFloat(existingRTT[0].Total || 0); + const ancienSolde = parseFloat(existingRTT[0].Solde || 0); - updates.push({ - type: 'RTT', - annee: currentYear, - typeContrat: rttData.typeContrat, - config: `${rttData.config.joursAnnuels}j/an`, - moisTravailles: rttData.moisTravailles, - acquisitionCumulee: acquisitionRTT, - action: 'created', - nouveauSolde: acquisitionRTT - }); + console.log(` RTT - Ancien acquis: ${ancienTotal.toFixed(2)}j`); + console.log(` RTT - Nouvel acquis: ${acquisitionRTT.toFixed(2)}j`); + + // 2️⃣ Calculer l'incrément d'acquisition + const incrementAcquis = acquisitionRTT - ancienTotal; + + if (incrementAcquis > 0) { + console.log(` RTT - Nouveaux jours ce mois: +${incrementAcquis.toFixed(2)}j`); + + // 3️⃣ Vérifier si le collaborateur a de l'anticipé utilisé + const [anticipeUtilise] = await conn.query(` + SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAnticipe + 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 dd.JoursUtilises > 0 + `, [collaborateurId, rttTypeId, currentYear]); + + const anticipePris = parseFloat(anticipeUtilise[0]?.totalAnticipe || 0); + + if (anticipePris > 0) { + // 4️⃣ Calculer le montant à rembourser + const aRembourser = Math.min(incrementAcquis, anticipePris); + + console.log(` 💳 RTT - Anticipé à rembourser: ${aRembourser.toFixed(2)}j (sur ${anticipePris.toFixed(2)}j)`); + + // 5️⃣ Rembourser l'anticipé + const [deductionsAnticipees] = await conn.query(` + SELECT dd.Id, dd.DemandeCongeId, dd.JoursUtilises + 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 dd.JoursUtilises > 0 + ORDER BY dd.Id ASC + `, [collaborateurId, rttTypeId, currentYear]); + + let resteARembourser = aRembourser; + + for (const deduction of deductionsAnticipees) { + if (resteARembourser <= 0) break; + + 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 - ?) + WHERE Id = ? + `, [aDeduiteDeCetteDeduction, deduction.Id]); + + // Vérifier si une déduction "Année N" existe déjà + const [existingAnneeN] = await conn.query(` + SELECT Id, JoursUtilises + FROM DeductionDetails + WHERE DemandeCongeId = ? + AND TypeCongeId = ? + AND Annee = ? + AND TypeDeduction IN ('Année N', 'Anne N') + `, [deduction.DemandeCongeId, rttTypeId, currentYear]); + + if (existingAnneeN.length > 0) { + await conn.query(` + UPDATE DeductionDetails + SET JoursUtilises = JoursUtilises + ? + WHERE Id = ? + `, [aDeduiteDeCetteDeduction, existingAnneeN[0].Id]); + } else { + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Année N', ?) + `, [deduction.DemandeCongeId, rttTypeId, currentYear, aDeduiteDeCetteDeduction]); + } + + resteARembourser -= aDeduiteDeCetteDeduction; + + console.log(` ✅ RTT - Remboursé ${aDeduiteDeCetteDeduction.toFixed(2)}j (Demande ${deduction.DemandeCongeId})`); + } + + // Supprimer les déductions anticipées à zéro + await conn.query(` + DELETE FROM DeductionDetails + WHERE TypeCongeId = ? + AND Annee = ? + AND TypeDeduction = 'N Anticip' + AND JoursUtilises <= 0 + `, [rttTypeId, currentYear]); + } + } + + // 6️⃣ Recalculer le solde total + const [consomme] = 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 Dosée') + AND dc.Statut != 'Refusée' + `, [collaborateurId, rttTypeId, currentYear]); + + const totalConsomme = parseFloat(consomme[0].total || 0); + const nouveauSolde = Math.max(0, acquisitionRTT - totalConsomme); + + console.log(` RTT - Consommé total: ${totalConsomme.toFixed(2)}j`); + console.log(` RTT - Nouveau solde: ${nouveauSolde.toFixed(2)}j`); + + // 7️⃣ Mettre à jour le compteur + await conn.query(` + UPDATE CompteurConges + SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() + WHERE Id = ? + `, [acquisitionRTT, nouveauSolde, existingRTT[0].Id]); + + updates.push({ + type: 'RTT', + annee: currentYear, + typeContrat: rttData.typeContrat, + config: `${rttData.config.joursAnnuels}j/an`, + moisTravailles: rttData.moisTravailles, + acquisitionCumulee: acquisitionRTT, + increment: incrementAcquis, + nouveauSolde: nouveauSolde + }); + } else { + // Créer le compteur s'il n'existe pas + await conn.query(` + INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, NOW()) + `, [collaborateurId, rttTypeId, currentYear, acquisitionRTT, acquisitionRTT]); + + console.log(` RTT - Compteur créé: ${acquisitionRTT.toFixed(2)}j`); + + updates.push({ + type: 'RTT', + annee: currentYear, + typeContrat: rttData.typeContrat, + config: `${rttData.config.joursAnnuels}j/an`, + moisTravailles: rttData.moisTravailles, + acquisitionCumulee: acquisitionRTT, + action: 'created', + nouveauSolde: acquisitionRTT + }); + } } } + console.log(`✅ Mise à jour terminée pour collaborateur ${collaborateurId}\n`); + return updates; } @@ -1284,7 +1945,7 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null // ROUTES API // ======================================== -app.post('/login', async (req, res) => { +app.post('/api/login', async (req, res) => { try { const { email, mot_de_passe, entraUserId, userPrincipalName } = req.body; const accessToken = req.headers.authorization?.replace('Bearer ', ''); @@ -1363,13 +2024,14 @@ app.post('/login', async (req, res) => { } }); -app.post('/check-user-groups', async (req, res) => { +app.post('/api/check-user-groups', async (req, res) => { try { const { userPrincipalName } = req.body; const accessToken = req.headers.authorization?.replace('Bearer ', ''); if (!userPrincipalName || !accessToken) return res.json({ authorized: false, message: 'Email ou token manquant' }); + // 1. Vérification locale const [users] = await pool.query(` SELECT ca.id, ca.entraUserId, ca.prenom, ca.nom, ca.email, s.Nom as service, ca.role, ca.CampusId, ca.SocieteId, @@ -1383,6 +2045,9 @@ app.post('/check-user-groups', async (req, res) => { if (users.length > 0) { const user = users[0]; + // Si l'utilisateur est inactif, on le bloque + if (user.Actif === 0) return res.json({ authorized: false, message: 'Compte désactivé' }); + return res.json({ authorized: true, role: user.role, @@ -1396,20 +2061,33 @@ app.post('/check-user-groups', async (req, res) => { }); } + // 2. Si pas trouvé, interrogation Microsoft Graph const userGraph = await axios.get(`https://graph.microsoft.com/v1.0/users/${userPrincipalName}?$select=id,displayName,givenName,surname,mail,department,jobTitle`, { headers: { Authorization: `Bearer ${accessToken}` } }); const userInfo = userGraph.data; const checkMemberResponse = await axios.post(`https://graph.microsoft.com/v1.0/users/${userInfo.id}/checkMemberGroups`, { groupIds: [AZURE_CONFIG.groupId] }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); const isInGroup = checkMemberResponse.data.value.includes(AZURE_CONFIG.groupId); - if (!isInGroup) return res.json({ authorized: false, message: 'Utilisateur non autorisé' }); + if (!isInGroup) return res.json({ authorized: false, message: 'Utilisateur non autorisé (Hors groupe)' }); - // ⭐ Insertion avec SocieteId par défaut (ajuster selon votre logique) + // 3. ⭐ INSERTION AVEC VALEURS PAR DÉFAUT CRITIQUES + // On met SocieteId=1 et TypeContrat='37h' par défaut pour éviter les bugs de calcul const [result] = await pool.query( `INSERT INTO CollaborateurAD - (entraUserId, prenom, nom, email, service, role, SocieteId) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - [userInfo.id, userInfo.givenName, userInfo.surname, userInfo.mail, userInfo.department, 'Collaborateur', null] + (entraUserId, prenom, nom, email, service, role, SocieteId, Actif, DateEntree, TypeContrat) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + userInfo.id, + userInfo.givenName || 'Prénom', + userInfo.surname || 'Nom', + userInfo.mail || userPrincipalName, + userInfo.department, + 'Collaborateur', + 1, // SocieteId par défaut (ex: 1 = ENSUP) + 1, // Actif = 1 (Important !) + new Date(), // DateEntree = Aujourd'hui + '37h' // TypeContrat par défaut + ] ); res.json({ @@ -1425,16 +2103,24 @@ app.post('/check-user-groups', async (req, res) => { email: userInfo.mail, service: userInfo.department, role: 'Collaborateur', - societeId: null, - societeNom: null + societeId: 1, + societeNom: 'Défaut' } }); } catch (error) { + console.error("Erreur check-user-groups:", error); res.json({ authorized: false, message: 'Erreur serveur', error: error.message }); } }); -app.get('/getDetailedLeaveCounters', async (req, res) => { + +// ======================================== +// ✅ CODE CORRIGÉ POUR getDetailedLeaveCounters +// À remplacer dans server.js à partir de la ligne ~1600 +// ======================================== + + +app.get('/api/getDetailedLeaveCounters', async (req, res) => { try { const userIdParam = req.query.user_id; @@ -1444,19 +2130,24 @@ app.get('/getDetailedLeaveCounters', async (req, res) => { const conn = await pool.getConnection(); - // ⭐ NOUVEAU : Récupérer le dernier arrêté - const dernierArrete = await getDernierArrete(conn); - const dateArrete = dernierArrete ? new Date(dernierArrete.DateArrete) : null; - - // Déterminer l'ID (UUID ou numérique) const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); const userQuery = ` - SELECT ca.id, ca.prenom, ca.nom, ca.email, ca.DateEntree, ca.role, - ca.TypeContrat, s.Nom as service, ca.CampusId, ca.SocieteId, - so.Nom as societe_nom - FROM CollaborateurAD ca - LEFT JOIN Services s ON ca.ServiceId = s.Id + 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 + 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) @@ -1474,20 +2165,16 @@ app.get('/getDetailedLeaveCounters', async (req, res) => { const dateEntree = user.DateEntree; const typeContrat = user.TypeContrat || '37h'; - const dateRefParam = req.query.dateRef; - const today = dateRefParam ? parseDateYYYYMMDD(dateRefParam) : new Date(); + const today = new Date(); const currentYear = today.getFullYear(); const previousYear = currentYear - 1; - // ⭐ MODIFICATION CRITIQUE : Utiliser la date d'arrêté si elle est plus récente que la référence - const dateCalcul = dateArrete && today <= dateArrete ? dateArrete : today; + console.log(`\n📊 === CALCUL COMPTEURS pour ${user.prenom} ${user.nom} ===`); + console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`); const ancienneteMs = today - new Date(dateEntree || today); const ancienneteMois = Math.floor(ancienneteMs / (1000 * 60 * 60 * 24 * 30.44)); - // ⭐ Calculer avec la bonne date - const cpMonthsCurrent = getMoisTravaillesCP(dateCalcul, dateEntree); - let counters = { user: { id: user.id, @@ -1497,60 +2184,60 @@ app.get('/getDetailedLeaveCounters', async (req, res) => { email: user.email, service: user.service || 'Non défini', role: user.role, + description: user.description, typeContrat: typeContrat, - societeId: userInfo.SocieteId, - societeNom: userInfo.societe_nom || 'Non défini', + societeId: user.SocieteId, + societeNom: user.societe_nom || 'Non défini', dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null, ancienneteMois: ancienneteMois, ancienneteAnnees: Math.floor(ancienneteMois / 12), ancienneteMoisRestants: ancienneteMois % 12 }, dateReference: today.toISOString().split('T')[0], - exerciceCP: getExerciceCP(dateCalcul), + exerciceCP: getExerciceCP(today), anneeRTT: currentYear, - - // ⭐ NOUVEAU : Indiquer si on est en période d'arrêté - arreteInfo: dernierArrete ? { - dateArrete: formatDateWithoutUTC(dateArrete), - libelle: dernierArrete.Libelle, - bloquage: today <= dateArrete - } : null, - cpN1: null, cpN: null, rttN: null, rttN1: null, - totalDisponible: { cp: 0, rtt: 0, total: 0 } + 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é) ===== + // ==================================== + // 1️⃣ CP N-1 (Report) - CALCUL CONSOMMÉ = ACQUIS - SOLDE + // ==================================== if (cpType.length > 0) { const [cpN1] = await conn.query(` - SELECT Annee, SoldeReporte, Total, Solde + SELECT Annee, Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, cpType[0].Id, previousYear]); - if (cpN1.length > 0 && parseFloat(cpN1[0].SoldeReporte || 0) > 0) { - const soldeReporte = parseFloat(cpN1[0].SoldeReporte || 0); - const soldeActuel = parseFloat(cpN1[0].Solde || 0); - const pris = Math.max(0, soldeReporte - soldeActuel); + if (cpN1.length > 0) { + const totalAcquis = parseFloat(cpN1[0].Total || 0); + const soldeReporte = parseFloat(cpN1[0].Solde || 0); + + // ⭐ CALCUL : Consommé = Acquis - Solde + const pris = Math.max(0, totalAcquis - soldeReporte); counters.cpN1 = { annee: previousYear, - exercice: `${previousYear - 1}-${previousYear}`, - reporte: parseFloat(soldeReporte.toFixed(2)), + exercice: `${previousYear}-${previousYear + 1}`, + reporte: parseFloat(totalAcquis.toFixed(2)), pris: parseFloat(pris.toFixed(2)), - solde: parseFloat(soldeActuel.toFixed(2)), - pourcentageUtilise: soldeReporte > 0 ? parseFloat(((pris / soldeReporte) * 100).toFixed(1)) : 0 + solde: parseFloat(soldeReporte.toFixed(2)), + pourcentageUtilise: totalAcquis > 0 ? parseFloat(((pris / totalAcquis) * 100).toFixed(1)) : 0 }; counters.totalDisponible.cp += counters.cpN1.solde; + + console.log(`✅ CP N-1: Acquis=${totalAcquis}j, Solde=${soldeReporte}j → Consommé=${pris}j`); } else { counters.cpN1 = { annee: previousYear, - exercice: `${previousYear - 1}-${previousYear}`, + exercice: `${previousYear}-${previousYear + 1}`, reporte: 0, pris: 0, solde: 0, @@ -1558,183 +2245,196 @@ app.get('/getDetailedLeaveCounters', async (req, res) => { }; } - // ===== CP N (Exercice en cours) ===== - const [cpN] = await conn.query(` - SELECT Annee, Total, Solde, SoldeReporte + // ==================================== + // 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]); - // ⭐ CORRECTION : Gérer le retour de calculerAcquisitionDepuisArrete - let acquisCumuleeCP; - if (typeof calculerAcquisitionDepuisArrete === 'function' && dernierArrete) { - const result = await calculerAcquisitionDepuisArrete(conn, userId, 'CP', dateCalcul); - acquisCumuleeCP = typeof result === 'number' ? result : parseFloat(result) || 0; + 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 { - acquisCumuleeCP = calculerAcquisitionCP(dateCalcul, dateEntree) || 0; + const acquisCP = calculerAcquisitionCP(today, dateEntree); + soldeActuelCP = acquisCP; + totalAcquis = acquisCP; + cpPris = 0; + console.log(` CP N - Pas de compteur BDD → Calcul: ${acquisCP}j`); } - // ⭐ SÉCURITÉ : S'assurer que c'est un nombre - acquisCumuleeCP = parseFloat(acquisCumuleeCP) || 0; + // 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]); - if (cpN.length > 0) { - const total = parseFloat(cpN[0].Total || 0); - const solde = parseFloat(cpN[0].Solde || 0); - const soldeReporte = parseFloat(cpN[0].SoldeReporte || 0); - const soldeN = solde - soldeReporte; - const pris = Math.max(0, total - soldeN); + const cpAnticipeUtilise = parseFloat(anticipeUtiliseCP[0]?.totalConsomme || 0); + const cpAnticipeMax = Math.max(0, 25 - totalAcquis); + const cpAnticipeDisponible = Math.max(0, cpAnticipeMax - cpAnticipeUtilise); - counters.cpN = { - annee: currentYear, - exercice: getExerciceCP(dateCalcul), - totalAnnuel: 25.00, - moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)), - acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)), + console.log(` CP - Acquis: ${totalAcquis.toFixed(2)}j`); + console.log(` CP - Consommé: ${cpPris.toFixed(2)}j`); + console.log(` CP - Solde: ${soldeActuelCP.toFixed(2)}j`); - // ⭐ MODIFICATION : Afficher l'acquisition à la date de calcul - acquis: parseFloat(acquisCumuleeCP.toFixed(2)), - pris: parseFloat(pris.toFixed(2)), - solde: parseFloat(soldeN.toFixed(2)), + 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 + } + }; - tauxAcquisition: parseFloat(((cpMonthsCurrent / 12) * 100).toFixed(1)), - pourcentageUtilise: total > 0 ? parseFloat(((pris / total) * 100).toFixed(1)) : 0, - joursRestantsAAcquerir: parseFloat((25 - acquisCumuleeCP).toFixed(2)) - }; - counters.totalDisponible.cp += counters.cpN.solde; - } else { - counters.cpN = { - annee: currentYear, - exercice: getExerciceCP(dateCalcul), - totalAnnuel: 25.00, - moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)), - acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)), - acquis: parseFloat(acquisCumuleeCP.toFixed(2)), - pris: 0, - solde: parseFloat(acquisCumuleeCP.toFixed(2)), - tauxAcquisition: parseFloat(((cpMonthsCurrent / 12) * 100).toFixed(1)), - pourcentageUtilise: 0, - joursRestantsAAcquerir: parseFloat((25 - acquisCumuleeCP).toFixed(2)) - }; - counters.totalDisponible.cp += counters.cpN.solde; - } + counters.totalDisponible.cp += counters.cpN.solde + cpAnticipeDisponible; } - // ===== RTT N (même logique) ===== + // ==================================== + // 3️⃣ RTT N - CALCUL CONSOMMÉ = ACQUIS - SOLDE + // ==================================== const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); - const isApprenti = userInfo[0].role === 'Apprenti'; - if (rttType.length > 0 && !isApprenti) { // ⭐ Condition modifiée - const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat) - // ⭐ CORRECTION : Gérer le retour de calculerAcquisitionDepuisArrete pour RTT - let rttData; - if (typeof calculerAcquisitionDepuisArrete === 'function' && dernierArrete) { - const result = await calculerAcquisitionDepuisArrete(conn, userId, 'RTT', dateCalcul); - const acquisRTT = typeof result === 'number' ? result : parseFloat(result) || 0; - rttData = { - acquisition: acquisRTT, - typeContrat: typeContrat, - moisTravailles: getMoisTravaillesRTT(dateCalcul, dateEntree), - config: rttConfig - }; - } else { - rttData = await calculerAcquisitionRTT(conn, userId, dateCalcul); - } - - // ⭐ SÉCURITÉ : S'assurer que acquisition est un nombre - rttData.acquisition = parseFloat(rttData.acquisition) || 0; - - const [rttN] = await conn.query(` - SELECT Annee, Total, Solde, SoldeReporte + if (rttType.length > 0 && user.role !== 'Apprenti') { + const [compteurRTT] = await conn.query(` + SELECT Solde, Total FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, rttType[0].Id, currentYear]); - if (rttN.length > 0) { - const total = parseFloat(rttN[0].Total || 0); - const solde = parseFloat(rttN[0].Solde || 0); - const pris = Math.max(0, total - solde); + let soldeActuelRTT = 0; + let totalAcquis = 0; + let rttPris = 0; - counters.rttN = { - annee: currentYear, - typeContrat: typeContrat, - totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)), - moisTravailles: rttData.moisTravailles, - acquisitionMensuelle: parseFloat(rttConfig.acquisitionMensuelle.toFixed(6)), - acquis: parseFloat(rttData.acquisition.toFixed(2)), - pris: parseFloat(pris.toFixed(2)), - solde: parseFloat(solde.toFixed(2)), - tauxAcquisition: parseFloat(((rttData.moisTravailles / 12) * 100).toFixed(1)), - pourcentageUtilise: total > 0 ? parseFloat(((pris / total) * 100).toFixed(1)) : 0, - joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2)) - }; - counters.totalDisponible.rtt += counters.rttN.solde; + const rttData = await calculerAcquisitionRTT(conn, userId, today); + const rttConfig = await getConfigurationRTT(conn, currentYear, typeContrat); + + if (compteurRTT.length > 0) { + soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0); + totalAcquis = parseFloat(compteurRTT[0].Total || 0); + + // ⭐ CALCUL : Consommé = Acquis - Solde + rttPris = Math.max(0, totalAcquis - soldeActuelRTT); + + console.log(` RTT - Acquis: ${totalAcquis}j, Solde: ${soldeActuelRTT}j → Consommé: ${rttPris}j`); } else { - counters.rttN = { - annee: currentYear, - typeContrat: typeContrat, - totalAnnuel: parseFloat(rttConfig.joursAnnuels.toFixed(2)), - moisTravailles: rttData.moisTravailles, - acquisitionMensuelle: parseFloat(rttConfig.acquisitionMensuelle.toFixed(6)), - acquis: parseFloat(rttData.acquisition.toFixed(2)), - pris: 0, - solde: parseFloat(rttData.acquisition.toFixed(2)), - tauxAcquisition: parseFloat(((rttData.moisTravailles / 12) * 100).toFixed(1)), - pourcentageUtilise: 0, - joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2)) - }; - counters.totalDisponible.rtt += counters.rttN.solde; + soldeActuelRTT = rttData.acquisition; + totalAcquis = rttData.acquisition; + rttPris = 0; + console.log(` RTT - Pas de compteur BDD → Calcul: ${rttData.acquisition}j`); } - 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" + 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.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt; - // ===== RÉCUP (NOUVEAU) ===== + 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']); + if (recupType.length > 0) { - const [recupN] = await conn.query(` - SELECT Annee, Total, Solde - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, recupType[0].Id, currentYear]); + const [compteurRecup] = await conn.query(` + SELECT Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [userId, recupType[0].Id, currentYear]); - if (recupN.length > 0) { - const total = parseFloat(recupN[0].Total || 0); - const solde = parseFloat(recupN[0].Solde || 0); + const soldeRecup = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0; - counters.recupN = { - annee: currentYear, - acquis: parseFloat(total.toFixed(2)), - pris: parseFloat((total - solde).toFixed(2)), - solde: parseFloat(solde.toFixed(2)), - message: "Jours de récupération accumulés (samedis travaillés)" - }; - counters.totalDisponible.recup = counters.recupN.solde; - counters.totalDisponible.total += counters.recupN.solde; - } else { - counters.recupN = { - annee: currentYear, - acquis: 0, - pris: 0, - solde: 0, - message: "Jours de récupération accumulés (samedis travaillés)" - }; - counters.totalDisponible.recup = 0; - } + // Récupérer accumulations depuis DeductionDetails + const [accumRecup] = await conn.query(` + SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalAccum + 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]); + + const acquis = parseFloat(accumRecup[0]?.totalAccum || 0); + + // ⭐ 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)), // ⭐ 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.total = counters.totalDisponible.cp + counters.totalDisponible.rtt + counters.totalDisponible.recup; + + console.log(`\n✅ TOTAL FINAL: ${counters.totalDisponible.total.toFixed(2)}j disponibles`); + conn.release(); + res.json({ success: true, message: 'Compteurs détaillés récupérés avec succès', - data: counters + data: counters, + availableCP: counters.totalDisponible.cp, + availableRTT: counters.totalDisponible.rtt, + availableRecup: counters.totalDisponible.recup }); } catch (error) { console.error('Erreur getDetailedLeaveCounters:', error); @@ -1745,190 +2445,7 @@ app.get('/getDetailedLeaveCounters', async (req, res) => { }); } }); - -app.post('/reinitializeAllCounters', async (req, res) => { - const conn = await pool.getConnection(); - try { - await conn.beginTransaction(); - - console.log('🔄 Réinitialisation de tous les compteurs...'); - - const [collaborateurs] = await conn.query(` - SELECT id, prenom, nom, DateEntree, TypeContrat, CampusId, SocieteId - FROM CollaborateurAD - WHERE (actif = 1 OR actif IS NULL) - `); - - console.log(`📊 ${collaborateurs.length} collaborateurs trouvés`); - - const dateRefParam = req.body.dateReference; - const today = dateRefParam ? new Date(dateRefParam) : new Date(); - const currentYear = today.getFullYear(); - const previousYear = currentYear - 1; - - const results = []; - - for (const collab of collaborateurs) { - const dateEntree = collab.DateEntree; - const typeContrat = collab.TypeContrat || '37h'; - - // Calculer les acquisitions - const acquisCP = calculerAcquisitionCP(today, dateEntree); - const isApprenti = collab.role === 'Apprenti'; - const rttData = isApprenti ? { acquisition: 0 } : await calculerAcquisitionRTT(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']); - - // ===== CP N ===== - if (cpType.length > 0) { - // ⭐ CALCUL FIABLE : Utiliser DeductionDetails au lieu des anciennes valeurs - const [deductionsCP] = await conn.query(` - SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme - FROM DeductionDetails dd - JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id - WHERE dc.CollaborateurADId = ? - AND dd.TypeCongeId = ? - AND dd.Annee = ? - AND dc.Statut != 'Refusée' - `, [collab.id, cpType[0].Id, currentYear]); - - const totalConsomme = parseFloat(deductionsCP[0].totalConsomme || 0); - - // Récupérer le reporté (qui ne change pas) - const [compteurExisting] = await conn.query(` - SELECT SoldeReporte FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collab.id, cpType[0].Id, currentYear]); - - const soldeReporte = compteurExisting.length > 0 - ? parseFloat(compteurExisting[0].SoldeReporte || 0) - : 0; - - // ⭐ CALCUL EXACT DU SOLDE - const nouveauSolde = Math.max(0, acquisCP + soldeReporte - totalConsomme); - - // Mise à jour ou insertion - if (compteurExisting.length > 0) { - await conn.query(` - UPDATE CompteurConges - SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [acquisCP, nouveauSolde, collab.id, cpType[0].Id, currentYear]); - } else { - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW()) - `, [collab.id, cpType[0].Id, currentYear, acquisCP, nouveauSolde]); - } - - console.log(`📊 ${collab.prenom} ${collab.nom} - CP: Acquis ${acquisCP.toFixed(2)}j, Consommé ${totalConsomme.toFixed(2)}j, Reporté ${soldeReporte.toFixed(2)}j → Solde ${nouveauSolde.toFixed(2)}j`); - - // Créer CP N-1 si nécessaire - const [cpN1] = await conn.query(` - SELECT Id FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collab.id, cpType[0].Id, previousYear]); - - if (cpN1.length === 0) { - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, 0, 0, 0, NOW()) - `, [collab.id, cpType[0].Id, previousYear]); - } - } - - // ===== RTT N ===== - if (rttType.length > 0 && !isApprenti) { - // ⭐ MÊME LOGIQUE POUR LES RTT - const [deductionsRTT] = await conn.query(` - SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsomme - FROM DeductionDetails dd - JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id - WHERE dc.CollaborateurADId = ? - AND dd.TypeCongeId = ? - AND dd.Annee = ? - AND dc.Statut != 'Refusée' - `, [collab.id, rttType[0].Id, currentYear]); - - const totalConsomme = parseFloat(deductionsRTT[0].totalConsomme || 0); - const nouveauSolde = Math.max(0, acquisRTT - totalConsomme); - - const [compteurExisting] = await conn.query(` - SELECT Id FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collab.id, rttType[0].Id, currentYear]); - - if (compteurExisting.length > 0) { - await conn.query(` - UPDATE CompteurConges - SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() - WHERE Id = ? - `, [acquisRTT, nouveauSolde, compteurExisting[0].Id]); - } else { - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW()) - `, [collab.id, rttType[0].Id, currentYear, acquisRTT, nouveauSolde]); - } - - console.log(`📊 ${collab.prenom} ${collab.nom} - RTT: Acquis ${acquisRTT.toFixed(2)}j, Consommé ${totalConsomme.toFixed(2)}j → Solde ${nouveauSolde.toFixed(2)}j`); - - // Créer RTT N-1 si nécessaire - const [rttN1] = await conn.query(` - SELECT Id FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collab.id, rttType[0].Id, previousYear]); - - if (rttN1.length === 0) { - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, 0, 0, 0, NOW()) - `, [collab.id, rttType[0].Id, previousYear]); - } - } - - results.push({ - collaborateur: `${collab.prenom} ${collab.nom}`, - type_contrat: typeContrat, - cp_acquis: acquisCP.toFixed(2), - cp_solde: (acquisCP - (deductionsCP?.[0]?.totalConsomme || 0)).toFixed(2), - rtt_acquis: acquisRTT.toFixed(2), - rtt_solde: (acquisRTT - (deductionsRTT?.[0]?.totalConsomme || 0)).toFixed(2) - }); - } - - await conn.commit(); - - console.log('✅ Réinitialisation terminée'); - - res.json({ - success: true, - message: `✅ Compteurs réinitialisés pour ${collaborateurs.length} collaborateurs`, - date_reference: today.toISOString().split('T')[0], - total_collaborateurs: collaborateurs.length, - results: results - }); - - } catch (error) { - await conn.rollback(); - console.error('❌ Erreur réinitialisation:', error); - res.status(500).json({ - success: false, - message: 'Erreur lors de la réinitialisation', - error: error.message - }); - } finally { - conn.release(); - } -}); - -app.post('/updateCounters', async (req, res) => { +app.post('/api/updateCounters', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; @@ -1945,7 +2462,7 @@ app.post('/updateCounters', async (req, res) => { } }); -app.post('/updateAllCounters', async (req, res) => { +app.post('/api/updateAllCounters', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -2065,9 +2582,9 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { console.log(`Demande ID: ${demandeCongeId}`); console.log(`Collaborateur ID: ${collaborateurId}`); - // Récupérer tous les détails de déduction pour cette demande + // 1️⃣ Récupérer TOUTES les déductions (y compris Récup) const [deductions] = await conn.query( - `SELECT dd.TypeCongeId, dd.Annee, dd.TypeDeduction, dd.JoursUtilises, tc.Nom as TypeNom + `SELECT dd.*, tc.Nom as TypeNom FROM DeductionDetails dd JOIN TypeConge tc ON dd.TypeCongeId = tc.Id WHERE dd.DemandeCongeId = ? @@ -2087,41 +2604,151 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { for (const deduction of deductions) { const { TypeCongeId, Annee, TypeDeduction, JoursUtilises, TypeNom } = deduction; - console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j`); + console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j (Année: ${Annee})`); - // 🔹 CAS SPÉCIAL : Récupération accumulée (RETIRER les jours) - if (TypeDeduction === 'Accum Récup') { - console.log(`❌ Annulation accumulation ${TypeNom}: -${JoursUtilises}j`); + // ======================================== + // RÉCUP POSÉE - RESTAURATION + // ======================================== + if (TypeDeduction === 'Récup Posée') { + console.log(`🔄 Restauration Récup posée: +${JoursUtilises}j`); const [compteur] = await conn.query( - `SELECT Id, Total, Solde FROM CompteurConges + `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 = ancienSolde + parseFloat(JoursUtilises); + await conn.query( `UPDATE CompteurConges - SET Total = GREATEST(0, Total - ?), - Solde = GREATEST(0, Solde - ?), + SET Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?`, - [JoursUtilises, JoursUtilises, compteur[0].Id] + [nouveauSolde, compteur[0].Id] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, - joursRetires: JoursUtilises + joursRestores: JoursUtilises }); - console.log(`✅ Récup retirée: ${compteur[0].Solde} → ${Math.max(0, compteur[0].Solde - JoursUtilises)}`); + console.log(`✅ Récup restaurée: ${ancienSolde} → ${nouveauSolde}`); + } else { + console.warn(`⚠️ Compteur Récup non trouvé pour l'année ${Annee}`); } continue; } - // 🔹 Reporté N-1 - if (TypeDeduction === 'Reporté N-1' || TypeDeduction === 'Report N-1') { + // ======================================== + // N+1 ANTICIPÉ - RESTAURATION + // ======================================== + if (TypeDeduction === 'N+1 Anticipé') { + console.log(`🔄 Restauration N+1 Anticipé: +${JoursUtilises}j`); + + const [compteur] = await conn.query( + `SELECT Id, SoldeAnticipe FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, TypeCongeId, Annee] + ); + + if (compteur.length > 0) { + const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0); + const nouveauSolde = ancienSolde + parseFloat(JoursUtilises); + + await conn.query( + `UPDATE CompteurConges + SET SoldeAnticipe = ?, + DerniereMiseAJour = NOW() + WHERE Id = ?`, + [nouveauSolde, compteur[0].Id] + ); + + restorations.push({ + type: TypeNom, + annee: Annee, + typeDeduction: TypeDeduction, + joursRestores: JoursUtilises + }); + console.log(`✅ N+1 Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`); + } else { + // 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())`, + [collaborateurId, TypeCongeId, Annee, JoursUtilises] + ); + + restorations.push({ + type: TypeNom, + annee: Annee, + typeDeduction: TypeDeduction, + joursRestores: JoursUtilises + }); + console.log(`✅ Compteur N+1 créé avec ${JoursUtilises}j anticipés`); + } + continue; + } + + // ======================================== + // N ANTICIPÉ - RESTAURATION + // ======================================== + if (TypeDeduction === 'N Anticipé') { + console.log(`🔄 Restauration N Anticipé: +${JoursUtilises}j`); + + const [compteur] = await conn.query( + `SELECT Id, SoldeAnticipe FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, TypeCongeId, Annee] + ); + + if (compteur.length > 0) { + const ancienSolde = parseFloat(compteur[0].SoldeAnticipe || 0); + const nouveauSolde = ancienSolde + parseFloat(JoursUtilises); + + await conn.query( + `UPDATE CompteurConges + SET SoldeAnticipe = ?, + DerniereMiseAJour = NOW() + WHERE Id = ?`, + [nouveauSolde, compteur[0].Id] + ); + + restorations.push({ + type: TypeNom, + annee: Annee, + typeDeduction: TypeDeduction, + joursRestores: JoursUtilises + }); + console.log(`✅ N Anticipé restauré: ${ancienSolde} → ${nouveauSolde}`); + } else { + // Créer le compteur s'il n'existe pas + await conn.query( + `INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) + VALUES (?, ?, ?, 0, 0, 0, ?, NOW())`, + [collaborateurId, TypeCongeId, Annee, JoursUtilises] + ); + + restorations.push({ + type: TypeNom, + annee: Annee, + typeDeduction: TypeDeduction, + joursRestores: JoursUtilises + }); + console.log(`✅ Compteur N créé avec ${JoursUtilises}j anticipés`); + } + continue; + } + + // ======================================== + // REPORTÉ N-1 + // ======================================== + if (TypeDeduction === 'Reporté N-1' || TypeDeduction === 'Report N-1' || TypeDeduction === 'Année N-1') { const [compteur] = await conn.query( `SELECT Id, SoldeReporte, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, @@ -2151,7 +2778,9 @@ 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 @@ -2180,8 +2809,53 @@ 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 + console.log(`\n🔄 Recalcul des soldes anticipés...`); + await updateSoldeAnticipe(conn, collaborateurId); + console.log(`\n✅ Restauration terminée: ${restorations.length} opérations\n`); return { @@ -2195,8 +2869,7 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { throw error; } } - -app.get('/testProrata', async (req, res) => { +app.get('/api/testProrata', async (req, res) => { try { const userId = parseInt(req.query.user_id || 0); if (userId <= 0) return res.json({ success: false, message: 'ID utilisateur requis' }); @@ -2242,7 +2915,7 @@ app.get('/testProrata', async (req, res) => { } }); -app.post('/fixAllCounters', async (req, res) => { +app.post('/api/fixAllCounters', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -2293,7 +2966,7 @@ app.post('/fixAllCounters', async (req, res) => { } }); -app.post('/processEndOfYear', async (req, res) => { +app.post('/api/processEndOfYear', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; @@ -2320,7 +2993,7 @@ app.post('/processEndOfYear', async (req, res) => { } }); -app.post('/processEndOfExercice', async (req, res) => { +app.post('/api/processEndOfExercice', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; @@ -2347,7 +3020,7 @@ app.post('/processEndOfExercice', async (req, res) => { } }); -app.get('/getAcquisitionDetails', async (req, res) => { +app.get('/api/getAcquisitionDetails', async (req, res) => { try { const today = new Date(); const details = { date_reference: today.toISOString().split('T')[0], CP: { exercice: getExerciceCP(today), mois_travailles: getMoisTravaillesCP(today), acquisition_mensuelle: LEAVE_RULES.CP.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('CP', today), total_annuel: LEAVE_RULES.CP.joursAnnuels, periode: '01/06 - 31/05', reportable: LEAVE_RULES.CP.reportable }, RTT: { annee: today.getFullYear(), mois_travailles: getMoisTravaillesRTT(today), acquisition_mensuelle: LEAVE_RULES.RTT.acquisitionMensuelle, acquisition_cumulee: calculerAcquisitionCumulee('RTT', today), total_annuel: LEAVE_RULES.RTT.joursAnnuels, periode: '01/01 - 31/12', reportable: LEAVE_RULES.RTT.reportable } }; @@ -2357,7 +3030,7 @@ app.get('/getAcquisitionDetails', async (req, res) => { } }); -app.get('/getLeaveCounters', async (req, res) => { +app.get('/api/getLeaveCounters', async (req, res) => { try { const userId = parseInt(req.query.user_id || 0); const data = {}; @@ -2371,7 +3044,7 @@ app.get('/getLeaveCounters', async (req, res) => { } }); -app.get('/getEmploye', async (req, res) => { +app.get('/api/getEmploye', async (req, res) => { try { const id = parseInt(req.query.id || 0); if (id <= 0) return res.json({ success: false, message: 'ID invalide' }); @@ -2490,7 +3163,7 @@ app.get('/getEmploye', async (req, res) => { } }); -app.get('/getEmployeRequest', async (req, res) => { +app.get('/api/getEmployeRequest', async (req, res) => { try { const id = parseInt(req.query.id || 0); if (id <= 0) return res.json({ success: false, message: 'ID invalide' }); @@ -2531,7 +3204,7 @@ app.get('/getEmployeRequest', async (req, res) => { } }); -app.get('/getRequests', async (req, res) => { +app.get('/api/getRequests', async (req, res) => { try { const userId = req.query.user_id; if (!userId) return res.json({ success: false, message: 'ID utilisateur manquant' }); @@ -2566,7 +3239,7 @@ app.get('/getRequests', async (req, res) => { let fileUrl = null; if (row.TypeConges && row.TypeConges.includes('Congé maladie') && row.DocumentJoint) { - fileUrl = `http://localhost:3000/uploads/${path.basename(row.DocumentJoint)}`; + fileUrl = `/uploads/${path.basename(row.DocumentJoint)}`; } return { @@ -2601,7 +3274,7 @@ app.get('/getRequests', async (req, res) => { }); } }); -app.get('/getAllTeamRequests', async (req, res) => { +app.get('/api/getAllTeamRequests', async (req, res) => { try { const managerId = req.query.SuperieurId; if (!managerId) return res.json({ success: false, message: 'Paramètre SuperieurId manquant' }); @@ -2613,7 +3286,7 @@ app.get('/getAllTeamRequests', async (req, res) => { } }); -app.get('/getPendingRequests', 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' }); @@ -2628,7 +3301,7 @@ app.get('/getPendingRequests', async (req, res) => { } }); -app.get('/getTeamMembers', async (req, res) => { +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' }); @@ -2642,7 +3315,7 @@ app.get('/getTeamMembers', async (req, res) => { } }); -app.get('/getNotifications', async (req, res) => { +app.get('/api/getNotifications', async (req, res) => { try { const userIdParam = req.query.user_id; @@ -2702,7 +3375,7 @@ app.get('/getNotifications', async (req, res) => { } }); -app.post('/markNotificationRead', async (req, res) => { +app.post('/api/markNotificationRead', async (req, res) => { try { const { notificationId } = req.body; if (!notificationId || notificationId <= 0) return res.status(400).json({ success: false, message: 'ID notification invalide' }); @@ -2712,8 +3385,598 @@ app.post('/markNotificationRead', async (req, res) => { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); +// À ajouter avant app.listen() -app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => { +/** + * POST /saisirRecupJour + * Saisir une journée de récupération (samedi travaillé) + */ +app.post('/api/saisirRecupJour', async (req, res) => { + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + + const { + user_id, + date, // Date du samedi travaillé + nombre_heures = 1, // Par défaut 1 jour = 1 samedi + commentaire + } = req.body; + + console.log('\n📝 === SAISIE RÉCUP ==='); + console.log('User ID:', user_id); + console.log('Date:', date); + console.log('Heures:', nombre_heures); + + if (!user_id || !date) { + await conn.rollback(); + conn.release(); + return res.json({ + success: false, + message: 'Données manquantes' + }); + } + + // Vérifier que c'est bien un samedi + const dateObj = new Date(date); + const dayOfWeek = dateObj.getDay(); + + if (dayOfWeek !== 6) { + await conn.rollback(); + conn.release(); + return res.json({ + success: false, + message: 'La récupération ne peut être saisie que pour un samedi' + }); + } + + // Vérifier que ce samedi n'a pas déjà été saisi + const [existing] = await conn.query(` + SELECT dc.Id + FROM DemandeConge dc + JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + WHERE dc.CollaborateurADId = ? + AND dc.DateDebut = ? + AND tc.Nom = 'Récupération' + `, [user_id, date]); + + if (existing.length > 0) { + await conn.rollback(); + conn.release(); + return res.json({ + success: false, + message: 'Ce samedi a déjà été déclaré' + }); + } + + // Récupérer infos utilisateur + const [userInfo] = await conn.query( + 'SELECT prenom, nom, email, CampusId FROM CollaborateurAD WHERE id = ?', + [user_id] + ); + + if (userInfo.length === 0) { + await conn.rollback(); + conn.release(); + return res.json({ + success: false, + message: 'Utilisateur non trouvé' + }); + } + + const user = userInfo[0]; + const userName = `${user.prenom} ${user.nom}`; + const dateFormatted = dateObj.toLocaleDateString('fr-FR'); + + // Récupérer le type Récupération + const [recupType] = await conn.query( + 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', + ['Récupération'] + ); + + if (recupType.length === 0) { + await conn.rollback(); + conn.release(); + return res.json({ + success: false, + message: 'Type Récupération non trouvé' + }); + } + + const recupTypeId = recupType[0].Id; + const currentYear = dateObj.getFullYear(); + + // CRÉER LA DEMANDE (validée automatiquement) + const [result] = await conn.query(` + INSERT INTO DemandeConge + (CollaborateurADId, DateDebut, DateFin, TypeCongeId, + Statut, DateDemande, Commentaire, NombreJours) + VALUES (?, ?, ?, ?, 'Validée', NOW(), ?, ?) + `, [user_id, date, date, recupTypeId, commentaire || `Samedi travaillé - ${dateFormatted}`, nombre_heures]); + + const demandeId = result.insertId; + + // SAUVEGARDER DANS DemandeCongeType + await conn.query(` + INSERT INTO DemandeCongeType + (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) + VALUES (?, ?, ?, 'Journée entière') + `, [demandeId, recupTypeId, nombre_heures]); + + // ACCUMULER DANS LE COMPTEUR + const [compteur] = await conn.query(` + SELECT Id, Total, Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [user_id, recupTypeId, currentYear]); + + if (compteur.length > 0) { + await conn.query(` + UPDATE CompteurConges + SET Total = Total + ?, + Solde = Solde + ?, + DerniereMiseAJour = NOW() + WHERE Id = ? + `, [nombre_heures, nombre_heures, compteur[0].Id]); + + console.log(`✅ Compteur mis à jour: ${parseFloat(compteur[0].Solde) + nombre_heures}j`); + } else { + await conn.query(` + INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, NOW()) + `, [user_id, recupTypeId, currentYear, nombre_heures, nombre_heures]); + + console.log(`✅ Compteur créé: ${nombre_heures}j`); + } + + // ENREGISTRER L'ACCUMULATION + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Accum Récup', ?) + `, [demandeId, recupTypeId, currentYear, nombre_heures]); + + // CRÉER NOTIFICATION + await conn.query(` + INSERT INTO Notifications + (CollaborateurADId, Type, Titre, Message, DemandeCongeId, DateCreation, Lu) + VALUES (?, 'Success', '✅ Récupération enregistrée', ?, ?, NOW(), 0) + `, [ + user_id, + `Samedi ${dateFormatted} enregistré : +${nombre_heures}j de récupération`, + demandeId + ]); + + await conn.commit(); + conn.release(); + + res.json({ + success: true, + message: `Samedi ${dateFormatted} enregistré`, + jours_ajoutes: nombre_heures, + demande_id: demandeId + }); + + } catch (error) { + await conn.rollback(); + if (conn) conn.release(); + console.error('❌ Erreur saisie récup:', error); + res.status(500).json({ + success: false, + message: 'Erreur serveur', + error: error.message + }); + } +}); + +/** + * GET /getMesSamedis + * Récupérer les samedis déjà déclarés + */ +app.get('/api/getMesSamedis', async (req, res) => { + try { + const { user_id, annee } = req.query; + + const conn = await pool.getConnection(); + + const [samedis] = await conn.query(` + SELECT + dc.Id, + DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as date, + dc.NombreJours as jours, + dc.Commentaire as commentaire, + DATE_FORMAT(dc.DateDemande, '%d/%m/%Y à %H:%i') as date_saisie + FROM DemandeConge dc + JOIN TypeConge tc ON dc.TypeCongeId = tc.Id + WHERE dc.CollaborateurADId = ? + AND tc.Nom = 'Récupération' + AND YEAR(dc.DateDebut) = ? + ORDER BY dc.DateDebut DESC + `, [user_id, annee]); + + conn.release(); + + res.json({ + success: true, + samedis: samedis + }); + + } catch (error) { + console.error('Erreur getMesSamedis:', error); + res.status(500).json({ + success: false, + message: 'Erreur serveur', + error: error.message + }); + } +}); + +async function checkLeaveBalanceWithAnticipation(conn, collaborateurId, repartition, dateDebut) { + const dateDebutObj = new Date(dateDebut); + const currentYear = dateDebutObj.getFullYear(); + const previousYear = currentYear - 1; + + console.log('\n🔍 === CHECK SOLDES AVEC ANTICIPATION ==='); + console.log(`📅 Date demande: ${dateDebut}`); + console.log(`📅 Année demande: ${currentYear}`); + + const verification = []; + + for (const rep of repartition) { + const typeCode = rep.TypeConge; + const joursNecessaires = parseFloat(rep.NombreJours || 0); + + if (typeCode === 'ABS' || typeCode === 'Formation') { + continue; + } + + const typeName = typeCode === 'CP' ? 'Congé payé' : typeCode === 'RTT' ? 'RTT' : typeCode; + const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [typeName]); + + if (typeRow.length === 0) { + continue; + } + + const typeCongeId = typeRow[0].Id; + + // ==================================== + // 1️⃣ Récupérer les infos du collaborateur + // ==================================== + 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'; + + // ==================================== + // 2️⃣ Calculer l'acquisition à la date de la demande + // ==================================== + let acquisALaDate = 0; + let budgetAnnuel = 0; + + if (typeCode === 'CP') { + acquisALaDate = calculerAcquisitionCP(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); + acquisALaDate = rttData.acquisition; + budgetAnnuel = rttData.config.joursAnnuels; + console.log(`💰 Acquisition RTT à la date ${dateDebut}: ${acquisALaDate.toFixed(2)}j`); + } + + // ==================================== + // 3️⃣ Récupérer le report N-1 (CP uniquement) + // ==================================== + let reporteN1 = 0; + + if (typeCode === 'CP') { + const [compteurN1] = await conn.query(` + SELECT Solde, SoldeReporte + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, typeCongeId, previousYear]); + + if (compteurN1.length > 0) { + reporteN1 = parseFloat(compteurN1[0].Solde || 0); + } + } + + // ==================================== + // 4️⃣ Calculer ce qui a déjà été posé (SANS l'anticipé) + // ==================================== + const [dejaPose] = 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 ('N Anticip', 'N+1 Anticip', 'Accum Récup', 'Accum Recup') + AND dc.Statut != 'Refusée' + `, [collaborateurId, typeCongeId, currentYear]); + + const dejaPoseNormal = parseFloat(dejaPose[0]?.total || 0); + + // ==================================== + // 5️⃣ Calculer l'anticipé déjà utilisé + // ==================================== + 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 dejaPoseAnticipe = parseFloat(anticipeUtilise[0]?.total || 0); + + // ==================================== + // 6️⃣ Calculer l'anticipé disponible + // ==================================== + const anticipableMax = Math.max(0, budgetAnnuel - acquisALaDate); + const anticipeDisponible = Math.max(0, anticipableMax - dejaPoseAnticipe); + + console.log(`💳 Anticipé max possible: ${anticipableMax.toFixed(2)}j`); + console.log(`💳 Anticipé déjà utilisé: ${dejaPoseAnticipe.toFixed(2)}j`); + console.log(`💳 Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`); + + // ==================================== + // 7️⃣ Calculer le solde TOTAL disponible + // ==================================== + const soldeActuel = Math.max(0, reporteN1 + acquisALaDate - dejaPoseNormal); + const soldeTotal = soldeActuel + anticipeDisponible; + + console.log(`📊 Soldes détaillés ${typeCode}:`); + console.log(` - Report N-1: ${reporteN1.toFixed(2)}j`); + console.log(` - Acquis à date: ${acquisALaDate.toFixed(2)}j`); + console.log(` - Déjà posé (normal): ${dejaPoseNormal.toFixed(2)}j`); + console.log(` - Solde actuel: ${soldeActuel.toFixed(2)}j`); + console.log(` - Anticipé disponible: ${anticipeDisponible.toFixed(2)}j`); + console.log(` ✅ TOTAL DISPONIBLE: ${soldeTotal.toFixed(2)}j`); + + // ==================================== + // 8️⃣ Vérifier la suffisance + // ==================================== + const suffisant = soldeTotal >= joursNecessaires; + const deficit = Math.max(0, joursNecessaires - soldeTotal); + + verification.push({ + type: typeName, + joursNecessaires, + reporteN1, + acquisALaDate, + dejaPoseNormal, + dejaPoseAnticipe, + soldeActuel, + anticipeDisponible, + soldeTotal, + suffisant, + deficit + }); + + console.log(`🔍 Vérification ${typeCode}: ${joursNecessaires}j demandés vs ${soldeTotal.toFixed(2)}j disponibles → ${suffisant ? '✅ OK' : '❌ INSUFFISANT'}`); + } + + const insuffisants = verification.filter(v => !v.suffisant); + + return { + valide: insuffisants.length === 0, + details: verification, + insuffisants + }; +} +/** + * Déduit les jours d'un compteur avec gestion de l'anticipation + * Ordre de déduction : N-1 → N → N Anticip + */ +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(` 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 + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, typeCongeId, currentYear]); + + 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 + + const aDeduireN = Math.min(soldeN, joursRestants); + + if (aDeduireN > 0) { + await conn.query(` + UPDATE CompteurConges + SET Solde = GREATEST(0, Solde - ?), + DerniereMiseAJour = NOW() + WHERE Id = ? + `, [aDeduireN, compteurN[0].Id]); + + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Année N', ?) + `, [demandeCongeId, typeCongeId, currentYear, aDeduireN]); + + deductions.push({ + annee: currentYear, + type: 'Année N', + joursUtilises: aDeduireN, + soldeAvant: soldeN + }); + + joursRestants -= aDeduireN; + console.log(` ✓ Déduit ${aDeduireN.toFixed(2)}j du solde N actuel`); + } + } + } + + // ==================================== + // 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`); + + return { + success: joursRestants === 0, + joursDeduitsTotal: nombreJours - joursRestants, + joursNonDeduits: joursRestants, + details: deductions + }; +} + + +app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -2731,7 +3994,6 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn const Repartition = JSON.parse(req.body.Repartition || '[]'); if (!DateDebut || !DateFin || !Repartition || !Email || !Nom) { - // ✅ Nettoyer les fichiers en cas d'erreur uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); @@ -2751,34 +4013,31 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn }); } - // ⭐ NOUVEAU : Validation de la répartition + // ⭐ VALIDATION DE LA RÉPARTITION console.log('\n📥 === SOUMISSION DEMANDE CONGÉ ==='); console.log('Email:', Email); console.log('Période:', DateDebut, '→', DateFin); console.log('Nombre de jours total:', NombreJours); console.log('Répartition reçue:', JSON.stringify(Repartition, null, 2)); - console.log('Fichiers médicaux:', uploadedFiles.length); - // ⭐ Calculer la somme de la répartition + // ⭐ Ne compter que CP, RTT ET RÉCUP dans la répartition const sommeRepartition = Repartition.reduce((sum, r) => { - // Ne compter que CP et RTT (pas ABS ni Formation ni Récup) - if (r.TypeConge === 'CP' || r.TypeConge === 'RTT') { + if (r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup') { return sum + parseFloat(r.NombreJours || 0); } return sum; }, 0); - console.log('Somme répartition CP+RTT:', sommeRepartition.toFixed(2)); + console.log('Somme répartition CP+RTT+Récup:', sommeRepartition.toFixed(2)); - // ⭐ VALIDATION : La somme doit correspondre au total (tolérance 0.01j) - const hasCountableLeave = Repartition.some(r => r.TypeConge === 'CP' || r.TypeConge === 'RTT'); + // ⭐ VALIDATION : La somme doit correspondre au total + const hasCountableLeave = Repartition.some(r => + r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup' + ); if (hasCountableLeave && Math.abs(sommeRepartition - NombreJours) > 0.01) { console.error('❌ ERREUR : Répartition incohérente !'); - console.error(` Attendu: ${NombreJours}j`); - console.error(` Reçu: ${sommeRepartition}j`); - // ✅ Nettoyer les fichiers uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); @@ -2790,59 +4049,26 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn return res.json({ success: false, - message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${NombreJours}j)`, - details: { - repartition: Repartition, - somme: sommeRepartition, - attendu: NombreJours - } + message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${NombreJours}j)` }); } - // ⭐ Vérifier qu'aucun type n'a 0 jour - for (const rep of Repartition) { - if ((rep.TypeConge === 'CP' || rep.TypeConge === 'RTT') && parseFloat(rep.NombreJours || 0) <= 0) { - console.error(`❌ ERREUR : ${rep.TypeConge} a ${rep.NombreJours} jours !`); - - // ✅ Nettoyer les fichiers - uploadedFiles.forEach(file => { - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - }); - - await conn.rollback(); - conn.release(); - - return res.json({ - success: false, - message: `Le type ${rep.TypeConge} doit avoir au moins 0.5 jour` - }); - } - } - console.log('✅ Validation répartition OK'); - // ⭐ Détection si c'est uniquement une formation + // ⭐ Récup n'est PAS une demande auto-validée const isFormationOnly = Repartition.length === 1 && Repartition[0].TypeConge === 'Formation'; const statutDemande = statut || (isFormationOnly ? 'Validée' : 'En attente'); - const dateDebut = new Date(DateDebut).toLocaleDateString('fr-FR'); - const dateFin = new Date(DateFin).toLocaleDateString('fr-FR'); - const datesPeriode = dateDebut === dateFin ? dateDebut : `du ${dateDebut} au ${dateFin}`; - console.log('🔍 Type de demande:', { isFormationOnly, statut: statutDemande }); const [collabAD] = await conn.query('SELECT id, CampusId FROM CollaborateurAD WHERE email = ? LIMIT 1', [Email]); const isAD = collabAD.length > 0; const collaborateurId = isAD ? collabAD[0].id : null; - const dateEntree = isAD && collabAD[0].DateEntree ? collabAD[0].DateEntree : null; let employeeId = null; if (!isAD) { const [user] = await conn.query('SELECT ID FROM Users WHERE Email = ? LIMIT 1', [Email]); if (user.length === 0) { - // ✅ Nettoyer les fichiers uploadedFiles.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); @@ -2856,102 +4082,51 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn } // ======================================== - // ÉTAPE 1 : Vérification des soldes AVANT tout + // ÉTAPE 1 : Vérification des soldes AVANT tout (MODE MIXTE AVEC ANTICIPATION N+1) // ======================================== if (isAD && collaborateurId && !isFormationOnly) { - console.log('\n🔍 Vérification des soldes (avec anticipation)...'); + console.log('\n🔍 Vérification des soldes en mode mixte avec anticipation...'); + console.log('Date début:', DateDebut); const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]); const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti'; - for (const rep of Repartition) { - if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation' || rep.TypeConge === 'Récup') { - continue; - } - if (rep.TypeConge === 'RTT' && isApprenti) { - uploadedFiles.forEach(file => { - if (fs.existsSync(file.path)) fs.unlinkSync(file.path); - }); - await conn.rollback(); - conn.release(); + // ⭐ CORRECTION : Passer la date de début pour détecter N+1 + // ✅ APRÈS (avec anticipation) + const checkResult = await checkLeaveBalanceWithAnticipation( + conn, + collaborateurId, + Repartition, + DateDebut + ); - return res.json({ - success: false, - message: `❌ Les apprentis ne peuvent pas poser de RTT` - }); - } + // Adapter le format de la réponse + if (!checkResult.valide) { + uploadedFiles.forEach(file => { + if (fs.existsSync(file.path)) fs.unlinkSync(file.path); + }); + 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'); - const joursNecessaires = parseFloat(rep.NombreJours); - const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT'; - - const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); - - if (typeRow.length === 0) continue; - - const typeCongeId = typeRow[0].Id; - - // Récupérer le solde actuel - const [compteur] = await conn.query(` - SELECT Total, Solde, SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, currentYear]); - - let soldeDisponible = 0; - - if (compteur.length > 0) { - soldeDisponible = parseFloat(compteur[0].Solde || 0); - } - - // Si le solde est insuffisant, calculer l'anticipation possible - if (soldeDisponible < joursNecessaires) { - const manque = joursNecessaires - soldeDisponible; - - // Calculer l'acquisition future - let acquisitionFuture = 0; - - if (rep.TypeConge === 'CP') { - const finExercice = new Date(currentYear + 1, 4, 31); - const acquisTotale = calculerAcquisitionCP(finExercice, dateEntree); - const acquisActuelle = compteur.length > 0 ? parseFloat(compteur[0].Total || 0) : 0; - acquisitionFuture = acquisTotale - acquisActuelle; - } else { - const finAnnee = new Date(currentYear, 11, 31); - const rttDataTotal = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee); - const acquisActuelle = compteur.length > 0 ? parseFloat(compteur[0].Total || 0) : 0; - acquisitionFuture = rttDataTotal.acquisition - acquisActuelle; - } - - // Vérifier si l'anticipation est possible - if (manque > acquisitionFuture) { - uploadedFiles.forEach(file => { - if (fs.existsSync(file.path)) fs.unlinkSync(file.path); - }); - await conn.rollback(); - conn.release(); - - return res.json({ - success: false, - message: `❌ Solde insuffisant pour ${name}`, - details: { - type: name, - demande: joursNecessaires, - soldeActuel: soldeDisponible.toFixed(2), - acquisitionFutureMax: acquisitionFuture.toFixed(2), - manque: (manque - acquisitionFuture).toFixed(2) - } - }); - } - - console.log(`⚠️ ${name}: Utilisation de ${manque.toFixed(2)}j en anticipé`); - } + return res.json({ + success: false, + message: `❌ Solde(s) insuffisant(s):\n${messagesErreur}`, + details: checkResult.details, + insuffisants: checkResult.insuffisants + }); } - console.log('✅ Soldes suffisants (avec anticipation si nécessaire)'); + + console.log('✅ Tous les soldes sont suffisants (incluant anticipation si nécessaire)\n'); } + // ======================================== - // ÉTAPE 2 : CRÉER LA DEMANDE EN PREMIER + // ÉTAPE 2 : CRÉER LA DEMANDE // ======================================== console.log('\n📝 Création de la demande...'); @@ -2959,22 +4134,24 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn for (const rep of Repartition) { const code = rep.TypeConge; - // Ne pas inclure ABS, Formation, Récup dans les typeIds principaux - if (code === 'ABS' || code === 'Formation' || code === 'Récup') { + // Ne pas inclure ABS et Formation dans les typeIds principaux + if (code === 'ABS' || code === 'Formation') { continue; } - const name = code === 'CP' ? 'Congé payé' : 'RTT'; + const name = code === 'CP' ? 'Congé payé' : + code === 'RTT' ? 'RTT' : + code === 'Récup' ? 'Récupération' : code; + const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); if (typeRow.length > 0) typeIds.push(typeRow[0].Id); } - // ⭐ Si aucun type CP/RTT, prendre le premier type de la répartition + // 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' : - firstType === 'ABS' ? 'Congé maladie' : - firstType === 'Récup' ? 'Récupération' : 'Congé payé'; + firstType === 'ABS' ? 'Congé maladie' : 'Congé payé'; const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); if (typeRow.length > 0) { @@ -3015,7 +4192,7 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn VALUES (?, ?, ?, ?, ?, NOW())`, [demandeId, file.originalname, file.path, file.mimetype, file.size] ); - console.log(` ✓ ${file.originalname} (${(file.size / 1024).toFixed(2)} KB)`); + console.log(` ✓ ${file.originalname}`); } } @@ -3039,13 +4216,13 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn if (typeRow.length > 0) { await conn.query( `INSERT INTO DemandeCongeType - (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) - VALUES (?, ?, ?, ?)`, + (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) + VALUES (?, ?, ?, ?)`, [ demandeId, typeRow[0].Id, rep.NombreJours, - rep.PeriodeJournee || 'Journée entière' // ✅ NOUVELLE COLONNE + rep.PeriodeJournee || 'Journée entière' ] ); console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`); @@ -3053,85 +4230,52 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn } // ======================================== - // ÉTAPE 5 : ACCUMULATION DES RÉCUP (maintenant demandeId existe) + // ÉTAPE 5 : Déduction des compteurs CP/RTT/RÉCUP (AVEC ANTICIPATION N+1) // ======================================== if (isAD && collaborateurId && !isFormationOnly) { - const hasRecup = Repartition.some(r => r.TypeConge === 'Récup'); - - if (hasRecup) { - console.log('\n📥 Accumulation des jours de récupération...'); - - const [recupType] = await conn.query( - 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', - ['Récupération'] - ); - - if (recupType.length > 0) { - const recupJours = Repartition.find(r => r.TypeConge === 'Récup')?.NombreJours || 0; - - if (recupJours > 0) { - const [compteurExisting] = await conn.query(` - SELECT Id, Total, Solde FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, recupType[0].Id, currentYear]); - - if (compteurExisting.length > 0) { - // ⭐ AJOUTER les jours au compteur existant - await conn.query(` - UPDATE CompteurConges - SET Total = Total + ?, - Solde = Solde + ?, - DerniereMiseAJour = NOW() - WHERE Id = ? - `, [recupJours, recupJours, compteurExisting[0].Id]); - - console.log(` ✓ Récupération: +${recupJours}j ajoutés (nouveau solde: ${(parseFloat(compteurExisting[0].Solde) + recupJours).toFixed(2)}j)`); - } else { - // ⭐ CRÉER le compteur avec les jours - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW()) - `, [collaborateurId, recupType[0].Id, currentYear, recupJours, recupJours]); - - console.log(` ✓ Récupération: ${recupJours}j créés (nouveau compteur)`); - } - - // ⭐ Enregistrer l'ACCUMULATION (maintenant demandeId existe !) - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Accum Récup', ?) - `, [demandeId, recupType[0].Id, currentYear, recupJours]); - - console.log(` ✓ Accumulation enregistrée dans DeductionDetails`); - } - } - } - } - - // ======================================== - // ÉTAPE 6 : Déduction des compteurs CP/RTT - // ======================================== - if (isAD && collaborateurId && !isFormationOnly) { - console.log('\n📉 Déduction des compteurs...'); + console.log('\n📉 Déduction des compteurs (avec anticipation N+1)...'); for (const rep of Repartition) { - if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation' || rep.TypeConge === 'Récup') { + if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') { console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`); continue; } + // ⭐ TRAITEMENT SPÉCIAL POUR RÉCUP + if (rep.TypeConge === 'Récup') { + const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); + + if (recupType.length > 0) { + await conn.query(` + UPDATE CompteurConges + SET Solde = GREATEST(0, Solde - ?), + DerniereMiseAJour = NOW() + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [rep.NombreJours, collaborateurId, recupType[0].Id, currentYear]); + + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Récup Posée', ?) + `, [demandeId, recupType[0].Id, currentYear, rep.NombreJours]); + + console.log(` ✓ Récup: ${rep.NombreJours}j déduits`); + } + continue; + } + + // ⭐ CP et RTT : AVEC ANTICIPATION N+1 const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT'; const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); if (typeRow.length > 0) { - const result = await deductLeaveBalanceWithTracking( + const result = await deductLeaveBalanceWithAnticipation( conn, collaborateurId, typeRow[0].Id, rep.NombreJours, - demandeId + demandeId, + DateDebut ); console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`); @@ -3143,20 +4287,24 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn } } - console.log('✅ Déductions terminées'); + await updateSoldeAnticipe(conn, collaborateurId); + console.log('✅ Déductions terminées\n'); } // ======================================== - // ÉTAPE 7 : Créer notification pour formation + // ÉTAPE 6 : Notifications (Formation uniquement) // ======================================== - // ÉTAPE 7 : Créer notification pour formation + const dateDebut = new Date(DateDebut).toLocaleDateString('fr-FR'); + const dateFin = new Date(DateFin).toLocaleDateString('fr-FR'); + 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)`, + VALUES (?, ?, ?, ?, ?, NOW(), 0)`, [ collaborateurId, - 'Success', // ✅ Valeur correcte de l'enum + 'Success', '✅ Formation validée automatiquement', `Votre période de formation ${datesPeriode} a été validée automatiquement.`, demandeId @@ -3166,7 +4314,7 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn } // ======================================== - // ÉTAPE 8 : Récupérer les managers + // ÉTAPE 7 : Récupérer les managers // ======================================== let managers = []; if (isAD) { @@ -3177,24 +4325,13 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn [collaborateurId] ); managers = rows.map(r => r.email); - } else { - const [rows] = await conn.query( - `SELECT u.Email FROM HierarchieValidation hv - JOIN Users u ON hv.SuperieurId = u.ID - WHERE hv.EmployeId = ?`, - [employeeId] - ); - managers = rows.map(r => r.Email); } - // ======================================== - // COMMIT DE LA TRANSACTION - // ======================================== await conn.commit(); console.log('\n🎉 Transaction validée\n'); // ======================================== - // ÉTAPE 9 : Notifier les clients SSE + // ÉTAPE 8 : Notifier les clients SSE // ======================================== if (isFormationOnly && isAD && collaborateurId) { notifyCollabClients({ @@ -3203,77 +4340,46 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn statut: 'Validée', timestamp: new Date().toISOString() }, collaborateurId); - - notifyCollabClients({ - type: 'demande-list-updated', - action: 'formation-auto-validated', - demandeId: parseInt(demandeId), - timestamp: new Date().toISOString() - }); } // ======================================== - // ENVOI DES EMAILS (code inchangé) + // ENVOI DES EMAILS // ======================================== const accessToken = await getGraphToken(); if (accessToken) { - const fromEmail = 'noreply@ensup.eu'; + const fromEmail = 'gtanoreply@ensup.eu'; const typesConges = Repartition.map(rep => { const typeNom = rep.TypeConge === 'CP' ? 'Congé payé' : rep.TypeConge === 'RTT' ? 'RTT' : rep.TypeConge === 'ABS' ? 'Congé maladie' : rep.TypeConge === 'Formation' ? 'Formation' : - rep.TypeConge; + rep.TypeConge === 'Récup' ? 'Récupération' : rep.TypeConge; return `${typeNom}: ${rep.NombreJours}j`; }).join(' | '); if (isFormationOnly) { - const subjectCollab = '✅ Votre saisie de période de formation a été enregistrée'; + // Email formation + const subjectCollab = '✅ Formation enregistrée et validée'; const bodyCollab = `
-
-

✅ Formation enregistrée

+
+

✅ Formation validée

-
-

Bonjour ${Nom},

-

Votre période de formation a bien été enregistrée et validée automatiquement.

-
-

Type : Formation

-

Période : ${datesPeriode}

-

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

- ${Commentaire ? `

Description : ${Commentaire}

` : ''} -
+
+

Bonjour ${Nom},

+

Votre période de formation a été automatiquement validée.

+

Période : ${datesPeriode}

+

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

`; try { await sendMailGraph(accessToken, fromEmail, Email, subjectCollab, bodyCollab); - console.log('✅ Email confirmation formation envoyé'); } catch (mailError) { console.error('❌ Erreur email:', mailError.message); } - - for (const managerEmail of managers) { - const subjectManager = `📚 Information : Formation déclarée - ${Nom}`; - const bodyManager = ` -
-
-

📚 Formation enregistrée

-
-
-

${Nom} vous informe d'une période de formation.

-

Période : ${datesPeriode}

-

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

-
-
- `; - try { - await sendMailGraph(accessToken, fromEmail, managerEmail, subjectManager, bodyManager); - } catch (mailError) { - console.error('❌ Erreur email manager:', mailError.message); - } - } } else { + // ⭐ EMAIL NORMAL (incluant Récup) const subjectCollab = '✅ Confirmation de réception de votre demande de congé'; const bodyCollab = `
@@ -3330,7 +4436,6 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn } catch (error) { await conn.rollback(); - // ✅ Nettoyer les fichiers uploadés en cas d'erreur if (req.files) { req.files.forEach(file => { if (fs.existsSync(file.path)) { @@ -3351,7 +4456,8 @@ app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), asyn } }); -app.get('/download-medical/:documentId', async (req, res) => { + +app.get('/api/download-medical/:documentId', async (req, res) => { try { const { documentId } = req.params; const conn = await pool.getConnection(); @@ -3382,7 +4488,7 @@ app.get('/download-medical/:documentId', async (req, res) => { }); // Récupérer les documents d'une demande -app.get('/medical-documents/:demandeId', async (req, res) => { +app.get('/api/medical-documents/:demandeId', async (req, res) => { try { const { demandeId } = req.params; const conn = await pool.getConnection(); @@ -3414,7 +4520,7 @@ app.get('/medical-documents/:demandeId', async (req, res) => { res.status(500).json({ success: false, message: 'Erreur serveur' }); } }); -app.post('/validateRequest', async (req, res) => { +app.post('/api/validateRequest', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -3505,7 +4611,7 @@ app.post('/validateRequest', async (req, res) => { console.log('2. Token obtenu ?', accessToken ? 'OUI' : 'NON'); if (accessToken && request.collaborateur_email) { - const fromEmail = 'noreply@ensup.eu'; + const fromEmail = 'gtanoreply@ensup.eu'; const collaborateurNom = `${request.prenom} ${request.nom}`; const validateurNom = `${validator[0].prenom} ${validator[0].nom}`; @@ -3633,7 +4739,7 @@ app.post('/validateRequest', async (req, res) => { } }); -app.get('/testRestoration', async (req, res) => { +app.get('/api/testRestoration', async (req, res) => { const conn = await pool.getConnection(); try { const { demande_id, collab_id } = req.query; @@ -3713,15 +4819,76 @@ function normalizeRole(role) { return roleLower; } +app.get('/api/getSocietesByCampus', async (req, res) => { + try { + const { campusId } = req.query; + + const conn = await pool.getConnection(); + + const [societes] = await conn.query(` + SELECT DISTINCT s.Id, s.Nom + FROM SocieteCampus sc + JOIN Societe s ON sc.SocieteId = s.Id + WHERE sc.CampusId = ? + ORDER BY + CASE WHEN s.Nom LIKE '%SOLUTION%' THEN 1 ELSE 2 END, + s.Nom + `, [campusId]); + + conn.release(); + + res.json({ + success: true, + societes: societes + }); + + } catch (error) { + console.error('Erreur getSocietesByCampus:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +// ⭐ NOUVELLE ROUTE HELPER : Récupérer les campus d'une société +app.get('/api/getCampusBySociete', async (req, res) => { + try { + const { societeId } = req.query; + + const conn = await pool.getConnection(); + + const [campus] = await conn.query(` + SELECT DISTINCT c.Id, c.Nom, sc.Principal + FROM SocieteCampus sc + JOIN Campus c ON sc.CampusId = c.Id + WHERE sc.SocieteId = ? + ORDER BY + sc.Principal DESC, -- Principal en premier + c.Nom + `, [societeId]); + + conn.release(); + + res.json({ + success: true, + campus: campus, + isMultiCampus: campus.length > 1 + }); + + } catch (error) { + console.error('Erreur getCampusBySociete:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + + // ======================================== // ROUTE getTeamLeaves COMPLÈTE // ======================================== -app.get('/getTeamLeaves', async (req, res) => { +app.get('/api/getTeamLeaves', async (req, res) => { try { - const { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query; + let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query; console.log(`🔍 Paramètres reçus: user_id=${userIdParam}, role=${roleParam}, selectedCampus=${selectedCampus}`); @@ -3791,34 +4958,141 @@ app.get('/getTeamLeaves', async (req, res) => { let query, params; const filters = {}; + + // ======================================== // CAS 1: PRESIDENT, ADMIN, RH // ======================================== - if (role === 'president' || role === 'admin' || role === 'rh') { - console.log("CAS 1: President/Admin/RH - Vue globale"); + // ======================================== + // CAS 1: PRESIDENT, ADMIN, RH, DIRECTEUR DE CAMPUS + // ======================================== + if (role === 'president' || role === 'admin' || role === 'rh' || role === 'directeur de campus') { + console.log("CAS 1: President/Admin/RH/Directeur de Campus - Vue globale"); + console.log(` Filtres reçus: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); - const [campusList] = await conn.query(`SELECT DISTINCT Nom FROM Campus ORDER BY Nom`); - filters.campus = campusList.map(c => c.Nom); + // ======================================== + // 🔧 LISTE COMPLÈTE DES FILTRES DISPONIBLES + // ======================================== - const [societesList] = await conn.query(`SELECT DISTINCT Nom FROM Societe ORDER BY Nom`); + // 1️⃣ SOCIÉTÉS (toutes disponibles) + const [societesList] = await conn.query(` + SELECT DISTINCT Nom + FROM Societe + ORDER BY Nom + `); filters.societes = societesList.map(s => s.Nom); + console.log('📊 Sociétés disponibles:', filters.societes); - const [servicesList] = await conn.query(`SELECT DISTINCT Nom FROM Services ORDER BY Nom`); + // 2️⃣ CAMPUS (tous les campus, filtrés par société si nécessaire) + let campusQuery; + let campusParams = []; + + if (selectedSociete && selectedSociete !== 'all') { + campusQuery = ` + SELECT DISTINCT c.Nom + FROM Campus c + JOIN CollaborateurAD ca ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + WHERE so.Nom = ? + AND (ca.actif = 1 OR ca.actif IS NULL) + ORDER BY c.Nom + `; + campusParams = [selectedSociete]; + } else { + campusQuery = ` + SELECT DISTINCT Nom + FROM Campus + ORDER BY Nom + `; + } + + const [campusList] = await conn.query(campusQuery, campusParams); + filters.campus = campusList.map(c => c.Nom); + console.log('📊 Campus disponibles:', filters.campus); + + // ⭐ NOUVEAU : Pour directeur de campus, envoyer son campus par défaut + if (role === 'directeur de campus') { + filters.defaultCampus = campusNom; // Le campus du directeur + console.log('🏢 Campus par défaut pour directeur:', campusNom); + } + + // 3️⃣ SERVICES (filtrés selon société + campus) + let servicesQuery = ` + SELECT DISTINCT s.Nom + FROM Services s + JOIN CollaborateurAD ca ON ca.ServiceId = s.Id + `; + + let servicesJoins = []; + let servicesConditions = ['(ca.actif = 1 OR ca.actif IS NULL)']; + let servicesParams = []; + + if (selectedSociete && selectedSociete !== 'all') { + servicesJoins.push('JOIN Societe so ON ca.SocieteId = so.Id'); + servicesConditions.push('so.Nom = ?'); + servicesParams.push(selectedSociete); + } + + if (selectedCampus && selectedCampus !== 'all') { + servicesJoins.push('JOIN Campus c ON ca.CampusId = c.Id'); + servicesConditions.push('c.Nom = ?'); + servicesParams.push(selectedCampus); + } + + if (servicesJoins.length > 0) { + servicesQuery += '\n' + servicesJoins.join('\n'); + } + + servicesQuery += `\nWHERE ${servicesConditions.join(' AND ')}\nORDER BY s.Nom`; + + const [servicesList] = await conn.query(servicesQuery, servicesParams); filters.services = servicesList.map(s => s.Nom); - const [employeesList] = await conn.query(` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - c.Nom AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY ca.prenom, ca.nom - `); + // ======================================== + // 🔧 LISTE DES EMPLOYÉS (avec filtres appliqués) + // ======================================== + let employeesQuery = ` + SELECT DISTINCT + CONCAT(ca.prenom, ' ', ca.nom) AS fullname, + c.Nom AS campusnom, + so.Nom AS societenom, + s.Nom AS servicenom + FROM CollaborateurAD ca + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; + + let employeesConditions = []; + let employeesParams = []; + + if (selectedSociete && selectedSociete !== 'all') { + employeesConditions.push('so.Nom = ?'); + employeesParams.push(selectedSociete); + } + + if (selectedCampus && selectedCampus !== 'all') { + employeesConditions.push('c.Nom = ?'); + employeesParams.push(selectedCampus); + } else if (role === 'directeur de campus' && campusNom) { + // ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut + employeesConditions.push('c.Nom = ?'); + employeesParams.push(campusNom); + } + + if (selectedService && selectedService !== 'all') { + employeesConditions.push('s.Nom = ?'); + employeesParams.push(selectedService); + } + + if (employeesConditions.length > 0) { + employeesQuery += ` AND ${employeesConditions.join(' AND ')}`; + } + + employeesQuery += ` ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom`; + + const [employeesList] = await conn.query(employeesQuery, employeesParams); filters.employees = employeesList.map(e => ({ name: e.fullname, @@ -3827,216 +5101,207 @@ app.get('/getTeamLeaves', async (req, res) => { service: e.servicenom })); + console.log(`👥 Employés trouvés:`, filters.employees.length); + + // ======================================== + // 🔧 QUERY DES CONGÉS (avec filtres appliqués) + // ======================================== + let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`]; + let whereParams = []; + + if (selectedSociete && selectedSociete !== 'all') { + whereConditions.push('so.Nom = ?'); + whereParams.push(selectedSociete); + } + + if (selectedCampus && selectedCampus !== 'all') { + whereConditions.push('c.Nom = ?'); + whereParams.push(selectedCampus); + } else if (role === 'directeur de campus' && campusNom) { + // ⭐ NOUVEAU : Si directeur et pas de filtre campus, filtrer par son campus par défaut + whereConditions.push('c.Nom = ?'); + whereParams.push(campusNom); + } + + if (selectedService && selectedService !== 'all') { + whereConditions.push('s.Nom = ?'); + whereParams.push(selectedService); + } + query = ` - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, - CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, - CONCAT( - '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' - ), - ']' - ) AS detailsconges, - MAX(tc.CouleurHex) AS color, - dc.Statut AS statut, - s.Nom AS servicenom, - c.Nom AS campusnom, - so.Nom AS societenom, - dc.NombreJours AS nombrejoursouvres - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours - ORDER BY c.Nom, dc.DateDebut ASC - `; - params = []; + SELECT + DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, + DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, + CONCAT(ca.prenom, ' ', ca.nom) AS employeename, + GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, + CONCAT( + '[', + GROUP_CONCAT( + JSON_OBJECT( + 'type', tc.Nom, + 'jours', dct.NombreJours, + 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') + ) + SEPARATOR ',' + ), + ']' + ) AS detailsconges, + MAX(tc.CouleurHex) AS color, + dc.Statut AS statut, + s.Nom AS servicenom, + c.Nom AS campusnom, + so.Nom AS societenom, + dc.NombreJours AS nombrejoursouvres + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + WHERE ${whereConditions.join(' AND ')} + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours + ORDER BY so.Nom, c.Nom, dc.DateDebut ASC + `; + params = whereParams; + + console.log(`🔍 Query finale WHERE:`, whereConditions.join(' AND ')); + console.log(`🔍 Params:`, whereParams); } + + // ======================================== - // CAS 2: DIRECTEUR/DIRECTRICE DE CAMPUS + // CAS 3: COLLABORATEUR // ======================================== - else if (role === 'directeur de campus' || role === 'directrice de campus') { - console.log("CAS 2: Directeur de campus"); - console.log(` Campus: ${campusNom} (ID: ${campusId})`); - console.log(` Filtres reçus: Société=${selectedSociete}, Service=${selectedService}`); + // Dans la route /getTeamLeaves, modifiez la section CAS 3: COLLABORATEUR + // ======================================== + // CAS 3: COLLABORATEUR + // ======================================== + // ======================================== + // CAS 3: COLLABORATEUR + // ======================================== + else if (role === 'collaborateur' || role === 'validateur' || role === 'apprenti') { + console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés"); - filters.societes = ['Ensup', 'Ensup Solution et Support']; + const serviceNom = userInfo.serviceNom || 'Non défini'; + const campusNom = userInfo.campusNom || 'Non défini'; + const societeNom = userInfo.societeNom || 'Non défini'; - let servicesQuery; + console.log(`📍 Filtres reçus du frontend: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); + + // ⭐ NOUVEAU : Si AUCUN filtre n'est envoyé (premier chargement), utiliser les valeurs par défaut + // Sinon, respecter EXACTEMENT ce que le frontend envoie (même "all") + const isFirstLoad = !selectedCampus && !selectedService && !selectedSociete; + + if (isFirstLoad) { + console.log('🎯 Premier chargement : initialisation avec service par défaut'); + selectedCampus = campusNom; + selectedService = serviceNom; + selectedSociete = societeNom; + } + // Si le frontend envoie "all", on garde "all" (ne pas forcer les valeurs par défaut) + + console.log(`📍 Filtres appliqués finaux: Société=${selectedSociete}, Campus=${selectedCampus}, Service=${selectedService}`); + + // ⭐ Construire les listes de filtres disponibles + // 1️⃣ Sociétés disponibles (TOUTES) + const [societesList] = await conn.query(` + SELECT DISTINCT so.Nom + FROM Societe so + JOIN CollaborateurAD ca ON ca.SocieteId = so.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + ORDER BY so.Nom + `); + filters.societes = societesList.map(s => s.Nom); + + // 2️⃣ Campus disponibles (filtrés par société si sélectionné) + let campusQuery = ` + SELECT DISTINCT c.Nom + FROM Campus c + JOIN CollaborateurAD ca ON ca.CampusId = c.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; + let campusParams = []; + + if (selectedSociete && selectedSociete !== 'all') { + campusQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`; + campusParams.push(selectedSociete); + } + + campusQuery += ` ORDER BY c.Nom`; + const [campusList] = await conn.query(campusQuery, campusParams); + filters.campus = campusList.map(c => c.Nom); + + // 3️⃣ Services disponibles (filtrés par société + campus) + let servicesQuery = ` + SELECT DISTINCT s.Nom + FROM Services s + JOIN CollaborateurAD ca ON ca.ServiceId = s.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; let servicesParams = []; - if (selectedSociete === 'Ensup Solution et Support') { - servicesQuery = ` - SELECT DISTINCT s.Nom - FROM Services s - JOIN CollaborateurAD ca ON ca.ServiceId = s.Id - WHERE ca.SocieteId = 1 - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY s.Nom - `; - servicesParams = []; - } else if (selectedSociete === 'Ensup') { - servicesQuery = ` - SELECT DISTINCT s.Nom - FROM Services s - JOIN CollaborateurAD ca ON ca.ServiceId = s.Id - WHERE ca.SocieteId = 2 - AND ca.CampusId = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY s.Nom - `; - servicesParams = [campusId]; - } else { - servicesQuery = ` - SELECT DISTINCT s.Nom - FROM Services s - JOIN CollaborateurAD ca ON ca.ServiceId = s.Id - WHERE ( - (ca.SocieteId = 2 AND ca.CampusId = ?) - OR (ca.SocieteId = 1) - ) - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY s.Nom - `; - servicesParams = [campusId]; + if (selectedSociete && selectedSociete !== 'all') { + servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`; + servicesParams.push(selectedSociete); } + if (selectedCampus && selectedCampus !== 'all') { + servicesQuery += ` AND ca.CampusId = (SELECT Id FROM Campus WHERE Nom = ? LIMIT 1)`; + servicesParams.push(selectedCampus); + } + + servicesQuery += ` ORDER BY s.Nom`; const [servicesList] = await conn.query(servicesQuery, servicesParams); filters.services = servicesList.map(s => s.Nom); - console.log(`📊 Services trouvés:`, filters.services.length, filters.services); - let employeesQuery; + // ⭐ Envoyer les valeurs par défaut au frontend (pour initialisation) + filters.defaultCampus = campusNom; + filters.defaultService = serviceNom; + filters.defaultSociete = societeNom; + + // ⭐ LISTE DES EMPLOYÉS (avec filtres conditionnels) + let employeesQuery = ` + SELECT DISTINCT + CONCAT(ca.prenom, ' ', ca.nom) AS fullname, + c.Nom AS campusnom, + so.Nom AS societenom, + s.Nom AS servicenom + FROM CollaborateurAD ca + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + WHERE (ca.actif = 1 OR ca.actif IS NULL) + `; + let employeesParams = []; + let employeesConditions = []; - if (selectedSociete === 'Ensup Solution et Support') { - if (selectedService && selectedService !== 'all') { - employeesQuery = ` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.SocieteId = 1 - AND s.Nom = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY ca.prenom, ca.nom - `; - employeesParams = [selectedService]; - } else { - employeesQuery = ` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.SocieteId = 1 - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY ca.prenom, ca.nom - `; - employeesParams = []; - } - } else if (selectedSociete === 'Ensup') { - if (selectedService && selectedService !== 'all') { - employeesQuery = ` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - c.Nom AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.SocieteId = 2 - AND ca.CampusId = ? - AND s.Nom = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY ca.prenom, ca.nom - `; - employeesParams = [campusId, selectedService]; - } else { - employeesQuery = ` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - c.Nom AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.SocieteId = 2 - AND ca.CampusId = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY ca.prenom, ca.nom - `; - employeesParams = [campusId]; - } - } else { - if (selectedService && selectedService !== 'all') { - employeesQuery = ` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ( - (ca.SocieteId = 2 AND ca.CampusId = ?) - OR (ca.SocieteId = 1) - ) - AND s.Nom = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY ca.prenom, ca.nom - `; - employeesParams = [campusId, selectedService]; - } else { - employeesQuery = ` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ( - (ca.SocieteId = 2 AND ca.CampusId = ?) - OR (ca.SocieteId = 1) - ) - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY ca.prenom, ca.nom - `; - employeesParams = [campusId]; - } + // ⭐ N'ajouter les filtres QUE si différents de "all" + if (selectedSociete && selectedSociete !== 'all') { + employeesConditions.push('so.Nom = ?'); + employeesParams.push(selectedSociete); } + if (selectedCampus && selectedCampus !== 'all') { + employeesConditions.push('c.Nom = ?'); + employeesParams.push(selectedCampus); + } + + if (selectedService && selectedService !== 'all') { + employeesConditions.push('s.Nom = ?'); + employeesParams.push(selectedService); + } + + if (employeesConditions.length > 0) { + employeesQuery += ` AND ${employeesConditions.join(' AND ')}`; + } + + employeesQuery += ` ORDER BY s.Nom, ca.prenom, ca.nom`; + const [employeesList] = await conn.query(employeesQuery, employeesParams); + filters.employees = employeesList.map(emp => ({ name: emp.fullname, campus: emp.campusnom, @@ -4044,343 +5309,74 @@ app.get('/getTeamLeaves', async (req, res) => { service: emp.servicenom })); - console.log(`👥 Employés trouvés:`, filters.employees.length); + console.log(`👥 Employés trouvés: ${filters.employees.length}`); - let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`]; - let whereParams = []; + // ⭐ QUERY DES CONGÉS (avec mêmes filtres conditionnels) + let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`; + params = []; + let congesConditions = []; - if (selectedSociete === 'Ensup Solution et Support') { - whereConditions.push(`ca.SocieteId = 1`); - } else if (selectedSociete === 'Ensup') { - whereConditions.push(`ca.SocieteId = 2 AND ca.CampusId = ?`); - whereParams.push(campusId); - } else { - whereConditions.push(`((ca.SocieteId = 2 AND ca.CampusId = ?) OR (ca.SocieteId = 1))`); - whereParams.push(campusId); + // ⭐ N'ajouter les filtres QUE si différents de "all" + if (selectedSociete && selectedSociete !== 'all') { + congesConditions.push('so.Nom = ?'); + params.push(selectedSociete); + } + + if (selectedCampus && selectedCampus !== 'all') { + congesConditions.push('c.Nom = ?'); + params.push(selectedCampus); } if (selectedService && selectedService !== 'all') { - whereConditions.push(`s.Nom = ?`); - whereParams.push(selectedService); + congesConditions.push('s.Nom = ?'); + params.push(selectedService); + } + + if (congesConditions.length > 0) { + queryConditions += ` AND ${congesConditions.join(' AND ')}`; } query = ` - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, - CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, - CONCAT( - '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' - ), - ']' - ) AS detailsconges, - MAX(tc.CouleurHex) AS color, - dc.Statut AS statut, - s.Nom AS servicenom, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - dc.NombreJours AS nombrejoursouvres - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ${whereConditions.join(' AND ')} - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours - ORDER BY so.Nom, c.Nom, dc.DateDebut ASC - `; - params = whereParams; + SELECT + DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, + DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, + CONCAT(ca.prenom, ' ', ca.nom) AS employeename, + GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, + CONCAT( + '[', + GROUP_CONCAT( + JSON_OBJECT( + 'type', tc.Nom, + 'jours', dct.NombreJours, + 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') + ) + SEPARATOR ',' + ), + ']' + ) AS detailsconges, + MAX(tc.CouleurHex) AS color, + dc.Statut AS statut, + s.Nom AS servicenom, + c.Nom AS campusnom, + so.Nom AS societenom, + dc.NombreJours AS nombrejoursouvres + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON ca.CampusId = c.Id + JOIN Societe so ON ca.SocieteId = so.Id + ${queryConditions} + GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours + ORDER BY s.Nom, dc.DateDebut ASC + `; + + console.log(`🔍 Query WHERE final:`, queryConditions); + console.log(`🔍 Params:`, params); } - // ======================================== - // CAS 3: COLLABORATEUR - // ======================================== - else if (role === 'collaborateur') { - console.log("CAS 3: Collaborateur"); - - const serviceNom = userInfo.serviceNom || 'Non défini'; - const accesTransversal = getUserAccesTransversal(userEmail); - const isServiceMultiCampus = [ - "Administratif & Financier", - "IT" - ].includes(serviceNom); - - if (accesTransversal) { - console.log(`🌐 Accès transversal détecté:`, accesTransversal); - - if (accesTransversal.typeAcces === 'service_multi_campus') { - filters.societes = []; - filters.campus = []; - filters.services = []; - - const [employeesList] = await conn.query(` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE s.Nom = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY c.Nom, ca.prenom, ca.nom - `, [accesTransversal.serviceNom]); - - filters.employees = employeesList.map(emp => ({ - name: emp.fullname, - campus: emp.campusnom, - societe: emp.societenom, - service: emp.servicenom - })); - - query = ` - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, - CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, - CONCAT( - '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' - ), - ']' - ) AS detailsconges, - MAX(tc.CouleurHex) AS color, - dc.Statut AS statut, - s.Nom AS servicenom, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - dc.NombreJours AS nombrejoursouvres - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE s.Nom = ? - AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours - ORDER BY c.Nom, dc.DateDebut ASC - `; - params = [accesTransversal.serviceNom]; - - } else if (accesTransversal.typeAcces === 'service_multi_campus_avec_vue_complete') { - filters.societes = []; - filters.campus = []; - filters.services = []; - - const [employeesList] = await conn.query(` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE s.Nom = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY so.Nom, c.Nom, ca.prenom, ca.nom - `, [accesTransversal.serviceNom]); - - filters.employees = employeesList.map(emp => ({ - name: emp.fullname, - campus: emp.campusnom, - societe: emp.societenom, - service: emp.servicenom - })); - - query = ` - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, - CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, - CONCAT( - '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' - ), - ']' - ) AS detailsconges, - MAX(tc.CouleurHex) AS color, - dc.Statut AS statut, - s.Nom AS servicenom, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - dc.NombreJours AS nombrejoursouvres - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE s.Nom = ? - AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours - ORDER BY so.Nom, c.Nom, dc.DateDebut ASC - `; - params = [accesTransversal.serviceNom]; - } - - } else if (isServiceMultiCampus) { - filters.societes = []; - filters.campus = []; - filters.services = []; - - const [employeesList] = await conn.query(` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE s.Nom = ? - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY c.Nom, ca.prenom, ca.nom - `, [serviceNom]); - - filters.employees = employeesList.map(emp => ({ - name: emp.fullname, - campus: emp.campusnom, - societe: emp.societenom, - service: emp.servicenom - })); - - query = ` - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, - CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, - CONCAT( - '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' - ), - ']' - ) AS detailsconges, - MAX(tc.CouleurHex) AS color, - dc.Statut AS statut, - s.Nom AS servicenom, - COALESCE(c.Nom, 'Multi-campus') AS campusnom, - so.Nom AS societenom, - dc.NombreJours AS nombrejoursouvres - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE s.Nom = ? - AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours - ORDER BY c.Nom, dc.DateDebut ASC - `; - params = [serviceNom]; - } else { - filters.societes = []; - filters.campus = []; - filters.services = []; - - const [employeesList] = await conn.query(` - SELECT DISTINCT - CONCAT(ca.prenom, ' ', ca.nom) AS fullname, - c.Nom AS campusnom, - so.Nom AS societenom, - s.Nom AS servicenom - FROM CollaborateurAD ca - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.ServiceId = ? - AND (ca.CampusId = ? OR ca.CampusId IS NULL) - AND (ca.actif = 1 OR ca.actif IS NULL) - ORDER BY ca.prenom, ca.nom - `, [serviceId, campusId]); - - filters.employees = employeesList.map(emp => ({ - name: emp.fullname, - campus: emp.campusnom, - societe: emp.societenom, - service: emp.servicenom - })); - - query = ` - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') AS startdate, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') AS enddate, - CONCAT(ca.prenom, ' ', ca.nom) AS employeename, - GROUP_CONCAT(DISTINCT tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS type, - CONCAT( - '[', - GROUP_CONCAT( - JSON_OBJECT( - 'type', tc.Nom, - 'jours', dct.NombreJours, - 'periode', COALESCE(dct.PeriodeJournee, 'Journée entière') - ) - SEPARATOR ',' - ), - ']' - ) AS detailsconges, - MAX(tc.CouleurHex) AS color, - dc.Statut AS statut, - s.Nom AS servicenom, - c.Nom AS campusnom, - so.Nom AS societenom, - dc.NombreJours AS nombrejoursouvres - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - LEFT JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - LEFT JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - JOIN Services s ON ca.ServiceId = s.Id - LEFT JOIN Campus c ON ca.CampusId = c.Id - JOIN Societe so ON ca.SocieteId = so.Id - WHERE ca.ServiceId = ? - AND (ca.CampusId = ? OR ca.CampusId IS NULL) - AND dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente') - GROUP BY dc.Id, dc.DateDebut, dc.DateFin, ca.prenom, ca.nom, dc.Statut, s.Nom, c.Nom, so.Nom, dc.NombreJours - ORDER BY dc.DateDebut ASC - `; - params = [serviceId, campusId]; - } - } + // ======================================== // CAS 4: AUTRES RÔLES @@ -4590,40 +5586,53 @@ app.get('/getTeamLeaves', async (req, res) => { -app.post('/initial-sync', async (req, res) => { - try { - const accessToken = await getGraphToken(); - if (!accessToken) return res.json({ success: false, message: 'Impossible obtenir token' }); +// ================================================ +// ROUTE DE SYNCHRONISATION INITIALE (CORRIGÉE) +// ================================================ + +// ✅ APRÈS - Version CORRIGÉE +// ✅ VERSION COMPLÈTE ET CORRIGÉE +app.post('/api/initial-sync', async (req, res) => { + try { + // 1. Obtenir le token Admin + const accessToken = await getGraphToken(); + if (!accessToken) return res.json({ success: false, message: 'Impossible obtenir token Microsoft Graph' }); + + // ============================================================================= + // SCÉNARIO 1 : Synchronisation unitaire (Un seul utilisateur spécifique) + // ============================================================================= if (req.body.userPrincipalName || req.body.mail) { const userEmail = req.body.mail || req.body.userPrincipalName; const entraUserId = req.body.id; - console.log(`🔄 Synchronisation utilisateur: ${userEmail}`); + console.log(`🔄 Synchronisation utilisateur unique : ${userEmail}`); - // ⭐ Insertion avec SocieteId (ajuster selon votre logique) + // Insertion ou Mise à jour await pool.query(` INSERT INTO CollaborateurAD - (entraUserId, prenom, nom, email, service, description, role, SocieteId) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + (entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW(), '37h') ON DUPLICATE KEY UPDATE - prenom=?, nom=?, email=?, service=?, description=? + prenom=VALUES(prenom), + nom=VALUES(nom), + email=VALUES(email), + service=VALUES(service), + description=VALUES(description), + Actif=1, -- On réactive si l'utilisateur revient + entraUserId=VALUES(entraUserId) `, [ entraUserId, - req.body.givenName, - req.body.surname, + req.body.givenName || 'Prénom', + req.body.surname || 'Nom', userEmail, - req.body.department, - req.body.jobTitle, + req.body.department || '', + req.body.jobTitle || null, 'Collaborateur', - null, // ⭐ À ajuster selon votre logique métier - req.body.givenName, - req.body.surname, - userEmail, - req.body.department, - req.body.jobTitle + 1 // SocieteId par défaut (ex: ENSUP) ]); + // Récupération des données fraîches pour renvoyer au front const [userRows] = await pool.query(` SELECT ca.id as localUserId, @@ -4646,15 +5655,11 @@ app.post('/initial-sync', async (req, res) => { `, [userEmail]); if (userRows.length === 0) { - return res.json({ - success: false, - message: 'Utilisateur synchronisé mais introuvable en BDD' - }); + return res.json({ success: false, message: 'Erreur : Utilisateur synchronisé mais introuvable en base.' }); } const userData = userRows[0]; - - console.log(`✅ Utilisateur synchronisé:`, userData); + console.log(`✅ Utilisateur synchronisé avec succès : ${userData.email}`); return res.json({ success: true, @@ -4665,79 +5670,118 @@ app.post('/initial-sync', async (req, res) => { typeContrat: userData.typeContrat, dateEntree: userData.dateEntree, societeId: userData.SocieteId, - societeNom: userData.societe_nom, user: userData }); } - // Full sync - console.log('🔄 Full sync de tous les membres du groupe...'); + // ============================================================================= + // SCÉNARIO 2 : Full Sync (Tous les membres du groupe Azure) + // ============================================================================= + console.log('🔄 Démarrage Full Sync des membres du groupe...'); + // A. Récupérer le nom du groupe (pour info) const groupResponse = await axios.get( `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, { headers: { Authorization: `Bearer ${accessToken}` } } ); - const group = groupResponse.data; + const groupName = groupResponse.data.displayName; - const membersResponse = await axios.get( - `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle`, - { headers: { Authorization: `Bearer ${accessToken}` } } - ); - const members = membersResponse.data.value; + // B. Récupérer TOUS les membres avec pagination (boucle while) + let allAzureMembers = []; + let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle&$top=999`; + while (nextLink) { + const membersResponse = await axios.get(nextLink, { headers: { Authorization: `Bearer ${accessToken}` } }); + allAzureMembers = allAzureMembers.concat(membersResponse.data.value); + nextLink = membersResponse.data['@odata.nextLink']; // Lien vers la page suivante (si existe) + } + + console.log(`📋 ${allAzureMembers.length} utilisateurs trouvés dans le groupe Azure "${groupName}".`); + + const azureEmails = new Set(); // Liste blanche des emails actifs let usersInserted = 0; - for (const m of members) { - if (!m.mail) continue; + + // C. Traitement de chaque membre Azure + for (const m of allAzureMembers) { + if (!m.mail) continue; // Ignorer ceux sans email + + azureEmails.add(m.mail.toLowerCase()); await pool.query(` INSERT INTO CollaborateurAD ( - entraUserId, prenom, nom, email, service, description, role, SocieteId - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + entraUserId, prenom, nom, email, service, description, role, SocieteId, Actif, DateEntree, TypeContrat + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW(), '37h') ON DUPLICATE KEY UPDATE - prenom=?, nom=?, email=?, service=?, description=? + prenom=VALUES(prenom), + nom=VALUES(nom), + service=VALUES(service), + entraUserId=VALUES(entraUserId), + Actif = 1 -- On s'assure qu'il est actif `, [ m.id, - m.givenName || '', - m.surname || '', + m.givenName || 'Prénom', + m.surname || 'Nom', m.mail, m.department || '', m.jobTitle || null, 'Collaborateur', - null, // ⭐ À ajuster selon votre logique métier - m.givenName, - m.surname, - m.mail, - m.department, - m.jobTitle + 1, // SocieteId par défaut ]); usersInserted++; } - console.log(`✅ Full sync terminée: ${usersInserted} utilisateurs`); + // D. Désactivation des fantômes (Ceux en base locale mais ABSENTS d'Azure) + const activeEmailsArray = Array.from(azureEmails); + let deactivatedCount = 0; + + if (activeEmailsArray.length > 0) { + // Création dynamique des placeholders (?, ?, ?) + const placeholders = activeEmailsArray.map(() => '?').join(','); + + // On désactive tous ceux qui NE SONT PAS dans la liste activeEmailsArray + const [resultDeactivate] = await pool.query(` + UPDATE CollaborateurAD + SET Actif = 0 + WHERE Email IS NOT NULL + AND LOWER(Email) NOT IN (${placeholders}) + AND Actif = 1 -- On ne modifie que ceux qui étaient actifs + `, activeEmailsArray); + + deactivatedCount = resultDeactivate.affectedRows; + } + + console.log(`✅ Full sync terminée avec succès.`); + console.log(` - ${usersInserted} utilisateurs synchronisés/actifs`); + console.log(` - ${deactivatedCount} utilisateurs désactivés (partis)`); res.json({ success: true, - message: 'Full synchronisation terminée', - groupe_sync: group.displayName, - users_sync: usersInserted + message: 'Synchronisation globale terminée', + groupe_sync: groupName, + stats: { + total_azure: allAzureMembers.length, + processed: usersInserted, + deactivated: deactivatedCount + } }); } catch (error) { - console.error('❌ Erreur sync:', error); + console.error('❌ Erreur critique lors de la synchronisation:', error); res.status(500).json({ success: false, - message: 'Erreur sync', + message: 'Erreur lors de la synchronisation', error: error.message }); } }); + // ======================================== // NOUVELLES ROUTES ADMINISTRATION RTT // ======================================== -app.get('/getAllCollaborateurs', async (req, res) => { +app.get('/api/getAllCollaborateurs', async (req, res) => { try { const [collaborateurs] = await pool.query(` SELECT @@ -4774,7 +5818,7 @@ app.get('/getAllCollaborateurs', async (req, res) => { } }); -app.post('/updateTypeContrat', async (req, res) => { +app.post('/api/updateTypeContrat', async (req, res) => { try { const { collaborateur_id, type_contrat } = req.body; @@ -4826,7 +5870,7 @@ app.post('/updateTypeContrat', async (req, res) => { } }); -app.get('/getConfigurationRTT', async (req, res) => { +app.get('/api/getConfigurationRTT', async (req, res) => { try { const annee = parseInt(req.query.annee || new Date().getFullYear()); const [configs] = await pool.query( @@ -4842,7 +5886,7 @@ app.get('/getConfigurationRTT', async (req, res) => { } }); -app.post('/updateConfigurationRTT', async (req, res) => { +app.post('/api/updateConfigurationRTT', async (req, res) => { try { const { annee, typeContrat, joursAnnuels } = req.body; if (!annee || !typeContrat || !joursAnnuels) { @@ -4865,11 +5909,11 @@ app.post('/updateConfigurationRTT', async (req, res) => { } }); -app.post('/updateRequest', async (req, res) => { - const conn = await pool.getConnection(); - +app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, res) => { + let connection; try { - await conn.beginTransaction(); + console.log('📥 Body reçu:', req.body); + console.log('📎 Fichiers reçus:', req.files); const { requestId, @@ -4881,311 +5925,446 @@ app.post('/updateRequest', async (req, res) => { userId, userEmail, userName, - accessToken, - repartition // ⭐ NOUVEAU - Ajout de la répartition + accessToken } = req.body; - console.log('\n📝 === MODIFICATION DEMANDE ==='); - console.log('Demande ID:', requestId); - console.log('Utilisateur:', userName); - console.log('Nouvelle répartition:', repartition); + // Validation + if (!requestId || !leaveType || !startDate || !endDate || !businessDays || !userId) { + return res.status(400).json({ + success: false, + message: '❌ Données manquantes' + }); + } - // 1. Vérifier que la demande existe et est "En attente" - const [existingRequest] = await conn.query( + connection = await pool.getConnection(); + await connection.beginTransaction(); + + console.log('\n✏️ === MODIFICATION DEMANDE ==='); + console.log(`Demande ID: ${requestId}, User ID: ${userId}`); + + // 1️⃣ RÉCUPÉRER LA DEMANDE ORIGINALE + const [originalRequest] = await connection.query( 'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?', [requestId, userId] ); - if (existingRequest.length === 0) { - await conn.rollback(); - conn.release(); + if (originalRequest.length === 0) { + await connection.rollback(); return res.status(404).json({ success: false, - message: 'Demande introuvable' + message: '❌ Demande introuvable ou non autorisée' }); } - const request = existingRequest[0]; + const original = originalRequest[0]; + const oldStatus = original.Statut; - if (request.Statut !== 'En attente') { - await conn.rollback(); - conn.release(); - return res.status(400).json({ - success: false, - message: 'Vous ne pouvez modifier que les demandes en attente' - }); + console.log(`📋 Demande originale: Statut=${oldStatus}`); + + // 2️⃣ REMBOURSER L'ANCIENNE DEMANDE (via DeductionDetails) + let restorationStats = { count: 0, details: [] }; + + if (oldStatus !== 'Refusée' && oldStatus !== 'Annulée' && original.TypeCongeId !== 3) { + console.log(`🔄 Remboursement de l'ancienne demande...`); + + const [oldDeductions] = await connection.query( + 'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?', + [requestId] + ); + + if (oldDeductions.length > 0) { + for (const d of oldDeductions) { + const [compteur] = await connection.query( + 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, d.TypeCongeId, d.Annee] + ); + + if (compteur.length > 0) { + const newSolde = parseFloat(compteur[0].Solde) + parseFloat(d.JoursUtilises); + await connection.query( + 'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?', + [newSolde, compteur[0].Id] + ); + restorationStats.count++; + restorationStats.details.push(`${d.JoursUtilises}j rendus (Type ${d.TypeCongeId}, Année ${d.Annee})`); + console.log(` ✅ Remboursé ${d.JoursUtilises}j au compteur TypeId=${d.TypeCongeId}, Annee=${d.Annee}`); + } + } + + // Supprimer les anciennes déductions + await connection.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]); + console.log(` 🧹 ${oldDeductions.length} déduction(s) supprimée(s)`); + } } - // 2. ⭐ RESTAURER LES ANCIENS COMPTEURS - console.log('\n🔄 ÉTAPE 1: Restauration des anciens compteurs...'); - try { - const restoration = await restoreLeaveBalance(conn, requestId, userId); - console.log('✅ Compteurs restaurés:', restoration); - } catch (restoreError) { - console.error('❌ Erreur restauration:', restoreError); - await conn.rollback(); - conn.release(); - return res.status(500).json({ - success: false, - message: 'Erreur lors de la restauration des compteurs', - error: restoreError.message - }); - } + // 3️⃣ METTRE À JOUR LA DEMANDE + console.log('📝 Mise à jour de la demande...'); - // 3. ⭐ SUPPRIMER LES ANCIENNES DÉDUCTIONS - console.log('\n🗑️ ÉTAPE 2: Suppression des anciennes déductions...'); - await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]); - await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]); + // Si elle était validée, on la repasse en "En attente" + const newStatus = (oldStatus === 'Validée' || oldStatus === 'Validé') ? 'En attente' : oldStatus; - // 4. METTRE À JOUR LA DEMANDE - console.log('\n📝 ÉTAPE 3: Mise à jour de la demande...'); - await conn.query( + await connection.query( `UPDATE DemandeConge - SET DateDebut = ?, + SET TypeCongeId = ?, + DateDebut = ?, DateFin = ?, Commentaire = ?, - NombreJours = ? + NombreJours = ?, + Statut = ?, + DateValidation = NOW(), + CommentaireValidation = CONCAT( + COALESCE(CommentaireValidation, ''), + '\n[Modifiée le ', + DATE_FORMAT(NOW(), '%d/%m/%Y à %H:%i'), + ']' + ) WHERE Id = ?`, - [startDate, endDate, reason || null, businessDays, requestId] + [leaveType, startDate, endDate, reason || '', businessDays, newStatus, requestId] ); - // 5. ⭐ SAUVEGARDER LA NOUVELLE RÉPARTITION - console.log('\n📊 ÉTAPE 4: Sauvegarde de la nouvelle répartition...'); - if (repartition && repartition.length > 0) { - for (const rep of repartition) { - const code = rep.TypeConge; - const name = code === 'CP' ? 'Congé payé' : - code === 'RTT' ? 'RTT' : - code === 'ABS' ? 'Congé maladie' : - code === 'Formation' ? 'Formation' : - code === 'Récup' ? 'Récupération' : code; + // 4️⃣ CALCULER ET APPLIQUER LA NOUVELLE RÉPARTITION + let newRepartition = []; - const [typeRow] = await conn.query( - 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', - [name] + if (parseInt(leaveType) !== 3) { // Pas pour Arrêt maladie + console.log('📊 Calcul de la nouvelle répartition...'); + + const currentYear = new Date().getFullYear(); + const previousYear = currentYear - 1; + let joursRestants = parseFloat(businessDays); + + // A. CONGÉ PAYÉ : N-1 → N → Anticipé + if (parseInt(leaveType) === 1) { + // Essayer N-1 + const [compteurN1] = await connection.query( + 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, leaveType, previousYear] ); - if (typeRow.length > 0) { - await conn.query( - `INSERT INTO DemandeCongeType - (DemandeCongeId, TypeCongeId, NombreJours, PeriodeJournee) - VALUES (?, ?, ?, ?)`, - [ - requestId, - typeRow[0].Id, - rep.NombreJours, - rep.PeriodeJournee || 'Journée entière' - ] + if (compteurN1.length > 0 && compteurN1[0].Solde > 0 && joursRestants > 0) { + const disponibleN1 = parseFloat(compteurN1[0].Solde); + const aPrendreN1 = Math.min(disponibleN1, joursRestants); + + await connection.query( + 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?', + [aPrendreN1, compteurN1[0].Id] ); - console.log(` ✓ ${name}: ${rep.NombreJours}j`); - } - } - } - // 6. ⭐ DÉDUIRE LES NOUVEAUX COMPTEURS - console.log('\n📉 ÉTAPE 5: Déduction des nouveaux compteurs...'); - const currentYear = new Date().getFullYear(); + await connection.query(` + INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction) + VALUES (?, ?, ?, ?, ?) + `, [requestId, leaveType, previousYear, aPrendreN1, 'Année N-1']); - for (const rep of repartition) { - if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') { - console.log(` ⏩ ${rep.TypeConge} ignoré (pas de déduction)`); - continue; - } - - // Récup: ACCUMULATION - if (rep.TypeConge === 'Récup') { - const [recupType] = await conn.query( - 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', - ['Récupération'] - ); - - if (recupType.length > 0) { - const recupJours = rep.NombreJours; - const [compteurExisting] = await conn.query(` - SELECT Id, Total, Solde FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, recupType[0].Id, currentYear]); - - if (compteurExisting.length > 0) { - await conn.query(` - UPDATE CompteurConges - SET Total = Total + ?, - Solde = Solde + ?, - DerniereMiseAJour = NOW() - WHERE Id = ? - `, [recupJours, recupJours, compteurExisting[0].Id]); - } else { - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, NOW()) - `, [userId, recupType[0].Id, currentYear, recupJours, recupJours]); - } - - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Accum Récup', ?) - `, [requestId, recupType[0].Id, currentYear, recupJours]); - - console.log(` ✓ Récup: +${recupJours}j accumulés`); - } - continue; - } - - // CP et RTT: DÉDUCTION - const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT'; - const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); - - if (typeRow.length > 0) { - const result = await deductLeaveBalanceWithTracking( - conn, - userId, - typeRow[0].Id, - rep.NombreJours, - requestId - ); - - console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`); - if (result.details && result.details.length > 0) { - result.details.forEach(d => { - console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`); + newRepartition.push({ + typeCongeId: leaveType, + annee: previousYear, + jours: aPrendreN1, + typeDeduction: 'Année N-1' }); + + joursRestants -= aPrendreN1; + console.log(` ✅ Déduit ${aPrendreN1}j de N-1`); + } + + // Essayer N + if (joursRestants > 0) { + const [compteurN] = await connection.query( + 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, leaveType, currentYear] + ); + + if (compteurN.length > 0) { + const disponibleN = parseFloat(compteurN[0].Solde); + const aPrendreN = Math.min(disponibleN, joursRestants); + + await connection.query( + 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?', + [aPrendreN, compteurN[0].Id] + ); + + await connection.query(` + INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction) + VALUES (?, ?, ?, ?, ?) + `, [requestId, leaveType, currentYear, aPrendreN, 'Année N']); + + newRepartition.push({ + typeCongeId: leaveType, + annee: currentYear, + jours: aPrendreN, + typeDeduction: 'Année N' + }); + + joursRestants -= aPrendreN; + console.log(` ✅ Déduit ${aPrendreN}j de N`); + } + } + + // Anticipé (si encore des jours restants) + if (joursRestants > 0) { + const [compteurN] = await connection.query( + 'SELECT Id FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, leaveType, currentYear] + ); + + if (compteurN.length > 0) { + await connection.query( + 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?', + [joursRestants, compteurN[0].Id] + ); + + await connection.query(` + INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction, IsAnticipe) + VALUES (?, ?, ?, ?, ?, 1) + `, [requestId, leaveType, currentYear, joursRestants, 'N Anticip']); + + newRepartition.push({ + typeCongeId: leaveType, + annee: currentYear, + jours: joursRestants, + typeDeduction: 'N Anticip' + }); + + console.log(` ⚠️ Déduit ${joursRestants}j en ANTICIPÉ`); + } + } + } + + // B. RTT : Uniquement année N + else if (parseInt(leaveType) === 2) { + const [compteurRTT] = await connection.query( + 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, leaveType, currentYear] + ); + + if (compteurRTT.length > 0) { + const disponible = parseFloat(compteurRTT[0].Solde); + + if (disponible < joursRestants) { + throw new Error(`Solde RTT insuffisant: ${disponible.toFixed(2)}j disponibles, ${joursRestants}j demandés`); + } + + await connection.query( + 'UPDATE CompteurConges SET Solde = Solde - ?, DerniereMiseAJour = NOW() WHERE Id = ?', + [joursRestants, compteurRTT[0].Id] + ); + + await connection.query(` + INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, JoursUtilises, TypeDeduction) + VALUES (?, ?, ?, ?, ?) + `, [requestId, leaveType, currentYear, joursRestants, 'Année N']); + + newRepartition.push({ + typeCongeId: leaveType, + annee: currentYear, + jours: joursRestants, + typeDeduction: 'Année N' + }); + + console.log(` ✅ Déduit ${joursRestants}j RTT de N`); } } } - // 7. Récupérer les infos pour l'email - const [hierarchie] = await conn.query( - `SELECT h.SuperieurId, m.email as managerEmail, m.prenom as managerPrenom, m.nom as managerNom - FROM HierarchieValidationAD h - LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id - WHERE h.CollaborateurId = ?`, - [userId] - ); + await connection.commit(); + console.log(`✅ Demande ${requestId} modifiée avec succès`); - const managerEmail = hierarchie[0]?.managerEmail; - const managerName = hierarchie[0] ? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}` : 'Manager'; + // 5️⃣ ENVOI DES EMAILS (Asynchrone, ne bloque pas la réponse) + const graphToken = await getGraphToken(); - await conn.commit(); - console.log('\n✅ Modification terminée avec succès\n'); + if (graphToken) { + const [managerInfo] = await connection.query( + `SELECT m.Email, m.Prenom, m.Nom + FROM CollaborateurAD c + JOIN HierarchieValidationAD h ON c.id = h.CollaborateurId + JOIN CollaborateurAD m ON h.SuperieurId = m.id + WHERE c.id = ?`, + [userId] + ); - // 8. Envoyer les emails (après commit) - if (managerEmail && accessToken) { - try { - const newStartDate = new Date(startDate).toLocaleDateString('fr-FR'); - const newEndDate = new Date(endDate).toLocaleDateString('fr-FR'); - const typesConges = repartition.map(r => `${r.TypeConge}: ${r.NombreJours}j`).join(' | '); + if (managerInfo.length > 0) { + const manager = managerInfo[0]; - const emailBody = { - message: { - subject: `🔄 Modification de demande de congé - ${userName}`, - body: { - contentType: "HTML", - content: ` -
-
-

🔄 Modification de demande

-
- -
-

Bonjour ${managerName},

- -

- ${userName} a modifié sa demande de congé. -

+ // Email au manager + sendMailGraph( + graphToken, + 'gtanoreply@ensup.eu', + manager.Email, + '🔄 Modification de demande de congé', + ` +
+
+

🔄 Modification de demande

+
+
+

Bonjour ${manager.Prenom},

+

${userName} a modifié sa demande de congé :

+
+ + + + + + + + + + + + + +
Type :${getLeaveTypeName(leaveType)}
Dates :du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}
Durée :${businessDays} jour(s)
+
+ ${restorationStats.count > 0 ? ` +
+

+ ✅ Les compteurs ont été automatiquement recalculés (${restorationStats.count} opération(s)). +

+
+ ` : ''} +

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

+
+

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

+
+
+
+ ` + ).catch(err => console.error('❌ Erreur email manager:', err)); -
-

✨ Nouvelles informations

- - - - - - - - - - - - - - - - - - ${reason ? ` - - - ` : ''} -
Type :${typesConges}
Du :${newStartDate}
Au :${newEndDate}
Jours :${businessDays} jour(s)
Motif :${reason}
-
- -

- Cette demande est toujours en attente de validation. -

-
-
- ` - }, - toRecipients: [ - { - emailAddress: { - address: managerEmail - } - } - ] - }, - saveToSentItems: "false" - }; - - await axios.post( - 'https://graph.microsoft.com/v1.0/me/sendMail', - emailBody, - { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json' - } - } - ); - - console.log('✅ Email de modification envoyé au manager'); - - } catch (emailError) { - console.error('❌ Erreur envoi email manager:', emailError); + // Email de confirmation au collaborateur + sendMailGraph( + graphToken, + 'gtanoreply@ensup.eu', + userEmail, + '✅ Confirmation de modification', + ` +
+
+

✅ Demande modifiée

+
+
+

Bonjour ${userName.split(' ')[0]},

+

Votre demande de congé a bien été modifiée :

+
+ + + + + + + + + + + + + +
Type :${getLeaveTypeName(leaveType)}
Dates :du ${formatDateFR(startDate)} au ${formatDateFR(endDate)}
Durée :${businessDays} jour(s)
+
+

Elle est maintenant en attente de validation.

+
+

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

+
+
+
+ ` + ).catch(err => console.error('❌ Erreur email collaborateur:', err)); } } - conn.release(); - res.json({ success: true, - message: 'Demande modifiée avec succès' + message: '✅ Demande modifiée avec succès', + newStatus: newStatus, + restoration: restorationStats, + repartition: newRepartition }); } catch (error) { - await conn.rollback(); - if (conn) conn.release(); + if (connection) { + await connection.rollback(); + } console.error('❌ Erreur updateRequest:', error); res.status(500).json({ success: false, - message: 'Erreur serveur lors de la modification', - error: error.message + message: error.message || 'Erreur lors de la modification' }); + } finally { + if (connection) { + connection.release(); + } } }); + + +// ⭐ Fonction helper pour calculer la répartition CP +function calculateCPRepartition(joursNecessaires, soldeN1, soldeN) { + const repartition = []; + let reste = joursNecessaires; + + // D'abord utiliser N-1 + if (reste > 0 && soldeN1 > 0) { + const joursN1 = Math.min(reste, soldeN1); + repartition.push({ + type: 'CP', + annee: 'N-1', + jours: joursN1 + }); + reste -= joursN1; + } + + // Puis utiliser N + if (reste > 0 && soldeN > 0) { + const joursN = Math.min(reste, soldeN); + repartition.push({ + type: 'CP', + annee: 'N', + jours: joursN + }); + reste -= joursN; + } + + return repartition; +} + +// ⭐ Fonction helper pour obtenir le champ de compteur +function getCounterField(type, annee) { + if (type === 'CP' && annee === 'N-1') return 'SoldeCP_N1'; + if (type === 'CP' && annee === 'N') return 'SoldeCP_N'; + if (type === 'RTT' && annee === 'N') return 'SoldeRTT_N'; + return null; +} + +// ⭐ Fonction helper pour le nom du type de congé +function getLeaveTypeName(typeId) { + const types = { + 1: 'Congé payé', + 2: 'RTT', + 3: 'Arrêt maladie', + 4: 'Formation', + 5: 'Récupération' + }; + return types[typeId] || 'Inconnu'; +} + +// ⭐ Fonction helper pour formater les dates +function formatDateFR(dateStr) { + const date = new Date(dateStr); + return date.toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); +} /** * Route pour SUPPRIMER une demande de congé * POST /deleteRequest */ -app.post('/deleteRequest', async (req, res) => { +app.post('/api/deleteRequest', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); - const { requestId, userId, userEmail, userName, accessToken } = req.body; + const { requestId, userId, userEmail, userName } = req.body; if (!requestId || !userId) { await conn.rollback(); @@ -5196,15 +6375,15 @@ app.post('/deleteRequest', async (req, res) => { }); } - console.log('\n🗑️ === SUPPRESSION DEMANDE ==='); - console.log('Demande ID:', requestId); - console.log('User ID:', userId); + console.log('\n🗑️ === ANNULATION DEMANDE ==='); + console.log(`Demande ID: ${requestId}, User ID: ${userId}`); - // 1. Vérifier que la demande existe et appartient à l'utilisateur + // 1️⃣ Vérifier que la demande existe const [existingRequest] = await conn.query( - `SELECT d.*, tc.Nom as TypeConge + `SELECT d.*, + DATE_FORMAT(d.DateDebut, '%Y-%m-%d') as DateDebut, + DATE_FORMAT(d.DateFin, '%Y-%m-%d') as DateFin FROM DemandeConge d - LEFT JOIN TypeConge tc ON d.TypeCongeId = tc.Id WHERE d.Id = ? AND d.CollaborateurADId = ?`, [requestId, userId] ); @@ -5220,141 +6399,299 @@ app.post('/deleteRequest', async (req, res) => { const request = existingRequest[0]; const requestStatus = request.Statut; + const dateDebut = new Date(request.DateDebut); + const aujourdhui = new Date(); + aujourdhui.setHours(0, 0, 0, 0); + dateDebut.setHours(0, 0, 0, 0); - console.log(`📋 Demande trouvée: ID=${requestId}, Statut=${requestStatus}`); + console.log(`📋 Demande: Statut=${requestStatus}, Date début=${dateDebut.toLocaleDateString('fr-FR')}`); - // 2. ⭐ RESTAURER LES COMPTEURS (si nécessaire) - if (['En attente', 'Valide', 'Validé', 'Valid'].includes(requestStatus)) { - console.log(`\n✅ Restauration des compteurs (Statut: ${requestStatus})`); - try { - const restoration = await restoreLeaveBalance(conn, requestId, userId); - if (restoration.success) { - console.log('✅ Compteurs restaurés:', restoration.restorations.length, 'opérations'); + // ❌ BLOQUER SI DATE DÉJÀ PASSÉE + if (dateDebut <= aujourdhui && requestStatus === 'Validée') { + await conn.rollback(); + conn.release(); + return res.status(400).json({ + success: false, + message: '❌ Impossible d\'annuler : la date de début est déjà passée ou c\'est aujourd\'hui', + dateDebut: formatDateWithoutUTC(dateDebut) + }); + } + + // 2️⃣ RÉCUPÉRER LA RÉPARTITION (pour l'email) + const [repartition] = await conn.query(` + SELECT dct.*, tc.Nom as TypeNom + FROM DemandeCongeType dct + JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + WHERE dct.DemandeCongeId = ? + ORDER BY tc.Nom + `, [requestId]); + + // 3️⃣ RESTAURER LES COMPTEURS via DeductionDetails + let restorationStats = { count: 0, details: [] }; + + if (requestStatus !== 'Refusée' && requestStatus !== 'Annulée') { + console.log(`🔄 Restauration des compteurs...`); + + const [deductions] = await conn.query( + 'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?', + [requestId] + ); + + if (deductions.length > 0) { + for (const d of deductions) { + const [compteur] = await conn.query( + 'SELECT Id, Solde FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', + [userId, d.TypeCongeId, d.Annee] + ); + + if (compteur.length > 0) { + const c = compteur[0]; + const newSolde = parseFloat(c.Solde) + parseFloat(d.JoursUtilises); + + await conn.query( + 'UPDATE CompteurConges SET Solde = ?, DerniereMiseAJour = NOW() WHERE Id = ?', + [newSolde, c.Id] + ); + + restorationStats.count++; + restorationStats.details.push({ + typeCongeId: d.TypeCongeId, + annee: d.Annee, + joursRendus: d.JoursUtilises + }); + + console.log(` ✅ Remboursé ${d.JoursUtilises}j au compteur TypeId=${d.TypeCongeId} Année=${d.Annee}`); + } else { + console.warn(`⚠️ Compteur introuvable (Type ${d.TypeCongeId}, Année ${d.Annee})`); + } } - } catch (restoreError) { - console.error('❌ Erreur restauration:', restoreError.message); - // Ne pas bloquer la suppression si la restauration échoue + + // Supprimer les déductions + await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]); + console.log(` 🧹 ${deductions.length} ligne(s) DeductionDetails supprimée(s)`); + } else { + console.log(' ℹ️ Aucune déduction à rembourser'); } } - // 3. Récupérer le manager pour l'email - const [hierarchie] = await conn.query( - `SELECT h.SuperieurId, m.email as managerEmail, - m.prenom as managerPrenom, m.nom as managerNom - FROM HierarchieValidationAD h - LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id - WHERE h.CollaborateurId = ?`, - [userId] + // 4️⃣ METTRE À JOUR LE STATUT + await conn.query( + `UPDATE DemandeConge + SET Statut = 'Annulée', + DateValidation = NOW(), + CommentaireValidation = CONCAT( + COALESCE(CommentaireValidation, ''), + '\n[Annulée par le collaborateur le ', + DATE_FORMAT(NOW(), '%d/%m/%Y à %H:%i'), + ']' + ) + WHERE Id = ?`, + [requestId] ); - const managerEmail = hierarchie[0]?.managerEmail; - const managerName = hierarchie[0] - ? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}` - : 'Manager'; - - // 4. ⭐ SUPPRIMER LES DÉPENDANCES - await conn.query('DELETE FROM HistoriqueActions WHERE DemandeCongeId = ?', [requestId]); - await conn.query('DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', [requestId]); - await conn.query('DELETE FROM DemandeCongeType WHERE DemandeCongeId = ?', [requestId]); - await conn.query('DELETE FROM DocumentsMedicaux WHERE DemandeCongeId = ?', [requestId]); - await conn.query('DELETE FROM Notifications WHERE DemandeCongeId = ?', [requestId]); - - // 5. Supprimer la demande - await conn.query('DELETE FROM DemandeConge WHERE Id = ?', [requestId]); - console.log(`✅ Demande ${requestId} supprimée définitivement`); + console.log(`✅ Demande ${requestId} marquée comme Annulée`); await conn.commit(); + conn.release(); - // 6. ⭐ ENVOYER EMAIL AU MANAGER - if (managerEmail && accessToken) { - try { - const emailBody = { - message: { - subject: `🗑️ Suppression de demande - ${userName}`, - body: { - contentType: "HTML", - content: ` -
-
-

🗑️ Suppression de demande

-
- -
-

Bonjour ${managerName},

- -

- ${userName} a supprimé sa demande de congé. -

+ // 5️⃣ ENVOI DES EMAILS + let emailsSent = { collaborateur: false, manager: false }; + const graphToken = await getGraphToken(); -
-

📋 Demande supprimée

- - - - - - - - - - - - - - - - - -
Type :${request.TypeConge || 'N/A'}
Période :${new Date(request.DateDebut).toLocaleDateString('fr-FR')} au ${new Date(request.DateFin).toLocaleDateString('fr-FR')}
Jours :${request.NombreJours} jour(s)
Statut initial :${requestStatus}
-
+ if (graphToken) { + const [collabInfo] = await conn.query( + 'SELECT email, prenom, nom FROM CollaborateurAD WHERE id = ?', + [userId] + ); -

- Les compteurs de congés ont été restaurés si nécessaire. -

-
+ const collabEmail = collabInfo.length > 0 ? collabInfo[0].email : userEmail; + const collabName = collabInfo.length > 0 + ? `${collabInfo[0].prenom} ${collabInfo[0].nom}` + : userName; + + const dateDebutFormatted = new Date(request.DateDebut).toLocaleDateString('fr-FR'); + const dateFinFormatted = new Date(request.DateFin).toLocaleDateString('fr-FR'); + const datesPeriode = dateDebutFormatted === dateFinFormatted + ? dateDebutFormatted + : `du ${dateDebutFormatted} au ${dateFinFormatted}`; + + const repartitionText = repartition.map(r => + ` + ${r.TypeNom} : + ${r.NombreJours}j ${r.PeriodeJournee !== 'Journée entière' ? `(${r.PeriodeJournee})` : ''} + ` + ).join(''); + + // 📧 EMAIL AU COLLABORATEUR + if (collabEmail) { + try { + const subjectCollab = '✅ Confirmation d\'annulation de votre demande'; + const bodyCollab = ` +
+
+

✅ Demande annulée

+
+ +
+

Bonjour ${collabName},

+ +

+ Votre demande de congé a bien été annulée. +

+ +
+

📋 Demande annulée

+ + + + + + + + + + + + + ${repartitionText} +
Période :${datesPeriode}
Durée totale :${request.NombreJours} jour(s)
+ Répartition : +
- ` - }, - toRecipients: [{ emailAddress: { address: managerEmail } }], - saveToSentItems: false - } - }; - await axios.post('https://graph.microsoft.com/v1.0/me/sendMail', emailBody, { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json' - } - }); + ${restorationStats.count > 0 ? ` +
+

+ ✅ Vos compteurs ont été restaurés
+ ${restorationStats.count} opération(s) de remboursement effectuée(s). +

+
+ ` : ''} - console.log('📧 Email de notification envoyé au manager'); - } catch (emailError) { - console.error('❌ Erreur email manager (non bloquant):', emailError.message); +
+

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

+
+
+
+ `; + + await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', collabEmail, subjectCollab, bodyCollab); + emailsSent.collaborateur = true; + console.log('✅ Email envoyé au collaborateur'); + } catch (emailError) { + console.error('❌ Erreur email collaborateur:', emailError.message); + } + } + + // 📧 EMAIL AU MANAGER + const [hierarchie] = await conn.query( + `SELECT h.SuperieurId, m.email as managerEmail, + m.prenom as managerPrenom, m.nom as managerNom + FROM HierarchieValidationAD h + LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id + WHERE h.CollaborateurId = ?`, + [userId] + ); + + const managerEmail = hierarchie[0]?.managerEmail; + const managerName = hierarchie[0] + ? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}` + : 'Manager'; + + if (managerEmail && requestStatus !== 'Refusée' && requestStatus !== 'Annulée') { + try { + const isValidated = requestStatus === 'Validée' || requestStatus === 'Validé'; + + const subjectManager = isValidated + ? `🗑️ Annulation de congé validé - ${collabName}` + : `🗑️ Annulation de demande - ${collabName}`; + + const bodyManager = ` +
+
+

🗑️ Annulation de ${isValidated ? 'congé' : 'demande'}

+
+ +
+

Bonjour ${managerName},

+ +

+ ${collabName} a annulé ${isValidated ? 'son congé validé' : 'sa demande de congé'}. +

+ +
+

📋 Demande annulée

+ + + + + + + + + + + + + + + + + ${repartitionText} +
Statut initial :${requestStatus}
Période :${datesPeriode}
Durée totale :${request.NombreJours} jour(s)
+ Répartition : +
+
+ + ${restorationStats.count > 0 ? ` +
+

+ ✅ Les compteurs ont été automatiquement restaurés (${restorationStats.count} opération(s)). +

+
+ ` : ''} + +
+

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

+
+
+
+ `; + + await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', managerEmail, subjectManager, bodyManager); + emailsSent.manager = true; + console.log('✅ Email envoyé au manager'); + } catch (emailError) { + console.error('❌ Erreur email manager:', emailError.message); + } } } - conn.release(); - res.json({ success: true, - message: 'Demande supprimée avec succès', - counterRestored: ['En attente', 'Valid', 'Validé', 'Valide'].includes(requestStatus) + message: 'Demande annulée avec succès', + restoration: restorationStats, + emailsSent: emailsSent }); } catch (error) { await conn.rollback(); - if (conn) conn.release(); console.error('❌ Erreur deleteRequest:', error); res.status(500).json({ success: false, - message: 'Erreur lors de la suppression', + message: 'Erreur lors de l\'annulation', error: error.message }); + } finally { + if (conn) conn.release(); } }); - -app.get('/exportCompteurs', async (req, res) => { +app.get('/api/exportCompteurs', async (req, res) => { try { const dateRef = req.query.dateRef || new Date().toISOString().split('T')[0]; const conn = await pool.getConnection(); @@ -5452,11 +6789,1006 @@ app.get('/exportCompteurs', async (req, res) => { } }); + +function isInPeriodeAnticipation(dateDebut, typeConge) { + const date = new Date(dateDebut); + const year = date.getFullYear(); + const month = date.getMonth() + 1; // 1-12 + + if (typeConge === 'CP') { + // CP : 01/06 année N → 31/05 année N+1 + // Période anticipation : du 01/06 de l'année suivante + return month >= 6; // Si >= juin, c'est pour l'exercice N+1 + } else if (typeConge === 'RTT') { + // RTT : 01/01 année N → 31/12 année N + // Pas d'anticipation possible car année civile + return month >= 1 && month <= 12; + } + + return false; +} + +function getAnneeCompteur(dateDebut, typeConge) { + const date = new Date(dateDebut); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + + if (typeConge === 'CP') { + // Si date entre 01/06 et 31/12 → année N + // Si date entre 01/01 et 31/05 → année N-1 (exercice précédent) + return month >= 6 ? year : year - 1; + } else { + // RTT : toujours année civile + return year; + } +} + +/** + * Vérifie la disponibilité des soldes pour une demande + * Retourne : { available: boolean, details: {}, useN1: boolean } + */ + + +async function checkSoldesDisponiblesMixte(conn, collaborateurId, repartition, dateDebut, isApprenti) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const currentYear = today.getFullYear(); + const dateDemandeObj = new Date(dateDebut); + dateDemandeObj.setHours(0, 0, 0, 0); + + const demandeYear = dateDemandeObj.getFullYear(); + const demandeMonth = dateDemandeObj.getMonth() + 1; + + console.log('\n🔍 === CHECK SOLDES MIXTE (AVEC ANTICIPATION) ==='); + console.log('📅 Date AUJOURD\'HUI:', today.toISOString().split('T')[0]); + console.log('📅 Date DEMANDE:', dateDebut); + console.log('📅 Année demande:', demandeYear, '/ Mois:', demandeMonth); + console.log('📅 Année actuelle:', currentYear); + + let totalDisponible = 0; + let totalNecessaire = 0; + const details = {}; + + for (const rep of repartition) { + const typeCode = rep.TypeConge; + const joursNecessaires = parseFloat(rep.NombreJours || 0); + + // Ignorer ABS et Formation + if (typeCode === 'ABS' || typeCode === 'Formation') { + continue; + } + + totalNecessaire += joursNecessaires; + + if (typeCode === 'CP') { + // ⭐ RÉCUPÉRER LES INFOS COLLABORATEUR + const [collabInfo] = await conn.query( + `SELECT DateEntree FROM CollaborateurAD WHERE id = ?`, + [collaborateurId] + ); + const dateEntree = collabInfo[0]?.DateEntree; + + // ⭐ CALCULER L'ACQUISITION JUSQU'À LA DATE DEMANDÉE + const acquisALaDate = calculerAcquisitionCP(dateDemandeObj, dateEntree); + + console.log('💰 Acquisition CP à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j'); + + // ⭐ RÉCUPÉRER LE REPORTÉ N-1 + const previousYear = currentYear - 1; + const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); + const cpTypeId = cpType[0].Id; + + const [compteurN1] = await conn.query(` + SELECT SoldeReporte + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, cpTypeId, previousYear]); + + const reporteN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0; + + // ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ (toutes demandes validées ou en attente) + const [totalPose] = await conn.query(` + SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose + FROM DemandeConge dc + JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + WHERE dc.CollaborateurADId = ? + AND tc.Nom = 'Congé payé' + AND dc.Statut IN ('Validée', 'En attente') + AND dc.DateDebut <= ? + `, [collaborateurId, dateDebut]); + + const dejaPose = parseFloat(totalPose[0].totalPose || 0); + + // ⭐ SOLDE RÉEL = Reporté N-1 + Acquisition - Déjà posé + const soldeReel = reporteN1 + acquisALaDate - dejaPose; + + console.log('💰 Soldes CP détaillés:', { + reporteN1: reporteN1.toFixed(2), + acquisALaDate: acquisALaDate.toFixed(2), + dejaPose: dejaPose.toFixed(2), + soldeReel: soldeReel.toFixed(2) + }); + + details.CP = { + reporteN1: reporteN1, + acquisALaDate: acquisALaDate, + dejaPose: dejaPose, + soldeReel: soldeReel, + necessaire: joursNecessaires + }; + + totalDisponible += Math.max(0, soldeReel); + + if (soldeReel < joursNecessaires) { + return { + available: false, + message: `Solde CP insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`, + details, + manque: joursNecessaires - soldeReel + }; + } + + } else if (typeCode === 'RTT') { + if (isApprenti) { + return { + available: false, + message: 'Les apprentis ne peuvent pas poser de RTT', + details + }; + } + + // ⭐ CALCUL RTT (utiliser la fonction existante) + const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDemandeObj); + const acquisALaDate = rttData.acquisition; + + console.log('💰 Acquisition RTT à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j'); + + // ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ + const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); + const rttTypeId = rttType[0].Id; + + const [totalPose] = await conn.query(` + SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose + FROM DemandeConge dc + JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId + JOIN TypeConge tc ON dct.TypeCongeId = tc.Id + WHERE dc.CollaborateurADId = ? + AND tc.Nom = 'RTT' + AND dc.Statut IN ('Validée', 'En attente') + AND dc.DateDebut <= ? + `, [collaborateurId, dateDebut]); + + const dejaPose = parseFloat(totalPose[0].totalPose || 0); + + // ⭐ SOLDE RÉEL = Acquisition - Déjà posé + const soldeReel = acquisALaDate - dejaPose; + + console.log('💰 Soldes RTT détaillés:', { + acquisALaDate: acquisALaDate.toFixed(2), + dejaPose: dejaPose.toFixed(2), + soldeReel: soldeReel.toFixed(2) + }); + + details.RTT = { + acquisALaDate: acquisALaDate, + dejaPose: dejaPose, + soldeReel: soldeReel, + necessaire: joursNecessaires + }; + + totalDisponible += Math.max(0, soldeReel); + + if (soldeReel < joursNecessaires) { + return { + available: false, + message: `Solde RTT insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`, + details, + manque: joursNecessaires - soldeReel + }; + } + + } else if (typeCode === 'Récup') { + const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); + if (recupType.length === 0) continue; + + const recupTypeId = recupType[0].Id; + + const [compteur] = await conn.query( + `SELECT Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, recupTypeId, currentYear] + ); + + const soldeRecup = compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; + + console.log('💰 Solde Récup:', soldeRecup.toFixed(2), 'j'); + + details.Recup = { + soldeN: soldeRecup, + total: soldeRecup, + necessaire: joursNecessaires + }; + + totalDisponible += Math.min(joursNecessaires, soldeRecup); + + if (soldeRecup < joursNecessaires) { + return { + available: false, + message: `Solde Récupération insuffisant (${soldeRecup.toFixed(2)}j disponibles, ${joursNecessaires}j demandés)`, + details, + manque: joursNecessaires - soldeRecup + }; + } + } + } + + console.log('\n✅ Check final:', { + totalDisponible: totalDisponible.toFixed(2), + totalNecessaire: totalNecessaire.toFixed(2), + available: totalDisponible >= totalNecessaire + }); + + return { + available: totalDisponible >= totalNecessaire, + details, + totalDisponible, + totalNecessaire + }; +} +// ======================================== +// FONCTIONS HELPER +// ======================================== + +async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe = false) { + const currentYear = new Date().getFullYear(); + const previousYear = currentYear - 1; + + console.log(`\n📊 getSoldesCP - includeN1Anticipe: ${includeN1Anticipe}`); + + const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Congé payé' LIMIT 1`); + const typeCongeId = cpType[0].Id; + + // N-1 (reporté) + const [compteursN1] = await conn.query( + `SELECT Solde, SoldeReporte FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, typeCongeId, previousYear] + ); + + const soldeN1 = compteursN1.length > 0 ? parseFloat(compteursN1[0].SoldeReporte || 0) : 0; + + // N (actuel) + const [compteursN] = await conn.query( + `SELECT Solde, SoldeReporte, Total FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, typeCongeId, currentYear] + ); + + const soldeN = compteursN.length > 0 + ? parseFloat(compteursN[0].Solde || 0) - parseFloat(compteursN[0].SoldeReporte || 0) + : 0; + const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0; + + // Anticipation N + const finExerciceN = new Date(currentYear + 1, 4, 31); // 31 mai N+1 + const acquisTotaleN = calculerAcquisitionCP(finExerciceN, dateEntree); + const soldeAnticipeN = Math.max(0, acquisTotaleN - totalAcquisN); + + console.log(' N-1:', soldeN1); + console.log(' N:', soldeN); + console.log(' Anticipé N:', soldeAnticipeN); + + // ⭐ Anticipation N+1 (si demandé) + let soldeAnticipeN1 = 0; + if (includeN1Anticipe) { + const debutExerciceN1 = new Date(currentYear + 1, 5, 1); // 01 juin N+1 + const finExerciceN1 = new Date(currentYear + 2, 4, 31); // 31 mai N+2 + + let dateCalculN1 = debutExerciceN1; + if (dateEntree && new Date(dateEntree) > debutExerciceN1) { + dateCalculN1 = new Date(dateEntree); + } + + const acquisTotaleN1 = calculerAcquisitionCP(finExerciceN1, dateCalculN1); + soldeAnticipeN1 = acquisTotaleN1; + + console.log(' Anticipé N+1:', soldeAnticipeN1); + } + + return { soldeN1, soldeN, soldeAnticipeN, soldeAnticipeN1 }; +} + +async function getSoldesRTT(conn, collaborateurId, typeContrat, dateEntree) { + const currentYear = new Date().getFullYear(); + + const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`); + const typeCongeId = rttType[0].Id; + + const compteursN = await conn.query( + `SELECT Solde, Total FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, typeCongeId, currentYear] + ); + + const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) : 0; + const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0; + + // Calcul anticipation N + const finAnneeN = new Date(currentYear, 11, 31); // 31 déc N + const rttDataTotalN = await calculerAcquisitionRTT(conn, collaborateurId, finAnneeN); + const soldeAnticipeN = Math.max(0, rttDataTotalN.acquisition - totalAcquisN); + + return { soldeN, soldeAnticipeN }; +} + +async function getSoldesRecup(conn, collaborateurId) { + const currentYear = new Date().getFullYear(); + + const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`); + if (recupType.length === 0) return 0; + + const typeCongeId = recupType[0].Id; + + const compteur = await conn.query( + `SELECT Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, typeCongeId, currentYear] + ); + + return compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; +} + +app.get('/api/getAnticipationDisponible', async (req, res) => { + try { + const { userid, dateDebut } = req.query; + + if (!userid || !dateDebut) { + return res.json({ success: false, message: 'Paramètres manquants' }); + } + + const conn = await pool.getConnection(); + + const [collabInfo] = await conn.query( + `SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?`, + [userid] + ); + + const dateEntree = collabInfo.DateEntree; + const isApprenti = collabInfo.role === 'Apprenti'; + + // Déterminer si c'est une demande N+1 + const dateDemandeObj = new Date(dateDebut); + const currentYear = new Date().getFullYear(); + const demandeYear = dateDemandeObj.getFullYear(); + const demandeMonth = dateDemandeObj.getMonth() + 1; + + const isN1 = (demandeYear === currentYear + 1 && demandeMonth >= 6) || + (demandeYear === currentYear + 2 && demandeMonth <= 5); + + // Calculer les soldes avec anticipation + const soldesCP = await getSoldesCP(conn, userid, dateEntree, isN1); + const soldesRTT = isApprenti ? { soldeN: 0, soldeAnticipeN: 0 } : + await getSoldesRTT(conn, userid, collabInfo.TypeContrat, dateEntree); + const soldesRecup = await getSoldesRecup(conn, userid); + + conn.release(); + + res.json({ + success: true, + isN1Request: isN1, + CP: { + actuel: soldesCP.soldeN1 + soldesCP.soldeN, + anticipeN: soldesCP.soldeAnticipeN, + anticipeN1: isN1 ? soldesCP.soldeAnticipeN1 : 0, + total: soldesCP.soldeN1 + soldesCP.soldeN + soldesCP.soldeAnticipeN + (isN1 ? soldesCP.soldeAnticipeN1 : 0) + }, + RTT: { + actuel: soldesRTT.soldeN, + anticipeN: soldesRTT.soldeAnticipeN, + total: soldesRTT.soldeN + soldesRTT.soldeAnticipeN + }, + Recup: { + actuel: soldesRecup, + total: soldesRecup + } + }); + + } catch (error) { + console.error('Erreur getAnticipationDisponible:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + + + +async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) { + const currentYear = new Date().getFullYear(); + const previousYear = currentYear - 1; + const nextYear = currentYear + 1; + + let joursRestants = nombreJours; + const deductions = []; + + const dateDemandeObj = new Date(dateDebut); + const demandeYear = dateDemandeObj.getFullYear(); + const demandeMonth = dateDemandeObj.getMonth() + 1; + + // Déterminer le type de congé + const [typeRow] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]); + const typeNom = typeRow[0].Nom; + const isCP = typeNom === 'Congé payé'; + + // Vérifier si demande pour N+1 + let useN1 = false; + if (isCP) { + useN1 = (demandeYear === nextYear && demandeMonth >= 6) || + (demandeYear === nextYear + 1 && demandeMonth <= 5); + } else { + useN1 = demandeYear === nextYear; + } + + console.log(`\n💰 Déduction ${typeNom}: ${nombreJours}j (useN1: ${useN1})`); + + if (useN1) { + // ORDRE N+1 : N+1 anticipé → N anticipé → N actuel → N-1 + + // 1. N+1 Anticipé (priorité absolue) + const compteurN1Anticipe = await conn.query( + `SELECT Id, SoldeAnticipe FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, typeCongeId, nextYear] + ); + + if (compteurN1Anticipe.length === 0 && joursRestants > 0) { + // Créer le compteur N+1 si inexistant + await conn.query( + `INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, IsAnticipe) + VALUES (?, ?, ?, 0, 0, 0, 0, 0)`, + [collaborateurId, typeCongeId, nextYear] + ); + } + + // Récupérer à nouveau après création + const compteurN1A = await conn.query( + `SELECT Id, SoldeAnticipe FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, + [collaborateurId, typeCongeId, nextYear] + ); + + if (compteurN1A.length > 0) { + const aDeduire = Math.min(joursRestants, joursRestants); // Tous les jours restants + + if (aDeduire > 0) { + await conn.query( + `UPDATE CompteurConges + SET SoldeAnticipe = SoldeAnticipe + ?, IsAnticipe = 1 + WHERE Id = ?`, + [aDeduire, compteurN1A[0].Id] + ); + + await conn.query( + `INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'N+1 Anticipé', ?)`, + [demandeCongeId, typeCongeId, nextYear, aDeduire] + ); + + deductions.push({ + annee: nextYear, + type: 'N+1 Anticipé', + joursUtilises: aDeduire + }); + + joursRestants -= aDeduire; + console.log(`✓ N+1 Anticipé: ${aDeduire}j - reste: ${joursRestants}j`); + } + } + // 2. N anticipé + if (joursRestants > 0) { + const [compteurN_Anticipe] = await conn.query(` + SELECT Id, SoldeAnticipe + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, typeCongeId, currentYear]); + + if (compteurN_Anticipe.length > 0) { + const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0); + const aDeduire = Math.min(soldeNA, joursRestants); + + if (aDeduire > 0) { + await conn.query(` + UPDATE CompteurConges + SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?) + WHERE Id = ? + `, [aDeduire, compteurN_Anticipe[0].Id]); + + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'N Anticipé', ?) + `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); + + deductions.push({ + annee: currentYear, + type: 'N Anticipé', + joursUtilises: aDeduire, + soldeAvant: soldeNA + }); + + joursRestants -= aDeduire; + console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`); + } + } + } + + // 3. N actuel + if (joursRestants > 0) { + const [compteurN] = await conn.query(` + SELECT Id, Solde, SoldeReporte + 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 aDeduire = Math.min(soldeN, joursRestants); + + if (aDeduire > 0) { + await conn.query(` + UPDATE CompteurConges + SET Solde = GREATEST(0, Solde - ?) + WHERE Id = ? + `, [aDeduire, compteurN[0].Id]); + + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Année N', ?) + `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); + + deductions.push({ + annee: currentYear, + type: 'Année N', + joursUtilises: aDeduire, + soldeAvant: soldeN + }); + + joursRestants -= aDeduire; + console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`); + } + } + } + + // 4. N-1 reporté + if (joursRestants > 0) { + const [compteurN1] = await conn.query(` + SELECT Id, SoldeReporte + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, typeCongeId, previousYear]); + + if (compteurN1.length > 0) { + const soldeN1 = parseFloat(compteurN1[0].SoldeReporte || 0); + const aDeduire = Math.min(soldeN1, joursRestants); + + if (aDeduire > 0) { + await conn.query(` + UPDATE CompteurConges + SET SoldeReporte = GREATEST(0, SoldeReporte - ?), + Solde = GREATEST(0, Solde - ?) + WHERE Id = ? + `, [aDeduire, aDeduire, compteurN1[0].Id]); + + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Reporté N-1', ?) + `, [demandeCongeId, typeCongeId, previousYear, aDeduire]); + + deductions.push({ + annee: previousYear, + type: 'Reporté N-1', + joursUtilises: aDeduire, + soldeAvant: soldeN1 + }); + + joursRestants -= aDeduire; + console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`); + } + } + } + + } else { + // ORDRE NORMAL : N-1 → N → N anticipé + + // 1. Reporté N-1 + const [compteurN1] = await conn.query(` + SELECT Id, SoldeReporte, Solde + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, typeCongeId, previousYear]); + + if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) { + const soldeN1 = parseFloat(compteurN1[0].SoldeReporte); + const aDeduire = Math.min(soldeN1, joursRestants); + + if (aDeduire > 0) { + await conn.query(` + UPDATE CompteurConges + SET SoldeReporte = GREATEST(0, SoldeReporte - ?), + Solde = GREATEST(0, Solde - ?) + WHERE Id = ? + `, [aDeduire, aDeduire, compteurN1[0].Id]); + + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Reporté N-1', ?) + `, [demandeCongeId, typeCongeId, previousYear, aDeduire]); + + deductions.push({ + annee: previousYear, + type: 'Reporté N-1', + joursUtilises: aDeduire, + soldeAvant: soldeN1 + }); + + joursRestants -= aDeduire; + console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`); + } + } + + // 2. Année N + if (joursRestants > 0) { + const [compteurN] = await conn.query(` + SELECT Id, Solde, SoldeReporte + 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 aDeduire = Math.min(soldeN, joursRestants); + + if (aDeduire > 0) { + await conn.query(` + UPDATE CompteurConges + SET Solde = GREATEST(0, Solde - ?) + WHERE Id = ? + `, [aDeduire, compteurN[0].Id]); + + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Année N', ?) + `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); + + deductions.push({ + annee: currentYear, + type: 'Année N', + joursUtilises: aDeduire, + soldeAvant: soldeN + }); + + joursRestants -= aDeduire; + console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`); + } + } + } + + // 3. N anticipé + if (joursRestants > 0) { + const [compteurN_Anticipe] = await conn.query(` + SELECT Id, SoldeAnticipe + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, typeCongeId, currentYear]); + + if (compteurN_Anticipe.length > 0) { + const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0); + const aDeduire = Math.min(soldeNA, joursRestants); + + if (aDeduire > 0) { + await conn.query(` + UPDATE CompteurConges + SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?) + WHERE Id = ? + `, [aDeduire, compteurN_Anticipe[0].Id]); + + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'N Anticipé', ?) + `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); + + deductions.push({ + annee: currentYear, + type: 'N Anticipé', + joursUtilises: aDeduire, + soldeAvant: soldeNA + }); + + joursRestants -= aDeduire; + console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`); + } + } + } + } + + return { + success: joursRestants === 0, + joursDeduitsTotal: nombreJours - joursRestants, + joursNonDeduits: joursRestants, + details: deductions, + useN1: useN1 + }; +} + +/** + * Met à jour les soldes anticipés pour un collaborateur + * Appelée après chaque mise à jour de compteur ou soumission de demande + */ +async function updateSoldeAnticipe(conn, collaborateurId) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const currentYear = today.getFullYear(); + + console.log(`\n🔄 Mise à jour soldes anticipés pour collaborateur ${collaborateurId}`); + + const [collab] = await conn.query( + 'SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?', + [collaborateurId] + ); + + if (collab.length === 0) { + console.log(' ❌ Collaborateur non trouvé'); + return; + } + + const dateEntree = collab[0].DateEntree; + const typeContrat = collab[0].TypeContrat || '37h'; + const isApprenti = collab[0].role === 'Apprenti'; + + // ===== CP ANTICIPÉ ===== + const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); + + if (cpType.length > 0) { + const cpAnticipe = calculerAcquisitionCPAnticipee(today, dateEntree); + + // Vérifier si le compteur existe + const [compteurCP] = await conn.query(` + SELECT Id FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, cpType[0].Id, currentYear]); + + if (compteurCP.length > 0) { + await conn.query(` + UPDATE CompteurConges + SET SoldeAnticipe = ?, + DerniereMiseAJour = NOW() + WHERE Id = ? + `, [cpAnticipe, compteurCP[0].Id]); + } else { + // Créer le compteur s'il n'existe pas + const acquisCP = calculerAcquisitionCP(today, dateEntree); + await conn.query(` + INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, ?, NOW()) + `, [collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP, cpAnticipe]); + } + + console.log(` ✓ CP Anticipé: ${cpAnticipe.toFixed(2)}j`); + } + + // ===== RTT ANTICIPÉ ===== + if (!isApprenti) { + const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); + + if (rttType.length > 0) { + const rttAnticipe = await calculerAcquisitionRTTAnticipee(conn, collaborateurId, today); + + // Vérifier si le compteur existe + const [compteurRTT] = await conn.query(` + SELECT Id FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [collaborateurId, rttType[0].Id, currentYear]); + + if (compteurRTT.length > 0) { + await conn.query(` + UPDATE CompteurConges + SET SoldeAnticipe = ?, + DerniereMiseAJour = NOW() + WHERE Id = ? + `, [rttAnticipe, compteurRTT[0].Id]); + } else { + // Créer le compteur s'il n'existe pas + const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); + await conn.query(` + INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, ?, NOW()) + `, [collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition, rttAnticipe]); + } + + console.log(` ✓ RTT Anticipé: ${rttAnticipe.toFixed(2)}j`); + } + } + + console.log(` ✅ Soldes anticipés mis à jour\n`); +} + +/** + * GET /getSoldesAnticipes + * Retourne les soldes actuels ET anticipés pour un collaborateur + */ +app.get('/api/getSoldesAnticipes', async (req, res) => { + try { + const userIdParam = req.query.user_id; + const dateRefParam = req.query.date_reference; + + if (!userIdParam) { + return res.json({ success: false, message: 'ID utilisateur manquant' }); + } + + const conn = await pool.getConnection(); + + // Déterminer l'ID + const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); + const userQuery = ` + SELECT ca.id, ca.prenom, ca.nom, ca.DateEntree, ca.TypeContrat, ca.role + FROM CollaborateurAD ca + WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ? + AND (ca.Actif = 1 OR ca.Actif IS NULL) + `; + + const [userInfo] = await conn.query(userQuery, [userIdParam]); + + if (userInfo.length === 0) { + conn.release(); + return res.json({ success: false, message: 'Utilisateur non trouvé' }); + } + + const user = userInfo[0]; + const userId = user.id; + const dateEntree = user.DateEntree; + const typeContrat = user.TypeContrat || '37h'; + const isApprenti = user.role === 'Apprenti'; + + const dateReference = dateRefParam ? new Date(dateRefParam) : new Date(); + dateReference.setHours(0, 0, 0, 0); + const currentYear = dateReference.getFullYear(); + + // ===== CP ===== + const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); + + let cpData = { + acquis: 0, + solde: 0, + anticipe: 0, + totalDisponible: 0 + }; + + if (cpType.length > 0) { + // Acquisition actuelle + const acquisCP = calculerAcquisitionCP(dateReference, dateEntree); + + // Anticipé + const anticipeCP = calculerAcquisitionCPAnticipee(dateReference, dateEntree); + + // Solde en base (avec consommations déduites) + const [compteurCP] = await conn.query(` + SELECT Solde, SoldeReporte, SoldeAnticipe + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [userId, cpType[0].Id, currentYear]); + + // Reporté N-1 + const [compteurCPN1] = await conn.query(` + SELECT Solde as SoldeReporte + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [userId, cpType[0].Id, currentYear - 1]); + + const soldeN1 = compteurCPN1.length > 0 ? parseFloat(compteurCPN1[0].SoldeReporte || 0) : 0; + const soldeN = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde || 0) : acquisCP; + + cpData = { + acquis: parseFloat(acquisCP.toFixed(2)), + soldeN1: parseFloat(soldeN1.toFixed(2)), + soldeN: parseFloat((soldeN - soldeN1).toFixed(2)), + soldeTotal: parseFloat(soldeN.toFixed(2)), + anticipe: parseFloat(anticipeCP.toFixed(2)), + totalDisponible: parseFloat((soldeN + anticipeCP).toFixed(2)) + }; + } + + // ===== RTT ===== + let rttData = { + acquis: 0, + solde: 0, + anticipe: 0, + totalDisponible: 0, + isApprenti: isApprenti + }; + + if (!isApprenti) { + const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); + + if (rttType.length > 0) { + // Acquisition actuelle + const rttCalc = await calculerAcquisitionRTT(conn, userId, dateReference); + const acquisRTT = rttCalc.acquisition; + + // Anticipé + const anticipeRTT = await calculerAcquisitionRTTAnticipee(conn, userId, dateReference); + + // Solde en base + const [compteurRTT] = await conn.query(` + SELECT Solde, SoldeAnticipe + FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [userId, rttType[0].Id, currentYear]); + + const soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde || 0) : acquisRTT; + + rttData = { + acquis: parseFloat(acquisRTT.toFixed(2)), + solde: parseFloat(soldeRTT.toFixed(2)), + anticipe: parseFloat(anticipeRTT.toFixed(2)), + totalDisponible: parseFloat((soldeRTT + anticipeRTT).toFixed(2)), + config: rttCalc.config, + typeContrat: typeContrat + }; + } + } + + // ===== RÉCUP ===== + const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); + let recupData = { solde: 0 }; + + if (recupType.length > 0) { + const [compteurRecup] = await conn.query(` + SELECT Solde FROM CompteurConges + WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? + `, [userId, recupType[0].Id, currentYear]); + + recupData.solde = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0; + } + + conn.release(); + + res.json({ + success: true, + user: { + id: user.id, + nom: `${user.prenom} ${user.nom}`, + typeContrat: typeContrat, + dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null + }, + dateReference: dateReference.toISOString().split('T')[0], + cp: cpData, + rtt: rttData, + recup: recupData, + totalGeneral: { + disponibleActuel: parseFloat((cpData.soldeTotal + rttData.solde + recupData.solde).toFixed(2)), + disponibleAvecAnticipe: parseFloat((cpData.totalDisponible + rttData.totalDisponible + recupData.solde).toFixed(2)) + } + }); + + } catch (error) { + console.error('Erreur getSoldesAnticipes:', error); + res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); + } +}); /** * GET /getCongesAnticipes * Calcule les congés anticipés disponibles pour un collaborateur */ -app.get('/getCongesAnticipes', async (req, res) => { +app.get('/api/getCongesAnticipes', async (req, res) => { try { const userIdParam = req.query.user_id; @@ -5639,7 +7971,84 @@ app.get('/getCongesAnticipes', async (req, res) => { } }); -app.get('/getStatistiquesCompteurs', async (req, res) => { +/** + * Calcule l'acquisition CP ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'exercice) + */ +function calculerAcquisitionCPAnticipee(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 la fin de l'exercice CP (31/05) + let finExercice; + if (mois >= 6) { + finExercice = new Date(annee + 1, 4, 31); // 31/05/N+1 + } else { + finExercice = new Date(annee, 4, 31); // 31/05/N + } + finExercice.setHours(0, 0, 0, 0); + + // 2️⃣ Calculer l'acquisition actuelle + const acquisActuelle = calculerAcquisitionCP(d, dateEntree); + + // 3️⃣ Calculer l'acquisition totale à fin d'exercice + const acquisTotaleFinExercice = calculerAcquisitionCP(finExercice, dateEntree); + + // 4️⃣ Anticipée = Totale - Actuelle (plafonnée à 25) + const acquisAnticipee = Math.min(25, acquisTotaleFinExercice) - acquisActuelle; + + return Math.max(0, Math.round(acquisAnticipee * 100) / 100); +} + +/** + * Calcule l'acquisition RTT ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'année) + */ +async function calculerAcquisitionRTTAnticipee(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) { + return 0; + } + + const typeContrat = collabInfo[0].TypeContrat || '37h'; + const isApprenti = collabInfo[0].role === 'Apprenti'; + + // 2️⃣ Apprentis = pas de RTT + if (isApprenti) { + return 0; + } + + // 3️⃣ Récupérer la configuration RTT + const config = await getConfigurationRTT(conn, annee, typeContrat); + + // 4️⃣ Calculer l'acquisition actuelle + const rttActuel = await calculerAcquisitionRTT(conn, collaborateurId, d); + const acquisActuelle = rttActuel.acquisition; + + // 5️⃣ Calculer l'acquisition totale à fin d'année (31/12) + const finAnnee = new Date(annee, 11, 31); + finAnnee.setHours(0, 0, 0, 0); + + const rttFinAnnee = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee); + const acquisTotaleFinAnnee = rttFinAnnee.acquisition; + + // 6️⃣ Anticipée = Totale - Actuelle (plafonnée au max annuel) + const acquisAnticipee = Math.min(config.joursAnnuels, acquisTotaleFinAnnee) - acquisActuelle; + + return Math.max(0, Math.round(acquisAnticipee * 100) / 100); +} + +app.get('/api/getStatistiquesCompteurs', async (req, res) => { try { const conn = await pool.getConnection(); const currentYear = new Date().getFullYear(); @@ -5764,7 +8173,7 @@ async function hasCompteRenduAccess(userId) { // Récupérer les jours du mois // GET - Récupérer les données du compte-rendu -app.get('/getCompteRenduActivites', async (req, res) => { +app.get('/api/getCompteRenduActivites', async (req, res) => { const { user_id, annee, mois } = req.query; try { @@ -5825,7 +8234,7 @@ app.get('/getCompteRenduActivites', async (req, res) => { // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE -app.post('/saveCompteRenduJour', async (req, res) => { +app.post('/api/saveCompteRenduJour', async (req, res) => { const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body; try { @@ -5924,7 +8333,7 @@ app.post('/saveCompteRenduJour', async (req, res) => { }); // POST - Saisie en masse avec AUTO-VERROUILLAGE -app.post('/saveCompteRenduMasse', async (req, res) => { +app.post('/api/saveCompteRenduMasse', async (req, res) => { const { user_id, annee, mois, jours, rh_override } = req.body; try { @@ -6019,7 +8428,7 @@ app.post('/saveCompteRenduMasse', async (req, res) => { } }); -app.post('/deverrouillerJour', async (req, res) => { +app.post('/api/deverrouillerJour', async (req, res) => { const { user_id, date, rh_user_id } = req.body; try { @@ -6053,7 +8462,7 @@ app.post('/deverrouillerJour', async (req, res) => { // POST - Verrouiller (RH uniquement) -app.post('/verrouillerCompteRendu', async (req, res) => { +app.post('/api/verrouillerCompteRendu', async (req, res) => { const { user_id, annee, mois, rh_user_id } = req.body; try { @@ -6088,7 +8497,7 @@ app.post('/verrouillerCompteRendu', async (req, res) => { }); // POST - Déverrouiller (RH uniquement) -app.post('/deverrouillerCompteRendu', async (req, res) => { +app.post('/api/deverrouillerCompteRendu', async (req, res) => { const { user_id, annee, mois, rh_user_id } = req.body; try { @@ -6124,7 +8533,7 @@ app.post('/deverrouillerCompteRendu', async (req, res) => { }); // GET - Stats annuelles -app.get('/getStatsAnnuelles', async (req, res) => { +app.get('/api/getStatsAnnuelles', async (req, res) => { const { user_id, annee } = req.query; try { @@ -6157,7 +8566,7 @@ app.get('/getStatsAnnuelles', async (req, res) => { }); // GET - Export PDF (RH uniquement) -app.get('/exportCompteRenduPDF', async (req, res) => { +app.get('/api/exportCompteRenduPDF', async (req, res) => { const { user_id, annee, mois } = req.query; try { @@ -6202,44 +8611,18 @@ app.get('/exportCompteRenduPDF', async (req, res) => { + + + + // ======================================== // DÉMARRAGE DU SERVEUR // ======================================== -app.listen(PORT, () => { - console.log(`✅ Serveur démarré sur http://localhost:${PORT}`); - console.log('📋 Routes disponibles:'); - console.log(' POST /login'); - console.log(' POST /check-user-groups'); - console.log(' GET /getDetailedLeaveCounters'); - console.log(' POST /updateCounters'); - console.log(' POST /updateAllCounters'); - console.log(' POST /reinitializeAllCounters'); - console.log(' GET /testProrata'); - console.log(' POST /fixAllCounters'); - console.log(' POST /submitLeaveRequest'); - console.log(' POST /validateRequest'); - console.log(' GET /getRequests'); - console.log(' GET /getNotifications'); - console.log(' POST /markNotificationRead'); - console.log(' GET /getPendingRequests'); - console.log(' GET /getTeamMembers'); - console.log(' GET /getTeamLeaves'); - console.log(' GET /getAllTeamRequests'); - console.log(' GET /getEmployeRequest'); - console.log(' POST /initial-sync'); - console.log(''); - console.log(' 🆕 ROUTES ADMIN RTT:'); - console.log(' GET /getAllCollaborateurs'); - console.log(' POST /updateTypeContrat'); - console.log(' GET /getConfigurationRTT'); - console.log(' POST /updateConfigurationRTT'); - console.log(' GET /exportCompteurs'); - console.log(' GET /getStatistiquesCompteurs'); - console.log(''); - console.log('⏰ Tâches CRON actives:'); - console.log(' 📅 Mise à jour mensuelle: 1er de chaque mois à 00h01'); - console.log(' 📸 Arrêtés mensuels: Dernier jour de chaque mois à 23h55'); // ⭐ NOUVEAU - console.log(' 🎆 Fin d\'année RTT: 31 décembre à 23h59'); - console.log(' 📅 Fin d\'exercice CP: 31 mai à 23h59'); +app.listen(PORT, '0.0.0.0', () => { + console.log('✅ ✅ ✅ SERVEUR PRINCIPAL DÉMARRÉ ✅ ✅ ✅'); + console.log(`📡 Port: ${PORT}`); + console.log(`🗄️ Base: ${dbConfig.database}@${dbConfig.host}`); + console.log(`⏰ Cron jobs: activés`); + console.log(`🌐 CORS origins: ${JSON.stringify(dbConfig)}`); }); \ No newline at end of file diff --git a/project/public/Backend/uploads/medical/medical-1763460916289-157625577.pdf b/project/public/Backend/uploads/medical/medical-1763460916289-157625577.pdf new file mode 100644 index 0000000..8e0a24c Binary files /dev/null and b/project/public/Backend/uploads/medical/medical-1763460916289-157625577.pdf differ diff --git a/project/public/Backend/useSSENotifications.js b/project/public/Backend/useSSENotifications.js new file mode 100644 index 0000000..879e222 --- /dev/null +++ b/project/public/Backend/useSSENotifications.js @@ -0,0 +1,38 @@ +// hooks/useSSENotifications.js +import { useEffect, useCallback } from 'react'; + +export const useSSENotifications = (token, collaborateurId, onEventReceived) => { + useEffect(() => { + if (!token || !collaborateurId) return; + + const eventSource = new EventSource( + `/api/events?token=${encodeURIComponent(token)}` + ); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + console.log('📨 SSE reçu:', data); + + // Log spécifique pour les récupérations + if (data.type === 'demande-validated' && data.typeConge === 'Récupération') { + console.log('🎨 Couleur reçue:', data.couleurHex); + } + + onEventReceived(data); + } catch (error) { + console.error('❌ Erreur parsing SSE:', error); + } + }; + + eventSource.onerror = (error) => { + console.error('❌ Erreur SSE:', error); + eventSource.close(); + }; + + return () => { + eventSource.close(); + }; + }, [token, collaborateurId, onEventReceived]); +}; \ No newline at end of file diff --git a/project/public/Backend/webhook-utils.js b/project/public/Backend/webhook-utils.js index 22fd15b..40b1bcf 100644 --- a/project/public/Backend/webhook-utils.js +++ b/project/public/Backend/webhook-utils.js @@ -1,4 +1,4 @@ -// webhook-utils.js (VERSION ES MODULES) +// webhook-utils.js (VERSION ES MODULES - CORRIGÉE) // Pour projets avec "type": "module" dans package.json import axios from 'axios'; @@ -65,6 +65,7 @@ class WebhookManager { for (let attempt = 1; attempt <= retries; attempt++) { try { console.log(`📤 Envoi webhook: ${eventType} vers ${targetUrl} (tentative ${attempt}/${retries})`); + console.log(` Données:`, JSON.stringify(data, null, 2)); const response = await axios.post( `${targetUrl}/api/webhook/receive`, diff --git a/project/public/php/Dockerfile.backend b/project/public/php/Dockerfile.backend deleted file mode 100644 index bbcc787..0000000 --- a/project/public/php/Dockerfile.backend +++ /dev/null @@ -1,14 +0,0 @@ -# Utilise une image PHP avec Apache et la version 8.1 -FROM php:8.1-apache - -# Installe l'extension mysqli pour te connecter à la base de données MySQL -RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli - -# Active le module de réécriture d'URL d'Apache (souvent utile) -RUN a2enmod rewrite - -# Copie tous les fichiers du back-end dans le dossier de travail d'Apache -COPY . /var/www/html/ - -# Expose le port 80 (par défaut pour un serveur web) -EXPOSE 80 \ No newline at end of file diff --git a/project/public/php/check-user-groups.php b/project/public/php/check-user-groups.php deleted file mode 100644 index eaa44e4..0000000 --- a/project/public/php/check-user-groups.php +++ /dev/null @@ -1,147 +0,0 @@ -connect_error) { - die(json_encode(["authorized" => false, "message" => "Erreur DB: " . $conn->connect_error])); -} - -// --- ID du groupe cible (Ensup-Groupe) --- -$groupId = "c1ea877c-6bca-4f47-bfad-f223640813a0"; - -// Récupération des données POST -$data = json_decode(file_get_contents("php://input"), true); -$userPrincipalName = $data["userPrincipalName"] ?? ""; - -// Récupération du token dans les headers -$headers = getallheaders(); -$accessToken = isset($headers['Authorization']) - ? str_replace("Bearer ", "", $headers['Authorization']) - : ""; - -if (!$userPrincipalName || !$accessToken) { - echo json_encode(["authorized" => false, "message" => "Email ou token manquant"]); - exit; -} - -/** - * Fonction générique pour appeler Graph API - */ -function callGraph($url, $accessToken, $method = "GET", $body = null) { - $ch = curl_init($url); - $headers = ["Authorization: Bearer $accessToken"]; - if ($method === "POST") { - $headers[] = "Content-Type: application/json"; - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode !== 200) { - return null; - } - return json_decode($response, true); -} - -/** - * Vérifier si utilisateur appartient à un groupe - */ -function isUserInGroup($userId, $groupId, $accessToken) { - $url = "https://graph.microsoft.com/v1.0/users/$userId/checkMemberGroups"; - $data = json_encode(["groupIds" => [$groupId]]); - $result = callGraph($url, $accessToken, "POST", $data); - - return $result && isset($result["value"]) && in_array($groupId, $result["value"]); -} - -// 🔹 1. Vérifier si utilisateur existe déjà en DB -$stmt = $conn->prepare("SELECT id, entraUserId, prenom, nom, email, service, role FROM CollaborateurAD WHERE email = ? LIMIT 1"); -$stmt->bind_param("s", $userPrincipalName); -$stmt->execute(); -$result = $stmt->get_result(); -$user = $result->fetch_assoc(); -$stmt->close(); - -if ($user) { - echo json_encode([ - "authorized" => true, - "role" => $user["role"], - "groups" => [$user["role"]], - "localUserId" => (int)$user["id"], // 🔹 ajout important - "user" => $user - ]); - $conn->close(); - exit; -} - - -// 🔹 2. Sinon → chercher l’utilisateur dans Microsoft Graph -$userGraph = callGraph("https://graph.microsoft.com/v1.0/users/$userPrincipalName?\$select=id,displayName,givenName,surname,mail,department,jobTitle", $accessToken); - -if (!$userGraph) { - echo json_encode([ - "authorized" => false, - "message" => "Utilisateur introuvable dans Entra ou token invalide" - ]); - $conn->close(); - exit; -} - -// 🔹 3. Vérifier appartenance au groupe Ensup-Groupe -$isInTargetGroup = isUserInGroup($userGraph["id"], $groupId, $accessToken); - -if (!$isInTargetGroup) { - echo json_encode([ - "authorized" => false, - "message" => "Utilisateur non autorisé : il n'appartient pas au groupe requis" - ]); - $conn->close(); - exit; -} - -// 🔹 4. Insérer dans la base si nouveau -$entraUserId = $userGraph["id"]; -$prenom = $userGraph["givenName"] ?? ""; -$nom = $userGraph["surname"] ?? ""; -$email = $userGraph["mail"] ?? $userPrincipalName; -$service = $userGraph["department"] ?? ""; -$role = "Collaborateur"; // rôle par défaut - -$stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, role) - VALUES (?, ?, ?, ?, ?, ?)"); -$stmt->bind_param("ssssss", $entraUserId, $prenom, $nom, $email, $service, $role); -$stmt->execute(); -$newUserId = $stmt->insert_id; -$stmt->close(); - -// 🔹 5. Réponse finale -echo json_encode([ - "authorized" => true, - "role" => $role, - "groups" => [$role], - "localUserId" => (int)$newUserId, - "user" => [ - "id" => $newUserId, - "entraUserId" => $entraUserId, - "prenom" => $prenom, - "nom" => $nom, - "email" => $email, - "service" => $service, - "role" => $role - ] -]); - - -$conn->close(); -?> diff --git a/project/public/php/db.php b/project/public/php/db.php deleted file mode 100644 index a3ab16f..0000000 --- a/project/public/php/db.php +++ /dev/null @@ -1,20 +0,0 @@ -connect_error) { - die(json_encode([ - "success" => false, - "message" => "Erreur DB: " . $conn->connect_error - ])); -} - -// Important : définir l’encodage en UTF-8 (pour accents, etc.) -$conn->set_charset("utf8mb4"); diff --git a/project/public/php/getAllTeamRequests.php b/project/public/php/getAllTeamRequests.php deleted file mode 100644 index 8a65dad..0000000 --- a/project/public/php/getAllTeamRequests.php +++ /dev/null @@ -1,103 +0,0 @@ -connect_error) { - error_log("Erreur connexion DB: " . $conn->connect_error); - echo json_encode(["success" => false, "message" => "Erreur de connexion DB"]); - exit(); -} - -// Récupération ID manager -$managerId = $_GET['SuperieurId'] ?? null; -if (!$managerId) { - echo json_encode(["success" => false, "message" => "Paramètre SuperieurId manquant"]); - exit(); -} - -$sql = " - SELECT - dc.Id, - dc.DateDebut, - dc.DateFin, - dc.Statut, - dc.DateDemande, - dc.Commentaire, - dc.DocumentJoint, - dc.CollaborateurADId AS employee_id, - CONCAT(ca.Prenom, ' ', ca.Nom) as employee_name, - ca.Email as employee_email, - tc.Nom as type - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - JOIN TypeConge tc ON dc.TypeCongeId = tc.Id - JOIN HierarchieValidationAD hv ON hv.CollaborateurId = ca.id - WHERE hv.SuperieurId = ? - ORDER BY dc.DateDemande DESC -"; - -$stmt = $conn->prepare($sql); -$stmt->bind_param("i", $managerId); -$stmt->execute(); -$result = $stmt->get_result(); - -$requests = []; -while ($row = $result->fetch_assoc()) { - $startDate = new DateTime($row['DateDebut']); - $endDate = new DateTime($row['DateFin']); - $submittedDate = new DateTime($row['DateDemande']); - $days = 0; - - $tmp = clone $startDate; - while ($tmp <= $endDate) { - if ((int)$tmp->format('N') < 6) $days++; - $tmp->modify('+1 day'); - } - - $requests[] = [ - "id" => (int)$row['Id'], - "employee_id" => (int)$row['employee_id'], - "employee_name" => $row['employee_name'], - "employee_email" => $row['employee_email'], - "type" => $row['type'], - "start_date" => $row['DateDebut'], - "end_date" => $row['DateFin'], - "date_display" => $row['DateDebut'] === $row['DateFin'] - ? $startDate->format('d/m/Y') - : $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'), - "days" => $days, - "status" => $row['Statut'], - "reason" => $row['Commentaire'] ?: '', - "file" => $row['DocumentJoint'] ?: null, - "submitted_at" => $row['DateDemande'], - "submitted_display" => $submittedDate->format('d/m/Y') - ]; -} - -echo json_encode([ - "success" => true, - "requests" => $requests -]); - -$stmt->close(); -$conn->close(); -?> diff --git a/project/public/php/getEmploye.php b/project/public/php/getEmploye.php deleted file mode 100644 index 844feae..0000000 --- a/project/public/php/getEmploye.php +++ /dev/null @@ -1,52 +0,0 @@ -connect_error) { - die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error])); -} - -// Récupérer l'ID -$id = isset($_GET['id']) ? (int)$_GET['id'] : 0; -if ($id <= 0) { - echo json_encode(["success" => false, "message" => "ID collaborateur invalide"]); - exit; -} - -try { - $stmt = $conn->prepare(" - SELECT id, Nom, Prenom, Email - FROM CollaborateurAD - WHERE id = ? -"); - - $stmt->bind_param("i", $id); - $stmt->execute(); - $result = $stmt->get_result(); - $employee = $result->fetch_assoc(); - - if ($employee) { - echo json_encode(["success" => true, "employee" => $employee]); - } else { - echo json_encode(["success" => false, "message" => "Collaborateur non trouvé"]); - } -} catch (Exception $e) { - echo json_encode(["success" => false, "message" => "Erreur DB: " . $e->getMessage()]); -} - -$conn->close(); -?> diff --git a/project/public/php/getEmployeRequest.php b/project/public/php/getEmployeRequest.php deleted file mode 100644 index bdb7358..0000000 --- a/project/public/php/getEmployeRequest.php +++ /dev/null @@ -1,66 +0,0 @@ -connect_error) { - die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error])); -} - -// Récupérer l'ID -$id = isset($_GET['id']) ? (int)$_GET['id'] : 0; -if ($id <= 0) { - echo json_encode(["success" => false, "message" => "ID employé invalide"]); - exit; -} - -try { - $sql = "SELECT Id, TypeCongeId, NombreJours, DateDebut, DateFin, Statut - FROM DemandeConge - WHERE EmployeeId = ? - ORDER BY DateDemande DESC"; - - $stmt = $conn->prepare($sql); - $stmt->bind_param("i", $id); - $stmt->execute(); - $result = $stmt->get_result(); - - // Mapping des types de congés - $typeNames = [ - 1 => "Congé payé", - 2 => "RTT", - 3 => "Maladie" - ]; - - $requests = []; - while ($row = $result->fetch_assoc()) { - $row['type'] = $typeNames[$row['TypeCongeId']] ?? "Autre"; - $row['days'] = (float)$row['NombreJours']; - // Formater jours : 2j ou 1.5j - $row['days_display'] = ((int)$row['days'] == $row['days'] ? (int)$row['days'] : $row['days']) . "j"; - $row['date_display'] = date("d/m/Y", strtotime($row['DateDebut'])) - . " - " - . date("d/m/Y", strtotime($row['DateFin'])); - $requests[] = $row; - } - - echo json_encode(["success" => true, "requests" => $requests]); -} catch (Exception $e) { - echo json_encode(["success" => false, "message" => "Erreur DB: " . $e->getMessage()]); -} - -$conn->close(); -?> diff --git a/project/public/php/getLeaveCounters.php b/project/public/php/getLeaveCounters.php deleted file mode 100644 index 21d331a..0000000 --- a/project/public/php/getLeaveCounters.php +++ /dev/null @@ -1,163 +0,0 @@ -connect_error) { - error_log("Erreur DB: " . $conn->connect_error); - echo json_encode(['success' => false, 'message' => 'Erreur de connexion à la base de données']); - exit; -} - -$today = new DateTime(); -$yearCurrent = (int)$today->format('Y'); -$yearNMinus1 = $yearCurrent - 1; - -function getTypeId($conn, $nom) { - $stmt = $conn->prepare("SELECT Id FROM TypeConge WHERE Nom=?"); - $stmt->bind_param("s", $nom); - $stmt->execute(); - $result = $stmt->get_result(); - $id = null; - if ($row = $result->fetch_assoc()) { - $id = (int)$row['Id']; - } - $stmt->close(); - error_log("TypeConge '$nom' => Id $id"); - return $id; -} - -$cpTypeId = getTypeId($conn, 'Congé payé'); -$rttTypeId = getTypeId($conn, 'RTT'); - -$soldeReportInitial_CP = 0.0; -$soldeReportInitial_RTT = 0.0; - -$collaborateursResult = $conn->query("SELECT id FROM CollaborateurAD"); -if (!$collaborateursResult) { - error_log("Erreur récupération collaborateurs : ".$conn->error); - echo json_encode(['success' => false, 'message' => 'Erreur récupération collaborateurs']); - exit; -} - -while ($collab = $collaborateursResult->fetch_assoc()) { - $collabId = (int)$collab['id']; - - if ($cpTypeId !== null) { - $existsStmt = $conn->prepare("SELECT Id FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?"); - $existsStmt->bind_param("iii", $collabId, $cpTypeId, $yearNMinus1); - $existsStmt->execute(); - $existsStmt->store_result(); - if ($existsStmt->num_rows === 0) { - $insertStmt = $conn->prepare("INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Solde, Total, SoldeReporte) VALUES (?, ?, ?, ?, ?, ?)"); - $insertStmt->bind_param("iiiddd", $collabId, $cpTypeId, $yearNMinus1, $soldeReportInitial_CP, $soldeReportInitial_CP, $soldeReportInitial_CP); - if (!$insertStmt->execute()) { - error_log("Erreur insertion CP N-1 collaborateur $collabId : ".$insertStmt->error); - } - $insertStmt->close(); - } - $existsStmt->close(); - } - - if ($rttTypeId !== null) { - $existsStmt = $conn->prepare("SELECT Id FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?"); - $existsStmt->bind_param("iii", $collabId, $rttTypeId, $yearNMinus1); - $existsStmt->execute(); - $existsStmt->store_result(); - if ($existsStmt->num_rows === 0) { - $insertStmt = $conn->prepare("INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Solde, Total, SoldeReporte) VALUES (?, ?, ?, ?, ?, ?)"); - $insertStmt->bind_param("iiiddd", $collabId, $rttTypeId, $yearNMinus1, $soldeReportInitial_RTT, $soldeReportInitial_RTT, $soldeReportInitial_RTT); - if (!$insertStmt->execute()) { - error_log("Erreur insertion RTT N-1 collaborateur $collabId : ".$insertStmt->error); - } - $insertStmt->close(); - } - $existsStmt->close(); - } -} - -$cpStart = new DateTime("$yearCurrent-06-01"); -$cpEnd = new DateTime(($yearCurrent + 1) . "-05-31"); -$rttStart = new DateTime("$yearCurrent-01-01"); -$rttEnd = new DateTime("$yearCurrent-12-31"); - -$cpAnnualDays = 25; -$rttAnnualDays = 10; - -$cpPeriodDays = $cpEnd->diff($cpStart)->days + 1; -$rttPeriodDays = $rttEnd->diff($rttStart)->days + 1; - -$cpDailyIncrement = $cpAnnualDays / $cpPeriodDays; -$rttDailyIncrement = $rttAnnualDays / $rttPeriodDays; - -error_log("Incrément CP jour : $cpDailyIncrement"); -error_log("Incrément RTT jour : $rttDailyIncrement"); - -if ($today >= $cpStart && $today <= $cpEnd && $cpTypeId !== null) { - $exerciseYear = (int)$cpStart->format('Y'); - $stmt = $conn->prepare("UPDATE CompteurConges SET Solde = Solde + ? WHERE TypeCongeId = ? AND Annee = ?"); - $stmt->bind_param("dii", $cpDailyIncrement, $cpTypeId, $exerciseYear); - if (!$stmt->execute()) { - error_log("Erreur incrément CP N : ".$stmt->error); - } - $stmt->close(); -} - -if ($today >= $rttStart && $today <= $rttEnd && $rttTypeId !== null) { - $exerciseYear = $yearCurrent; - $stmt = $conn->prepare("UPDATE CompteurConges SET Solde = Solde + ? WHERE TypeCongeId = ? AND Annee = ?"); - $stmt->bind_param("dii", $rttDailyIncrement, $rttTypeId, $exerciseYear); - if (!$stmt->execute()) { - error_log("Erreur incrément RTT N : ".$stmt->error); - } - $stmt->close(); -} - -// Récupérer les compteurs actuels de l'utilisateur demandé en GET -$userId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : 0; -$data = []; - -if ($userId > 0) { - $stmt = $conn->prepare( - "SELECT tc.Nom, cc.Annee, cc.Solde, cc.Total, cc.SoldeReporte - FROM CompteurConges cc - JOIN TypeConge tc ON cc.TypeCongeId = tc.Id - WHERE cc.CollaborateurADId = ?" - ); - $stmt->bind_param("i", $userId); - $stmt->execute(); - $result = $stmt->get_result(); - - while ($row = $result->fetch_assoc()) { - $data[$row['Nom']] = [ - 'Annee' => $row['Annee'], - 'Solde' => (float)$row['Solde'], - 'Total' => (float)$row['Total'], - 'SoldeReporte' => (float)$row['SoldeReporte'], - ]; - } - $stmt->close(); -} - -$conn->close(); - -echo json_encode([ - 'success' => true, - 'message' => 'Compteurs mis à jour', - 'counters' => $data, -]); -exit; diff --git a/project/public/php/getNotifications.php b/project/public/php/getNotifications.php deleted file mode 100644 index 6854373..0000000 --- a/project/public/php/getNotifications.php +++ /dev/null @@ -1,75 +0,0 @@ -connect_error) { - http_response_code(500); - echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); - exit; -} - -$user_id = isset($_GET['user_id']) ? intval($_GET['user_id']) : 0; - -if ($user_id <= 0) { - http_response_code(400); - echo json_encode(["success" => false, "message" => "Paramètre user_id manquant ou invalide"]); - exit; -} - -// Récupérer les notifications non lues ou récentes (ex: dernières 30 j) -$query = " -SELECT Id, Titre, Message, Type, DemandeCongeId, DateCreation, lu -FROM Notifications -WHERE CollaborateurADId = ? -ORDER BY DateCreation DESC -LIMIT 50 -"; - -$stmt = $conn->prepare($query); -if (!$stmt) { - http_response_code(500); - echo json_encode(["success" => false, "message" => "Erreur préparation requête"]); - exit; -} - -$stmt->bind_param('i', $user_id); // ✅ correction ici -$stmt->execute(); -$result = $stmt->get_result(); - -$notifications = []; -while ($row = $result->fetch_assoc()) { - $notifications[] = [ - "Id" => intval($row['Id']), - "Titre" => $row['Titre'], - "Message" => $row['Message'], - "Type" => $row['Type'], - "DemandeCongeId" => intval($row['DemandeCongeId']), - "DateCreation" => $row['DateCreation'], - "lu" => intval($row['lu']) === 1, - ]; -} - -$stmt->close(); -$conn->close(); - -echo json_encode([ - "success" => true, - "notifications" => $notifications -]); diff --git a/project/public/php/getPendingRequests.php b/project/public/php/getPendingRequests.php deleted file mode 100644 index 92a7941..0000000 --- a/project/public/php/getPendingRequests.php +++ /dev/null @@ -1,159 +0,0 @@ -connect_error) { - error_log("Erreur connexion DB getPendingRequests: " . $conn->connect_error); - echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); - exit(); -} - -$managerId = $_GET['manager_id'] ?? null; - -if ($managerId === null) { - echo json_encode(["success" => false, "message" => "ID manager manquant"]); - exit(); -} - -error_log("getPendingRequests - Manager ID: $managerId"); - -// Fonction pour calculer les jours ouvrés -function getWorkingDays($startDate, $endDate) { - $workingDays = 0; - $current = new DateTime($startDate); - $end = new DateTime($endDate); - - while ($current <= $end) { - $dayOfWeek = (int)$current->format('N'); - if ($dayOfWeek < 6) { - $workingDays++; - } - $current->modify('+1 day'); - } - return $workingDays; -} - -try { - // Récupérer le service du manager (table CollaborateurAD) - $queryManagerService = "SELECT ServiceId FROM CollaborateurAD WHERE id = ?"; - $stmtManager = $conn->prepare($queryManagerService); - $stmtManager->bind_param("i", $managerId); - $stmtManager->execute(); - $resultManager = $stmtManager->get_result(); - - if ($managerRow = $resultManager->fetch_assoc()) { - $serviceId = $managerRow['ServiceId']; - error_log("getPendingRequests - Service ID du manager: $serviceId"); - - // Récupérer les demandes en attente (multi-types) - $queryRequests = " - 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 - "; - - $stmtRequests = $conn->prepare($queryRequests); - $stmtRequests->bind_param("ii", $serviceId, $managerId); - $stmtRequests->execute(); - $resultRequests = $stmtRequests->get_result(); - - $requests = []; - while ($row = $resultRequests->fetch_assoc()) { - $workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']); - - $startDate = new DateTime($row['DateDebut']); - $endDate = new DateTime($row['DateFin']); - $submittedDate = new DateTime($row['DateDemande']); - - if ($row['DateDebut'] === $row['DateFin']) { - $dateDisplay = $startDate->format('d/m/Y'); - } else { - $dateDisplay = $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'); - } - - $requests[] = [ - 'id' => (int)$row['Id'], - 'employee_id' => (int)$row['CollaborateurADId'], - 'employee_name' => $row['employee_name'], - 'employee_email' => $row['employee_email'], - 'type' => $row['types'], // ex: "Congé payé, RTT" - 'start_date' => $row['DateDebut'], - 'end_date' => $row['DateFin'], - 'date_display' => $dateDisplay, - 'days' => $workingDays, - 'status' => $row['Statut'], - 'reason' => $row['Commentaire'] ?: '', - 'submitted_at' => $row['DateDemande'], - 'submitted_display' => $submittedDate->format('d/m/Y') - ]; - } - - error_log("getPendingRequests - Demandes en attente trouvées: " . count($requests)); - - echo json_encode([ - "success" => true, - "message" => "Demandes en attente récupérées avec succès", - "requests" => $requests, - "service_id" => $serviceId - ]); - - $stmtRequests->close(); - } else { - error_log("getPendingRequests - Manager non trouvé: $managerId"); - echo json_encode([ - "success" => false, - "message" => "Manager non trouvé" - ]); - } - - $stmtManager->close(); - -} catch (Exception $e) { - error_log("Erreur getPendingRequests: " . $e->getMessage()); - echo json_encode([ - "success" => false, - "message" => "Erreur lors de la récupération des demandes: " . $e->getMessage() - ]); -} - -$conn->close(); -?> diff --git a/project/public/php/getRequests.php b/project/public/php/getRequests.php deleted file mode 100644 index b5a02d2..0000000 --- a/project/public/php/getRequests.php +++ /dev/null @@ -1,133 +0,0 @@ -connect_error) { - echo json_encode(["success" => false, "message" => "Erreur connexion DB: " . $conn->connect_error]); - exit(); -} - -// Récup paramètre -$userId = $_GET['user_id'] ?? null; -if (!$userId) { - echo json_encode(["success" => false, "message" => "ID utilisateur manquant"]); - exit(); -} - -// Fonction jours ouvrés -function getWorkingDays($startDate, $endDate) { - $workingDays = 0; - $current = new DateTime($startDate); - $end = new DateTime($endDate); - while ($current <= $end) { - $dayOfWeek = (int)$current->format('N'); - if ($dayOfWeek < 6) { - $workingDays++; - } - $current->modify('+1 day'); - } - return $workingDays; -} - -try { - // Requête multi-types - $query = " - SELECT - dc.Id, - dc.DateDebut, - dc.DateFin, - dc.Statut, - dc.DateDemande, - dc.Commentaire, - dc.Validateur, - dc.DocumentJoint, - GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS TypeConges - FROM DemandeConge dc - JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) - WHERE (dc.EmployeeId = ? OR dc.CollaborateurADId = ?) - GROUP BY - dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, - dc.Commentaire, dc.Validateur, dc.DocumentJoint - ORDER BY dc.DateDemande DESC -"; - - $stmt = $conn->prepare($query); - if (!$stmt) { - throw new Exception("Erreur préparation SQL : " . $conn->error); - } - - $stmt->bind_param("ii", $userId, $userId); - $stmt->execute(); - $result = $stmt->get_result(); - - $requests = []; - while ($row = $result->fetch_assoc()) { - $workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']); - - // Format dates - $startDate = new DateTime($row['DateDebut']); - $endDate = new DateTime($row['DateFin']); - $submittedDate = new DateTime($row['DateDemande']); - - $dateDisplay = ($row['DateDebut'] === $row['DateFin']) - ? $startDate->format('d/m/Y') - : $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'); - - // Lien fichier si congé maladie - $fileUrl = null; - if (strpos($row['TypeConges'], 'Congé maladie') !== false && !empty($row['DocumentJoint'])) { - $fileUrl = 'http://localhost/GTA/project/uploads/' . basename($row['DocumentJoint']); - } - - $requests[] = [ - 'id' => (int)$row['Id'], - 'type' => $row['TypeConges'], // ex: "Congé payé, RTT" - 'startDate' => $row['DateDebut'], - 'endDate' => $row['DateFin'], - 'dateDisplay' => $dateDisplay, - 'days' => $workingDays, - 'status' => $row['Statut'], - 'reason' => $row['Commentaire'] ?: 'Aucun commentaire', - 'submittedAt' => $row['DateDemande'], - 'submittedDisplay' => $submittedDate->format('d/m/Y'), - 'validator' => $row['Validateur'] ?: null, - 'fileUrl' => $fileUrl - ]; - } - - echo json_encode([ - "success" => true, - "message" => "Demandes récupérées avec succès", - "requests" => $requests, - "total" => count($requests) - ]); - -} catch (Exception $e) { - echo json_encode([ - "success" => false, - "message" => "Erreur: " . $e->getMessage() - ]); -} - -$conn->close(); diff --git a/project/public/php/getTeamLeaves.php b/project/public/php/getTeamLeaves.php deleted file mode 100644 index f10b181..0000000 --- a/project/public/php/getTeamLeaves.php +++ /dev/null @@ -1,228 +0,0 @@ -connect_error) { - echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); - exit(); -} - -// On récupère le rôle directement depuis la requête GET pour la logique PHP -$userId = $_GET['user_id'] ?? null; -$role = strtolower($_GET['role'] ?? 'collaborateur'); - -if ($userId === null) { - echo json_encode(["success" => false, "message" => "ID utilisateur manquant"]); - exit(); -} - -try { - // 🔹 Infos utilisateur - $queryUser = " - SELECT ca.ServiceId, sa.CampusId, sa.SocieteId, - s.Nom as service_nom, c.Nom as campus_nom, so.Nom as societe_nom - FROM CollaborateurAD ca - JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId - JOIN Services s ON ca.ServiceId = s.Id - JOIN Campus c ON sa.CampusId = c.Id - JOIN Societe so ON sa.SocieteId = so.Id - WHERE ca.id = ? - LIMIT 1 - "; - $stmtUser = $conn->prepare($queryUser); - $stmtUser->bind_param("i", $userId); - $stmtUser->execute(); - $resultUser = $stmtUser->get_result(); - - if (!$userRow = $resultUser->fetch_assoc()) { - echo json_encode(["success" => false, "message" => "Collaborateur non trouvé"]); - exit(); - } - - $serviceId = $userRow['ServiceId']; - $campusId = $userRow['CampusId']; - $societeId = $userRow['SocieteId']; - - // ------------------------- - // 🔹 Construire la requête selon le rôle - // ------------------------- - switch ($role) { - case 'president': - case 'rh': - $queryLeaves = " - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date, - CONCAT(ca.prenom, ' ', ca.nom) as employee_name, - tc.Nom as type, - tc.CouleurHex as color, - s.Nom as service_nom, - c.Nom as campus_nom, - so.Nom as societe_nom - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - JOIN TypeConge tc ON dc.TypeCongeId = tc.Id - JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId - JOIN Services s ON sa.ServiceId = s.Id - JOIN Campus c ON sa.CampusId = c.Id - JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI - WHERE dc.Statut = 'Validée' - ORDER BY c.Nom, so.Nom, s.Nom, dc.DateDebut ASC - "; - $stmtLeaves = $conn->prepare($queryLeaves); - break; - - case 'directeur de campus': - $queryLeaves = " - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date, - CONCAT(ca.prenom, ' ', ca.nom) as employee_name, - tc.Nom as type, - tc.CouleurHex as color, - s.Nom as service_nom, - so.Nom as societe_nom, - c.Nom as campus_nom - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - JOIN TypeConge tc ON dc.TypeCongeId = tc.Id - JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId - JOIN Services s ON sa.ServiceId = s.Id - JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI - JOIN Campus c ON sa.CampusId = c.Id - WHERE sa.CampusId = ? - AND dc.Statut = 'Validée' - ORDER BY so.Nom, s.Nom, dc.DateDebut ASC - "; - $stmtLeaves = $conn->prepare($queryLeaves); - $stmtLeaves->bind_param("i", $campusId); - break; - - case 'validateur': - case 'collaborateur': - default: - $queryLeaves = " - SELECT - DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date, - DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date, - CONCAT(ca.prenom, ' ', ca.nom) as employee_name, - tc.Nom as type, - tc.CouleurHex as color, - s.Nom as service_nom, - c.Nom as campus_nom, - so.Nom as societe_nom - FROM DemandeConge dc - JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id - JOIN TypeConge tc ON dc.TypeCongeId = tc.Id - JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId - JOIN Services s ON sa.ServiceId = s.Id - JOIN Campus c ON sa.CampusId = c.Id - JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI - WHERE ca.ServiceId = ? - AND sa.CampusId = ? - AND dc.Statut = 'Validée' - AND dc.DateFin >= CURDATE() - INTERVAL 30 DAY - ORDER BY dc.DateDebut ASC - "; - $stmtLeaves = $conn->prepare($queryLeaves); - $stmtLeaves->bind_param("ii", $serviceId, $campusId); - } - - $stmtLeaves->execute(); - $resultLeaves = $stmtLeaves->get_result(); - - $leaves = []; - while ($row = $resultLeaves->fetch_assoc()) { - $leaves[] = [ - 'start_date' => $row['start_date'], - 'end_date' => $row['end_date'], - 'employee_name' => $row['employee_name'], - 'type' => $row['type'], - 'color' => $row['color'] ?? '#3B82F6', - 'service_nom' => $row['service_nom'], - 'campus_nom' => $row['campus_nom'] ?? null, - 'societe_nom' => $row['societe_nom'] ?? null - ]; - } - - // ------------------------- - // 🔹 Construire les filtres dynamiques - // ------------------------- - $filters = []; - - if (in_array($role, ['collaborateur', 'validateur'])) { - $queryEmployees = " - SELECT CONCAT(ca.prenom, ' ', ca.nom) as employee_name - FROM CollaborateurAD ca - JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId - WHERE ca.ServiceId = ? - AND sa.CampusId = ? - ORDER BY ca.prenom, ca.nom - "; - $stmtEmployees = $conn->prepare($queryEmployees); - $stmtEmployees->bind_param("ii", $serviceId, $campusId); - $stmtEmployees->execute(); - $resultEmployees = $stmtEmployees->get_result(); - - $employees = []; - while ($row = $resultEmployees->fetch_assoc()) { - $employees[] = $row['employee_name']; - } - $filters['employees'] = $employees; - $stmtEmployees->close(); - - } elseif ($role === 'directeur de campus') { - // Pour le directeur, les filtres se basent sur les congés de son campus - $filters['societes'] = array_values(array_unique(array_column($leaves, 'societe_nom'))); - $filters['services'] = array_values(array_unique(array_column($leaves, 'service_nom'))); - - } elseif (in_array($role, ['president', 'rh'])) { - // 🔹 Récupérer tous les campus, sociétés, services de manière unique - $filters['campus'] = []; - $filters['societes'] = []; - $filters['services'] = []; - - $result = $conn->query("SELECT DISTINCT Nom as campus_nom FROM Campus ORDER BY campus_nom"); - while($row = $result->fetch_assoc()) $filters['campus'][] = $row['campus_nom']; - - $result = $conn->query("SELECT DISTINCT Nom as societe_nom FROM Societe ORDER BY societe_nom"); - while($row = $result->fetch_assoc()) $filters['societes'][] = $row['societe_nom']; - - $result = $conn->query("SELECT DISTINCT Nom as service_nom FROM Services ORDER BY service_nom"); - while($row = $result->fetch_assoc()) $filters['services'][] = $row['service_nom']; -} - - echo json_encode([ - "success" => true, - "role" => $role, - "leaves" => $leaves, - "filters" => $filters - ]); - - $stmtLeaves->close(); - $stmtUser->close(); - -} catch (Exception $e) { - echo json_encode(["success" => false, "message" => "Erreur: " . $e->getMessage()]); -} - -$conn->close(); -?> \ No newline at end of file diff --git a/project/public/php/getTeamMembers.php b/project/public/php/getTeamMembers.php deleted file mode 100644 index e6a910b..0000000 --- a/project/public/php/getTeamMembers.php +++ /dev/null @@ -1,116 +0,0 @@ -connect_error) { - error_log("Erreur connexion DB getTeamMembersAD: " . $conn->connect_error); - echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); - exit(); -} - -$managerId = $_GET['manager_id'] ?? null; - -if ($managerId === null) { - echo json_encode(["success" => false, "message" => "ID manager manquant"]); - exit(); -} - -error_log("getTeamMembersAD - Manager ID: $managerId"); - -try { - // 🔹 1. Récupérer le ServiceId du manager - $queryManagerService = "SELECT ServiceId FROM CollaborateurAD WHERE id = ?"; - $stmtManager = $conn->prepare($queryManagerService); - $stmtManager->bind_param("i", $managerId); - $stmtManager->execute(); - $resultManager = $stmtManager->get_result(); - - if ($managerRow = $resultManager->fetch_assoc()) { - $serviceId = $managerRow['ServiceId']; - error_log("getTeamMembersAD - ServiceId du manager: $serviceId"); - - // 🔹 2. Récupérer tous les collaborateurs du même service (sauf le manager) - $queryTeam = " - SELECT - c.id, - c.nom, - c.prenom, - c.email, - c.role, - - s.Nom as service_name - FROM CollaborateurAD c - JOIN Services s ON c.ServiceId = s.Id - WHERE c.ServiceId = ? AND c.id != ? - ORDER BY c.prenom, c.nom - "; - - $stmtTeam = $conn->prepare($queryTeam); - $stmtTeam->bind_param("ii", $serviceId, $managerId); - $stmtTeam->execute(); - $resultTeam = $stmtTeam->get_result(); - - $teamMembers = []; - while ($row = $resultTeam->fetch_assoc()) { - $teamMembers[] = [ - 'id' => (int)$row['id'], - 'nom' => $row['nom'], - 'prenom' => $row['prenom'], - 'email' => $row['email'], - 'role' => $row['role'], - - 'service_name' => $row['service_name'] - ]; - } - - error_log("getTeamMembersAD - Membres trouvés: " . count($teamMembers)); - - echo json_encode([ - "success" => true, - "message" => "Équipe récupérée avec succès", - "team_members" => $teamMembers, - "service_id" => $serviceId - ]); - - $stmtTeam->close(); - } else { - error_log("getTeamMembersAD - Manager non trouvé: $managerId"); - echo json_encode([ - "success" => false, - "message" => "Manager non trouvé" - ]); - } - - $stmtManager->close(); - -} catch (Exception $e) { - error_log("Erreur getTeamMembersAD: " . $e->getMessage()); - echo json_encode([ - "success" => false, - "message" => "Erreur lors de la récupération de l'équipe: " . $e->getMessage() - ]); -} - -$conn->close(); -?> diff --git a/project/public/php/initial-sync.php b/project/public/php/initial-sync.php deleted file mode 100644 index ba8857c..0000000 --- a/project/public/php/initial-sync.php +++ /dev/null @@ -1,104 +0,0 @@ -connect_error) { - die(json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error])); -} - -$tenantId = "9840a2a0-6ae1-4688-b03d-d2ec291be0f9"; -$clientId = "4bb4cc24-bac3-427c-b02c-5d14fc67b561"; -$clientSecret = "ViC8Q~n4F5YweE18wjS0kfhp3kHh6LB2gZ76_b4R"; -$scope = "https://graph.microsoft.com/.default"; - -$url = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"; -$data = [ - "grant_type" => "client_credentials", - "client_id" => $clientId, - "client_secret" => $clientSecret, - "scope" => $scope -]; - -$ch = curl_init(); -curl_setopt($ch, CURLOPT_URL, $url); -curl_setopt($ch, CURLOPT_POST, true); -curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -$result = curl_exec($ch); -curl_close($ch); - -$tokenData = json_decode($result, true); -$accessToken = $tokenData["access_token"] ?? ""; -if (!$accessToken) { - die(json_encode(["success" => false, "message" => "Impossible d'obtenir un token Microsoft", "details" => $tokenData])); -} - -// --- ID du groupe cible (Ensup-Groupe) --- -$groupId = "c1ea877c-6bca-4f47-bfad-f223640813a0"; - -// --- Récupérer infos du groupe --- -$urlGroup = "https://graph.microsoft.com/v1.0/groups/$groupId?\$select=id,displayName,description,mail,createdDateTime"; -$ch = curl_init($urlGroup); -curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -$respGroup = curl_exec($ch); -curl_close($ch); - -$group = json_decode($respGroup, true); -if (!isset($group["id"])) { - die(json_encode(["success" => false, "message" => "Impossible de récupérer le groupe Ensup-Groupe"])); -} - -$displayName = $group["displayName"] ?? ""; - -// --- Récupérer les membres du groupe --- -$urlMembers = "https://graph.microsoft.com/v1.0/groups/$groupId/members?\$select=id,givenName,surname,mail,department,jobTitle"; -$ch = curl_init($urlMembers); -curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -$respMembers = curl_exec($ch); -curl_close($ch); - -$members = json_decode($respMembers, true)["value"] ?? []; - -$usersInserted = 0; -foreach ($members as $m) { - $entraUserId = $m["id"]; - $prenom = $m["givenName"] ?? ""; - $nom = $m["surname"] ?? ""; - $email = $m["mail"] ?? ""; - $service = $m["department"] ?? ""; - $description = $m["jobTitle"] ?? null; - if (!$email) continue; - - $stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, description, role) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE prenom=?, nom=?, email=?, service=?, description=?"); - if ($stmt) { - $role = "Collaborateur"; - $stmt->bind_param("ssssssssssss", - $entraUserId, $prenom, $nom, $email, $service, $description, $role, - $prenom, $nom, $email, $service, $description - ); - $stmt->execute(); - $usersInserted++; - } -} - -// --- Réponse finale --- -echo json_encode([ - "success" => true, - "message" => "Synchronisation terminée", - "groupe_sync" => $displayName, - "users_sync" => $usersInserted -]); - -$conn->close(); -?> diff --git a/project/public/php/login.php b/project/public/php/login.php deleted file mode 100644 index cbb2e1f..0000000 --- a/project/public/php/login.php +++ /dev/null @@ -1,152 +0,0 @@ -connect_error) { - die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error])); -} - -$data = json_decode(file_get_contents('php://input'), true); -$email = $data['email'] ?? ''; -$mot_de_passe = $data['mot_de_passe'] ?? ''; -$entraUserId = $data['entraUserId'] ?? ''; -$userPrincipalName = $data['userPrincipalName'] ?? ''; - -$headers = getallheaders(); -$accessToken = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : ''; - -// ====================================================== -// 1️⃣ Mode Azure AD (avec token + Entra) -// ====================================================== -if ($accessToken && $entraUserId) { - // Vérifier si utilisateur existe déjà dans CollaborateurAD - $stmt = $conn->prepare("SELECT * FROM CollaborateurAD WHERE entraUserId=? OR email=? LIMIT 1"); - $stmt->bind_param("ss", $entraUserId, $email); - $stmt->execute(); - $result = $stmt->get_result(); - - if ($result->num_rows === 0) { - echo json_encode(["success" => false, "message" => "Utilisateur non autorisé (pas dans l'annuaire)"]); - exit(); - } - $user = $result->fetch_assoc(); - - // Récupérer groupes de l’utilisateur via Graph - $ch = curl_init("https://graph.microsoft.com/v1.0/users/$userPrincipalName/memberOf?\$select=id"); - curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = curl_exec($ch); - curl_close($ch); - - $dataGraph = json_decode($response, true); - $userGroups = []; - if (isset($dataGraph['value'])) { - foreach ($dataGraph['value'] as $g) { - if (isset($g['id'])) { - $userGroups[] = $g['id']; - } - } - } - - // Vérifier si au moins un groupe est autorisé - $res = $conn->query("SELECT Id FROM EntraGroups WHERE IsActive=1"); - $allowedGroups = []; - while ($row = $res->fetch_assoc()) { - $allowedGroups[] = $row['Id']; - } - - $authorized = count(array_intersect($userGroups, $allowedGroups)) > 0; - - if ($authorized) { - echo json_encode([ - "success" => true, - "message" => "Connexion réussie via Azure AD", - "user" => [ - "id" => $user['id'], - "prenom" => $user['prenom'], - "nom" => $user['nom'], - "email" => $user['email'], - "role" => $user['role'], - "service" => $user['service'] - ] - ]); - } else { - echo json_encode(["success" => false, "message" => "Utilisateur non autorisé - pas dans un groupe actif"]); - } - - $conn->close(); - exit(); -} - -// ====================================================== -// 2️⃣ Mode local (login/password → Users) -// ====================================================== -if ($email && $mot_de_passe) { - $query = " - SELECT - u.ID, - u.Prenom, - u.Nom, - u.Email, - u.Role, - u.ServiceId, - s.Nom AS ServiceNom - FROM Users u - LEFT JOIN Services s ON u.ServiceId = s.Id - WHERE u.Email = ? AND u.MDP = ? - "; - - $stmt = $conn->prepare($query); - - if ($stmt === false) { - die(json_encode(["success" => false, "message" => "Erreur de préparation : " . $conn->error])); - } - - $stmt->bind_param("ss", $email, $mot_de_passe); - $stmt->execute(); - $result = $stmt->get_result(); - - if ($result->num_rows === 1) { - $user = $result->fetch_assoc(); - - echo json_encode([ - "success" => true, - "message" => "Connexion réussie (mode local)", - "user" => [ - "id" => $user['ID'], - "prenom" => $user['Prenom'], - "nom" => $user['Nom'], - "email" => $user['Email'], - "role" => $user['Role'], - "service" => $user['ServiceNom'] ?? 'Non défini' - ] - ]); - } else { - echo json_encode(["success" => false, "message" => "Identifiants incorrects (mode local)"]); - } - - $stmt->close(); - $conn->close(); - exit(); -} - -// ====================================================== -// 3️⃣ Aucun mode ne correspond -// ====================================================== -echo json_encode(["success" => false, "message" => "Aucune méthode de connexion fournie"]); -$conn->close(); -?> diff --git a/project/public/php/manualResetCounters.php b/project/public/php/manualResetCounters.php deleted file mode 100644 index bd737d0..0000000 --- a/project/public/php/manualResetCounters.php +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - Réinitialisation des Compteurs - - - -
-

🔄 Réinitialisation des Compteurs de Congés

- -
-

⚠️ ATTENTION

-

Cette opération va réinitialiser TOUS les compteurs de congés selon les règles suivantes :

-
    -
  • Congés Payés : 25 jours (exercice du 01/06 au 31/05)
  • -
  • RTT : 10 jours pour 2025 (exercice du 01/01 au 31/12)
  • -
  • Congés Maladie : 0 jours (remise à zéro)
  • -
-

Cette action est irréversible !

-
- - [ - 'method' => 'POST', - 'header' => 'Content-Type: application/json', - 'content' => json_encode(['manual_reset' => true]) - ] - ]); - - $result = file_get_contents($resetUrl, false, $context); - $data = json_decode($result, true); - - if ($data && $data['success']) { - echo '
'; - echo '

✅ Réinitialisation réussie !

'; - echo '

Employés mis à jour : ' . $data['details']['employees_updated'] . '

'; - echo '

Exercice CP : ' . $data['details']['leave_year'] . '

'; - echo '

Année RTT : ' . $data['details']['rtt_year'] . '

'; - echo '

Date de réinitialisation : ' . $data['details']['reset_date'] . '

'; - - if (!empty($data['log'])) { - echo '
Voir le détail
';
-                    foreach ($data['log'] as $logLine) {
-                        echo htmlspecialchars($logLine) . "\n";
-                    }
-                    echo '
'; - } - echo '
'; - } else { - echo '
'; - echo '

❌ Erreur lors de la réinitialisation

'; - echo '

' . ($data['message'] ?? 'Erreur inconnue') . '

'; - echo '
'; - } - } - ?> - -
-

- -

- -
- -
- -

📋 Informations sur les exercices

- format('Y'); - $currentMonth = (int)$currentDate->format('m'); - - // Calcul exercice CP - $leaveYear = ($currentMonth < 6) ? $currentYear - 1 : $currentYear; - $leaveYearEnd = $leaveYear + 1; - - echo "

Exercice Congés Payés actuel : du 01/06/$leaveYear au 31/05/$leaveYearEnd

"; - echo "

Exercice RTT actuel : du 01/01/$currentYear au 31/12/$currentYear

"; - echo "

Date actuelle : " . $currentDate->format('d/m/Y H:i:s') . "

"; - ?> - -

🔗 Actions rapides

-

- - - -

-
- - \ No newline at end of file diff --git a/project/public/php/markNotificationRead.php b/project/public/php/markNotificationRead.php deleted file mode 100644 index b4b8158..0000000 --- a/project/public/php/markNotificationRead.php +++ /dev/null @@ -1,62 +0,0 @@ -connect_error) { - http_response_code(500); - echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); - exit; -} - -// Récupération données JSON POST -$postData = json_decode(file_get_contents("php://input"), true); -if (!isset($postData['notificationId'])) { - http_response_code(400); - echo json_encode(["success" => false, "message" => "Paramètre notificationId manquant"]); - exit; -} -$notificationId = intval($postData['notificationId']); -if ($notificationId <= 0) { - http_response_code(400); - echo json_encode(["success" => false, "message" => "ID notification invalide"]); - exit; -} - -// Mettre à jour notification lu = 1 -$query = "UPDATE Notifications SET lu = 1 WHERE Id = ?"; -$stmt = $conn->prepare($query); -if (!$stmt) { - http_response_code(500); - echo json_encode(["success" => false, "message" => "Erreur préparation requête"]); - exit; -} -$stmt->bind_param("i", $notificationId); - -if ($stmt->execute()) { - echo json_encode(["success" => true, "message" => "Notification marquée comme lue"]); -} else { - http_response_code(500); - echo json_encode(["success" => false, "message" => "Erreur lors de la mise à jour"]); -} - -$stmt->close(); -$conn->close(); diff --git a/project/public/php/resetLeaveCounters.php b/project/public/php/resetLeaveCounters.php deleted file mode 100644 index 60e4cd9..0000000 --- a/project/public/php/resetLeaveCounters.php +++ /dev/null @@ -1,228 +0,0 @@ -connect_error) { - error_log("Erreur connexion DB reset: " . $conn->connect_error); - echo json_encode([ - "success" => false, - "message" => "Erreur de connexion à la base de données : " . $conn->connect_error - ]); - exit(); -} - -// Log de debug -error_log("Reset counters - Début du script"); - -// Fonction pour déterminer l'exercice des congés payés (01/06 au 31/05) -function getLeaveYear($date = null) { - if ($date === null) { - $date = new DateTime(); - } else { - $date = new DateTime($date); - } - - $currentYear = (int)$date->format('Y'); - $currentMonth = (int)$date->format('m'); - - // Si on est avant le 1er juin, l'exercice a commencé l'année précédente - if ($currentMonth < 6) { - return $currentYear - 1; - } - // Si on est le 1er juin ou après, l'exercice a commencé cette année - return $currentYear; -} - -// Fonction pour déterminer l'année RTT (01/01 au 31/12) -function getRTTYear($date = null) { - if ($date === null) { - $date = new DateTime(); - } else { - $date = new DateTime($date); - } - - return (int)$date->format('Y'); -} - -try { - $conn->begin_transaction(); - - $currentDate = new DateTime(); - $leaveYear = getLeaveYear(); - $rttYear = getRTTYear(); - - error_log("Reset counters - Exercice CP: $leaveYear, RTT: $rttYear"); - - $resetLog = []; - - // 1. Récupérer tous les employés depuis la table Users - $queryEmployees = "SELECT ID FROM Users"; - $resultEmployees = $conn->query($queryEmployees); - - if (!$resultEmployees) { - throw new Exception("Erreur lors de la récupération des employés : " . $conn->error); - } - - error_log("Reset counters - Nombre d'employés trouvés: " . $resultEmployees->num_rows); - - // 2. Récupérer les IDs des types de congés - $queryTypes = "SELECT Id, Nom FROM TypeConge WHERE Nom IN ('Congé payé', 'RTT', 'Congé maladie')"; - $resultTypes = $conn->query($queryTypes); - - $typeIds = []; - while ($row = $resultTypes->fetch_assoc()) { - $typeIds[$row['Nom']] = $row['Id']; - } - - error_log("Reset counters - Types trouvés: " . print_r($typeIds, true)); - - if (count($typeIds) < 3) { - throw new Exception("Types de congés manquants dans la base de données"); - } - - // 3. Pour chaque employé, réinitialiser les compteurs - $employeesUpdated = 0; - while ($employee = $resultEmployees->fetch_assoc()) { - $employeeId = $employee['ID']; - - error_log("Reset counters - Traitement employé: $employeeId"); - - // CONGÉS PAYÉS - Exercice du 01/06 au 31/05 (25 jours) - $queryUpdateCP = " - INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total) - VALUES (?, ?, ?, 25, 25) - ON DUPLICATE KEY UPDATE - Solde = 25, - Total = 25 - "; - $stmtCP = $conn->prepare($queryUpdateCP); - if (!$stmtCP) { - throw new Exception("Erreur préparation CP: " . $conn->error); - } - $stmtCP->bind_param("iii", $employeeId, $typeIds['Congé payé'], $leaveYear); - - if (!$stmtCP->execute()) { - throw new Exception("Erreur lors de la mise à jour des CP pour l'employé $employeeId : " . $stmtCP->error); - } - $stmtCP->close(); - - // RTT - Année civile du 01/01 au 31/12 - // Calcul du nombre de RTT selon l'année - $rttCount = 10; // Par défaut 10 pour 2025 - if ($rttYear == 2024) { - $rttCount = 8; // Exemple pour 2024 - } elseif ($rttYear >= 2025) { - $rttCount = 10; // 10 pour 2025 et après - } - - $queryUpdateRTT = " - INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total) - VALUES (?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - Solde = ?, - Total = ? - "; - $stmtRTT = $conn->prepare($queryUpdateRTT); - if (!$stmtRTT) { - throw new Exception("Erreur préparation RTT: " . $conn->error); - } - $stmtRTT->bind_param("iiiiiii", $employeeId, $typeIds['RTT'], $rttYear, $rttCount, $rttCount, $rttCount, $rttCount); - - if (!$stmtRTT->execute()) { - throw new Exception("Erreur lors de la mise à jour des RTT pour l'employé $employeeId : " . $stmtRTT->error); - } - $stmtRTT->close(); - - // CONGÉ MALADIE - Réinitialiser à 0 (pas de limite) - $queryUpdateABS = " - INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total) - VALUES (?, ?, ?, 0, 0) - ON DUPLICATE KEY UPDATE - Solde = 0, - Total = 0 - "; - $stmtABS = $conn->prepare($queryUpdateABS); - if (!$stmtABS) { - throw new Exception("Erreur préparation ABS: " . $conn->error); - } - $stmtABS->bind_param("iii", $employeeId, $typeIds['Congé maladie'], $rttYear); - - if (!$stmtABS->execute()) { - throw new Exception("Erreur lors de la mise à jour des ABS pour l'employé $employeeId : " . $stmtABS->error); - } - $stmtABS->close(); - - $resetLog[] = "Employé $employeeId : CP=$leaveYear (25j), RTT=$rttYear ({$rttCount}j), ABS=$rttYear (0j)"; - $employeesUpdated++; - } - - error_log("Reset counters - Employés mis à jour: $employeesUpdated"); - - // 4. Log de la réinitialisation - $logEntry = " - === RÉINITIALISATION DES COMPTEURS === - Date: " . $currentDate->format('Y-m-d H:i:s') . " - Exercice CP: $leaveYear (01/06/$leaveYear au 31/05/" . ($leaveYear + 1) . ") - Année RTT: $rttYear (01/01/$rttYear au 31/12/$rttYear) - Employés traités: $employeesUpdated - - Détails: - " . implode("\n ", $resetLog) . " - "; - - // Sauvegarder le log (optionnel - créer une table de logs si nécessaire) - error_log($logEntry, 3, "reset_counters.log"); - - $conn->commit(); - error_log("Reset counters - Transaction commitée avec succès"); - - echo json_encode([ - "success" => true, - "message" => "Compteurs réinitialisés avec succès", - "details" => [ - "employees_updated" => $employeesUpdated, - "leave_year" => $leaveYear, - "rtt_year" => $rttYear, - "cp_days" => 25, - "rtt_days" => $rttCount, - "reset_date" => $currentDate->format('Y-m-d H:i:s') - ], - "log" => $resetLog - ]); - -} catch (Exception $e) { - $conn->rollback(); - error_log("Erreur réinitialisation compteurs : " . $e->getMessage()); - - echo json_encode([ - "success" => false, - "message" => "Erreur lors de la réinitialisation : " . $e->getMessage() - ]); -} - -$conn->close(); -?> \ No newline at end of file diff --git a/project/public/php/submitLeaveRequest.php b/project/public/php/submitLeaveRequest.php deleted file mode 100644 index 5595b95..0000000 --- a/project/public/php/submitLeaveRequest.php +++ /dev/null @@ -1,293 +0,0 @@ -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); -} catch (PDOException $e) { - echo json_encode(["success"=>false,"message"=>"Erreur DB: ".$e->getMessage()]); - exit; -} - -// Lecture JSON brut -$input = file_get_contents('php://input'); -$data = json_decode($input, true); - -// 🔎 Debug pour vérifier ce qui arrive -error_log("📥 Payload reçu : " . print_r($data, true)); - -if (!$data) { - echo json_encode(["success"=>false,"message"=>"JSON invalide","raw"=>$input]); - exit; -} - -// Vérification des champs obligatoires -$required = ['DateDebut','DateFin','Repartition','NombreJours','Email','Nom']; -foreach ($required as $f) { - if (!array_key_exists($f, $data)) { - echo json_encode([ - "success"=>false, - "message"=>"Donnée manquante : $f", - "debug"=>$data - ]); - exit; - } -} - -$dateDebut = $data['DateDebut']; -$dateFin = $data['DateFin']; -$commentaire = $data['Commentaire'] ?? ''; -$numDays = (float)$data['NombreJours']; -$userEmail = $data['Email']; -$userName = $data['Nom']; -$statut = 'En attente'; -$currentDate = date('Y-m-d H:i:s'); - -// 🔎 Identifier si c'est un CollaborateurAD ou un User -$stmt = $pdo->prepare("SELECT id FROM CollaborateurAD WHERE email = :email LIMIT 1"); -$stmt->execute([':email'=>$userEmail]); -$collabAD = $stmt->fetch(PDO::FETCH_ASSOC); - -$isAD = false; -$employeeId = null; -$collaborateurId = null; - -if ($collabAD) { - $isAD = true; - $collaborateurId = (int)$collabAD['id']; -} else { - $stmt = $pdo->prepare("SELECT ID FROM Users WHERE Email = :email LIMIT 1"); - $stmt->execute([':email'=>$userEmail]); - $user = $stmt->fetch(PDO::FETCH_ASSOC); - - if (!$user) { - echo json_encode(["success"=>false,"message"=>"Aucun collaborateur trouvé pour $userEmail"]); - exit; - } - $employeeId = (int)$user['ID']; -} - -// 🔎 Résoudre les IDs des types de congés -$typeIds = []; -foreach ($data['Repartition'] as $rep) { - $code = $rep['TypeConge']; - switch ($code) { - case 'CP': $name = 'Congé payé'; break; - case 'RTT': $name = 'RTT'; break; - case 'ABS': $name = 'Congé maladie'; break; - default: $name = $code; break; - } - $s = $pdo->prepare("SELECT Id FROM TypeConge WHERE Nom = :nom LIMIT 1"); - $s->execute([':nom'=>$name]); - if ($r = $s->fetch(PDO::FETCH_ASSOC)) { - $typeIds[] = $r['Id']; - } -} -if (empty($typeIds)) { - echo json_encode(["success"=>false,"message"=>"Aucun type de congé valide"]); - exit; -} -$typeCongeIdCsv = implode(',', $typeIds); - -// ✅ Insertion DemandeConge -$sql = "INSERT INTO DemandeConge - (EmployeeId, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours) - VALUES (:eid, :cid, :dd, :df, :tc, :st, :cd, :com, :val, :nj)"; - -$stmt = $pdo->prepare($sql); -$stmt->execute([ - ':eid'=> $isAD ? 0 : $employeeId, - ':cid'=> $isAD ? $collaborateurId : null, - ':dd'=>$dateDebut, - ':df'=>$dateFin, - ':tc'=>$typeCongeIdCsv, - ':st'=>$statut, - ':cd'=>$currentDate, - ':com'=>$commentaire, - ':val'=>'', - ':nj'=>$numDays -]); - -$demandeId = $pdo->lastInsertId(); - -// ✅ Insertion DemandeCongeType -$sql = "INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours) VALUES (:did, :tid, :nj)"; -$stmt = $pdo->prepare($sql); - -foreach ($data['Repartition'] as $rep) { - $jours = (float)$rep['NombreJours']; - $code = $rep['TypeConge']; - switch ($code) { - case 'CP': $name = 'Congé payé'; break; - case 'RTT': $name = 'RTT'; break; - case 'ABS': $name = 'Congé maladie'; break; - default: $name = $code; break; - } - $s = $pdo->prepare("SELECT Id FROM TypeConge WHERE Nom = :nom LIMIT 1"); - $s->execute([':nom'=>$name]); - if ($r = $s->fetch(PDO::FETCH_ASSOC)) { - $stmt->execute([ - ':did'=>$demandeId, - ':tid'=>$r['Id'], - ':nj'=>$jours - ]); - } -} - -// ✅ Récupérer les validateurs selon hiérarchie -if ($isAD) { - $stmt = $pdo->prepare(" - SELECT c.email - FROM HierarchieValidationAD hv - JOIN CollaborateurAD c ON hv.SuperieurId = c.id - WHERE hv.CollaborateurId = :id - "); - $stmt->execute([':id'=>$collaborateurId]); -} else { - $stmt = $pdo->prepare(" - SELECT u.Email - FROM HierarchieValidation hv - JOIN Users u ON hv.SuperieurId = u.ID - WHERE hv.EmployeId = :id - "); - $stmt->execute([':id'=>$employeeId]); -} -$managers = $stmt->fetchAll(PDO::FETCH_COLUMN); - -# ============================================================= -# 📧 AUTH Microsoft Graph (client_credentials) -# ============================================================= -$tenantId = "9840a2a0-6ae1-4688-b03d-d2ec291be0f9"; -$clientId = "4bb4cc24-bac3-427c-b02c-5d14fc67b561"; -$clientSecret = "gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t"; - -$url = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"; - -$data = [ - "client_id" => $clientId, - "scope" => "https://graph.microsoft.com/.default", - "client_secret" => $clientSecret, - "grant_type" => "client_credentials" -]; - -$ch = curl_init($url); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -curl_setopt($ch, CURLOPT_POST, true); -curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); -curl_setopt($ch, CURLOPT_HTTPHEADER, [ - "Content-Type: application/x-www-form-urlencoded" -]); -$response = curl_exec($ch); -curl_close($ch); - -$tokenData = json_decode($response, true); -if (!isset($tokenData['access_token'])) { - echo json_encode(["success" => false, "message" => "Impossible de générer un token Graph", "debug"=>$tokenData]); - exit; -} -$accessToken = $tokenData['access_token']; - -# ============================================================= -# 📧 Fonction envoi mail -# ============================================================= -function sendMailGraph($accessToken, $fromEmail, $toEmail, $subject, $bodyHtml) { - $url = "https://graph.microsoft.com/v1.0/users/$fromEmail/sendMail"; - - $mailData = [ - "message" => [ - "subject" => $subject, - "body" => [ - "contentType" => "HTML", - "content" => $bodyHtml - ], - "toRecipients" => [ - ["emailAddress" => ["address" => $toEmail]] - ] - ], - "saveToSentItems" => "false" - ]; - - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - "Authorization: Bearer $accessToken", - "Content-Type: application/json" - ]); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($mailData)); - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode >= 200 && $httpCode < 300) { - return true; - } else { - error_log("❌ Erreur envoi mail: $response"); - return false; - } -} - -# ============================================================= -# 📧 Envoi automatique des emails -# ============================================================= -$fromEmail = "noreply@ensup.eu"; - -# Mail au collaborateur -sendMailGraph( - $accessToken, - $fromEmail, - $userEmail, - "Confirmation de votre demande de congés", - " - Bonjour {$userName},

- Votre demande du {$dateDebut} au {$dateFin} - ({$numDays} jour(s)) a bien été enregistrée.
- Elle est en attente de validation par votre manager.

- Merci. - " -); - -# Mail aux managers -foreach ($managers as $managerEmail) { - sendMailGraph( - $accessToken, - $fromEmail, - $managerEmail, - "Nouvelle demande de congé - {$userName}", - " - Bonjour,

- {$userName} a soumis une demande de congé :
- - Du {$dateDebut} au {$dateFin} ({$numDays} jour(s))
- - Commentaire : " . (!empty($commentaire) ? $commentaire : "Aucun") . "

- Merci de valider cette demande. - " - ); -} - -# ✅ Réponse finale -echo json_encode([ - "success"=>true, - "message"=>"Demande soumise", - "request_id"=>$demandeId, - "managers"=>$managers -]); diff --git a/project/public/php/sync-groups.php b/project/public/php/sync-groups.php deleted file mode 100644 index e69de29..0000000 diff --git a/project/public/php/validateRequest.php b/project/public/php/validateRequest.php deleted file mode 100644 index 74124ba..0000000 --- a/project/public/php/validateRequest.php +++ /dev/null @@ -1,157 +0,0 @@ -connect_error) { - echo json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error]); - exit(); -} - -// Lecture du JSON envoyé -$input = file_get_contents('php://input'); -$data = json_decode($input, true); - -if (!isset($data['request_id'], $data['action'], $data['validator_id'])) { - echo json_encode(["success" => false, "message" => "Données manquantes"]); - exit(); -} - -$requestId = (int)$data['request_id']; -$action = $data['action']; // "approve" | "reject" -$validatorId = (int)$data['validator_id']; -$comment = $data['comment'] ?? ''; - -try { - $conn->begin_transaction(); - - // Vérifier que le validateur existe dans CollaborateurAD - $stmt = $conn->prepare("SELECT Id, prenom, nom FROM CollaborateurAD WHERE Id = ?"); - $stmt->bind_param("i", $validatorId); - $stmt->execute(); - $validator = $stmt->get_result()->fetch_assoc(); - $stmt->close(); - - if (!$validator) { - throw new Exception("Validateur introuvable dans CollaborateurAD"); - } - - // Récupération de la demande - $queryCheck = " - SELECT dc.Id, dc.CollaborateurADId, dc.TypeCongeId, dc.DateDebut, dc.DateFin, dc.NombreJours, - ca.prenom as CADPrenom, ca.nom as CADNom, - tc.Nom as TypeNom - FROM DemandeConge dc - JOIN TypeConge tc ON dc.TypeCongeId = tc.Id - LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id - WHERE dc.Id = ? AND dc.Statut = 'En attente' - "; - $stmtCheck = $conn->prepare($queryCheck); - $stmtCheck->bind_param("i", $requestId); - $stmtCheck->execute(); - $requestRow = $stmtCheck->get_result()->fetch_assoc(); - $stmtCheck->close(); - - if (!$requestRow) { - throw new Exception("Demande non trouvée ou déjà traitée"); - } - - $collaborateurId = $requestRow['CollaborateurADId']; - $typeCongeId = $requestRow['TypeCongeId']; - $nombreJours = $requestRow['NombreJours']; - $employeeName = $requestRow['CADPrenom']." ".$requestRow['CADNom']; - $typeNom = $requestRow['TypeNom']; - - $newStatus = ($action === 'approve') ? 'Validée' : 'Refusée'; - - // 🔹 Mise à jour DemandeConge - $queryUpdate = " - UPDATE DemandeConge - SET Statut = ?, - ValidateurId = ?, - ValidateurADId = ?, - DateValidation = NOW(), - CommentaireValidation = ? - WHERE Id = ? - "; - $stmtUpdate = $conn->prepare($queryUpdate); - $stmtUpdate->bind_param("siisi", $newStatus, $validatorId, $validatorId, $comment, $requestId); - $stmtUpdate->execute(); - $stmtUpdate->close(); - - // 🔹 Déduction solde (pas maladie) - if ($action === 'approve' && $typeNom !== 'Congé maladie' && $collaborateurId) { - $year = date("Y"); - $queryDeduct = " - UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?) - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - "; - $stmtDeduct = $conn->prepare($queryDeduct); - $stmtDeduct->bind_param("diii", $nombreJours, $collaborateurId, $typeCongeId, $year); - $stmtDeduct->execute(); - $stmtDeduct->close(); - } - - // 🔹 Notification - $notificationTitle = ($action === 'approve') ? 'Demande approuvée' : 'Demande refusée'; - $notificationMessage = "Votre demande de $typeNom a été " . (($action === 'approve') ? "approuvée" : "refusée"); - if ($comment) $notificationMessage .= " (Commentaire: $comment)"; - $notifType = ($action === 'approve') ? 'Success' : 'Error'; - - $queryNotif = " - INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId) - VALUES (?, ?, ?, ?, ?) - "; - $stmtNotif = $conn->prepare($queryNotif); - $stmtNotif->bind_param("isssi", $collaborateurId, $notificationTitle, $notificationMessage, $notifType, $requestId); - $stmtNotif->execute(); - $stmtNotif->close(); - - // 🔹 Historique - $actionText = ($action === 'approve') ? 'Validation congé' : 'Refus congé'; - $actionDetails = "$actionText $employeeName ($typeNom)"; - if ($comment) $actionDetails .= " - $comment"; - - $queryHistory = " - INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DemandeCongeId) - VALUES (?, ?, ?, ?) - "; - $stmtHistory = $conn->prepare($queryHistory); - $stmtHistory->bind_param("issi", $validatorId, $actionText, $actionDetails, $requestId); - $stmtHistory->execute(); - $stmtHistory->close(); - - $conn->commit(); - - echo json_encode([ - "success" => true, - "message" => "Demande " . (($action === 'approve') ? 'approuvée' : 'refusée'), - "new_status" => $newStatus - ]); - -} catch (Exception $e) { - $conn->rollback(); - echo json_encode(["success" => false, "message" => $e->getMessage()]); -} - -$conn->close(); diff --git a/project/src/App.jsx b/project/src/App.jsx index 8c57e02..0c402e0 100644 --- a/project/src/App.jsx +++ b/project/src/App.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { AuthProvider } from './context/AuthContext'; +import { AuthProvider, useAuth } from './context/AuthContext'; // ⭐ Ajout de useAuth import Dashboard from './pages/Dashboard'; import Login from './pages/Login'; import Requests from './pages/Requests'; @@ -9,87 +9,103 @@ import Manager from './pages/Manager'; import ProtectedRoute from './components/ProtectedRoute'; import EmployeeDetails from './pages/EmployeeDetails'; import Collaborateur from './pages/Collaborateur'; -import CompteRenduActivites from './pages/CompteRenduActivite'; // ⭐ Ajout +import CompteRenduActivites from './pages/CompteRenduActivite'; +import GlobalTutorial from './components/GlobalTutorial'; + +// ⭐ Créer un composant séparé pour utiliser useAuth +function AppContent() { + const { user } = useAuth(); + const userId = user?.id || user?.CollaborateurADId || user?.ID; + + return ( + <> + {/* ⭐ Tutoriel global - Il s'affichera sur toutes les pages */} + + + + {/* Route publique */} + } /> + + {/* Routes protégées */} + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + {/* ⭐ Nouvelle route pour Compte-Rendu d'Activités */} + + + + } + /> + + {/* Redirection par défaut */} + } /> + + {/* Route 404 - Redirection vers dashboard */} + } /> + + + ); +} function App() { return ( - - {/* Route publique */} - } /> - - {/* Routes protégées */} - - - - } - /> - - - - - } - /> - - - - - } - /> - - - - - } - /> - - - - - } - /> - - - - - } - /> - - {/* ⭐ Nouvelle route pour Compte-Rendu d'Activités */} - - - - } - /> - - {/* Redirection par défaut */} - } /> - - {/* Route 404 - Redirection vers dashboard */} - } /> - + ); diff --git a/project/src/AuthConfig.js b/project/src/AuthConfig.js index e9e1b02..8ab94fc 100644 --- a/project/src/AuthConfig.js +++ b/project/src/AuthConfig.js @@ -1,22 +1,47 @@ // authConfig.js + +const hostname = window.location.hostname; +const protocol = window.location.protocol; + +// Détection environnements (utile pour le debug) +const isProduction = hostname === "mygta.ensup-adm.net"; + +// --- API URL --- +// On utilise TOUJOURS /api car le proxy Vite (port 80) va rediriger vers le backend (port 3000) +// Cela évite les problèmes CORS et les problèmes de ports fermés (8000) +export const API_BASE_URL = "/api"; + +// --- MSAL Config --- export const msalConfig = { auth: { - clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561", // Application (client) ID dans Azure - authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9", // Directory (tenant) ID - redirectUri: "http://localhost:5173" + clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561", + authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9", + + // En prod, on force l'URL sans slash final pour être propre + redirectUri: isProduction + ? "https://mygta.ensup-adm.net" + : `${protocol}//${hostname}`, }, cache: { - cacheLocation: "sessionStorage", + cacheLocation: "sessionStorage", storeAuthStateInCookie: false, - } + }, }; +// --- Permissions Graph --- export const loginRequest = { scopes: [ "User.Read", - "User.Read.All", // Pour lire les profils des autres utilisateurs - "Group.Read.All", // Pour lire les groupes - "GroupMember.Read.All", // Pour lire les membres des groupes - "Mail.Send" //Envoyer les emails. - ] + "User.Read.All", + "Group.Read.All", + "GroupMember.Read.All", + "Mail.Send", + ], }; + +console.log("🔧 Config Auth:", { + hostname, + protocol, + API_BASE_URL, + redirectUri: msalConfig.auth.redirectUri, +}); diff --git a/project/src/components/EditLeaveRequestModal.jsx b/project/src/components/EditLeaveRequestModal.jsx index d74ca34..73e0267 100644 --- a/project/src/components/EditLeaveRequestModal.jsx +++ b/project/src/components/EditLeaveRequestModal.jsx @@ -178,31 +178,45 @@ const EditLeaveRequestModal = ({ try { const formDataToSend = new FormData(); - formDataToSend.append('requestId', request.id); - formDataToSend.append('leaveType', parseInt(leaveType)); + // ⭐ Ajouter tous les champs texte AVANT les fichiers + formDataToSend.append('requestId', request.id.toString()); + formDataToSend.append('leaveType', leaveType.toString()); formDataToSend.append('startDate', startDate); formDataToSend.append('endDate', endDate); - formDataToSend.append('reason', reason); - formDataToSend.append('userId', userId); + formDataToSend.append('reason', reason || ''); + formDataToSend.append('userId', userId.toString()); formDataToSend.append('userEmail', userEmail); formDataToSend.append('userName', userName); - formDataToSend.append('accessToken', accessToken); + formDataToSend.append('accessToken', accessToken || ''); // ⭐ Calcul des jours selon le type const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType)); const daysToSend = selectedType?.key === 'Récup' ? saturdayCount : businessDays; - formDataToSend.append('businessDays', daysToSend); + formDataToSend.append('businessDays', daysToSend.toString()); - // ⭐ Documents médicaux - medicalDocuments.forEach((file) => { - formDataToSend.append('medicalDocuments', file); - }); + // ⭐ Documents médicaux EN DERNIER + if (medicalDocuments.length > 0) { + medicalDocuments.forEach((file) => { + formDataToSend.append('medicalDocuments', file); + }); + } - const response = await fetch('http://localhost:3000/updateRequest', { + // ⭐ DEBUG : Vérifier le contenu + console.log('📤 FormData à envoyer:'); + for (let pair of formDataToSend.entries()) { + console.log(pair[0], ':', pair[1]); + } + + const response = await fetch('/api/updateRequest', { method: 'POST', + // ⭐ NE PAS mettre de Content-Type, le navigateur le fera automatiquement avec boundary body: formDataToSend }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); if (data.success) { @@ -222,7 +236,7 @@ const EditLeaveRequestModal = ({ }); } } catch (error) { - console.error('Erreur:', error); + console.error('❌ Erreur:', error); setSubmitMessage({ type: 'error', text: '❌ Une erreur est survenue. Veuillez réessayer.' diff --git a/project/src/components/GlobalTutorial.jsx b/project/src/components/GlobalTutorial.jsx new file mode 100644 index 0000000..20c2f96 --- /dev/null +++ b/project/src/components/GlobalTutorial.jsx @@ -0,0 +1,728 @@ +import React, { useState, useEffect } from 'react'; +import Joyride, { STATUS } from 'react-joyride'; +import { useLocation } from 'react-router-dom'; + +const GlobalTutorial = ({ userId, userRole }) => { + const [runTour, setRunTour] = useState(false); + const [dontShowAgain, setDontShowAgain] = useState(false); + const [availableSteps, setAvailableSteps] = useState([]); + const location = useLocation(); + + const isEmployee = userRole === "Collaborateur" || userRole === "Apprenti"; + const canViewAllFilters = ['president', 'rh', 'admin', 'directeur de campus', 'directrice de campus'].includes(userRole?.toLowerCase()); + + // 🎯 NOUVELLE FONCTION : Vérifier si un élément existe dans le DOM + const elementExists = (selector) => { + return document.querySelector(selector) !== null; + }; + + // 🎯 NOUVELLE FONCTION : Filtrer les étapes selon les éléments disponibles + const filterAvailableSteps = (steps) => { + return steps.filter(step => { + // Les étapes centrées (body) sont toujours affichées + if (step.target === 'body') return true; + + // Pour les autres, vérifier si l'élément existe + const element = document.querySelector(step.target); + if (!element) { + console.log(`⚠️ Élément non trouvé, étape ignorée: ${step.target}`); + return false; + } + + // Vérifier si l'élément est visible + const isVisible = element.offsetParent !== null; + if (!isVisible) { + console.log(`⚠️ Élément caché, étape ignorée: ${step.target}`); + return false; + } + + return true; + }); + }; + + // 🎯 Déclencher le tutoriel avec vérification + useEffect(() => { + if (userId) { + let tutorialKey = ''; + + if (location.pathname === '/dashboard') { + tutorialKey = 'dashboard'; + } else if (location.pathname === '/manager') { + tutorialKey = 'manager'; + } else if (location.pathname === '/calendar') { + tutorialKey = 'calendar'; + } + + if (tutorialKey) { + const hasSeenTutorial = localStorage.getItem(`${tutorialKey}-tutorial-completed-${userId}`); + + if (!hasSeenTutorial) { + // ⭐ NOUVEAU : Attendre que le DOM soit chargé + setTimeout(() => { + const allSteps = getTourSteps(); + const available = filterAvailableSteps(allSteps); + + console.log(`📊 Étapes totales: ${allSteps.length}, disponibles: ${available.length}`); + + if (available.length > 2) { // Au moins 3 étapes (intro + 1 élément + conclusion) + setAvailableSteps(available); + setRunTour(true); + } else { + console.log('⚠️ Pas assez d\'éléments pour le tutoriel, annulation'); + } + }, 2000); + } + } + } + }, [userId, location.pathname]); + + // 🎯 Obtenir les étapes selon la page actuelle + const getTourSteps = () => { + // ==================== DASHBOARD ==================== + if (location.pathname === '/dashboard') { + return [ + { + target: 'body', + content: ( +
+

👋 Bienvenue sur votre application GTA !

+

Découvrez toutes les fonctionnalités en quelques étapes. Ce tutoriel ne s'affichera qu'une seule fois.

+
+ ), + placement: 'center', + disableBeacon: true, + }, + { + target: '[data-tour="dashboard"]', + content: '🏠 Accédez à votre tableau de bord pour voir vos soldes de congés.', + placement: 'right', + }, + { + target: '[data-tour="demandes"]', + content: '📋 Consultez et gérez toutes vos demandes de congés ici.', + placement: 'right', + }, + { + target: '[data-tour="calendrier"]', + content: '📅 Visualisez vos congés et ceux de votre équipe dans le calendrier.', + placement: 'right', + }, + { + target: '[data-tour="mon-equipe"]', + content: '👥 Consultez votre équipe et leurs absences.', + placement: 'right', + }, + { + target: '[data-tour="nouvelle-demande"]', + content: '➕ Cliquez ici pour créer une nouvelle demande de congé, RTT ou récupération.', + placement: 'left', + }, + { + target: '[data-tour="notifications"]', + content: '🔔 Consultez ici vos notifications (validations, refus, modifications de vos demandes).', + placement: 'bottom', + }, + { + target: '[data-tour="refresh"]', + content: '🔄 Rafraîchissez manuellement vos données. Mais pas d\'inquiétude : elles se mettent à jour automatiquement en temps réel !', + placement: 'bottom', + }, + { + target: '[data-tour="demandes-recentes"]', + content: '📄 Consultez rapidement vos 5 dernières demandes et leur statut. Cliquez sur "Voir toutes les demandes" pour accéder à la page complète.', + placement: 'top', + }, + { + target: '[data-tour="conges-service"]', + content: '👥 Visualisez les congés de votre service pour le mois en cours. Pratique pour planifier vos absences !', + placement: 'top', + }, + { + target: 'body', + content: ( +
+

📊 Vos compteurs de congés

+

Découvrez maintenant vos différents soldes de congés disponibles.

+
+ ), + placement: 'center', + }, + { + target: '[data-tour="cp-n-1"]', + content: '📅 Vos congés payés de l\'année précédente. ⚠️ Attention : ils doivent être soldés avant le 31 décembre de l\'année en cours !', + placement: 'top', + }, + { + target: '[data-tour="cp-n"]', + content: '📈 Vos congés payés de l\'année en cours, en cours d\'acquisition. Ils se cumulent au fil des mois travaillés.', + placement: 'top', + }, + { + target: '[data-tour="rtt"]', + content: '⏰ Vos RTT disponibles pour l\'année en cours. Ils sont acquis progressivement et à consommer avant le 31/12.', + placement: 'top', + }, + { + target: '[data-tour="recup"]', + content: '🔄 Vos jours de récupération accumulés suite à des heures supplémentaires.', + placement: 'top', + }, + { + target: 'body', + content: ( +
+

🎉 Vous êtes prêt !

+

Vous pouvez maintenant utiliser l'application en toute autonomie.

+
+

+ 💡 Besoin d'aide ? Cliquez sur le bouton "Aide" 🆘 en bas à droite pour relancer ce tutoriel à tout moment. +

+
+
+ ), + placement: 'center', + }, + ]; + } + + // ==================== MANAGER ==================== + if (location.pathname === '/manager') { + const baseSteps = [ + { + target: 'body', + content: ( +
+

👥 Bienvenue dans la gestion d'équipe !

+

Découvrez comment gérer {isEmployee ? 'votre équipe' : 'les demandes de congés de votre équipe'}.

+
+ ), + placement: 'center', + disableBeacon: true, + } + ]; + + if (!isEmployee) { + // Pour les managers/validateurs + return [ + ...baseSteps, + { + target: '[data-tour="demandes-attente"]', + content: '⏳ Consultez ici toutes les demandes en attente de validation. Vous pouvez les approuver ou les refuser directement.', + placement: 'right', + }, + { + target: '[data-tour="approuver-btn"]', + content: '✅ Cliquez sur "Approuver" pour valider une demande. Vous pourrez ajouter un commentaire optionnel.', + placement: 'top', + }, + { + target: '[data-tour="refuser-btn"]', + content: '❌ Cliquez sur "Refuser" pour rejeter une demande. Un commentaire expliquant le motif sera obligatoire.', + placement: 'top', + }, + { + target: '[data-tour="mon-equipe"]', + content: '👥 Consultez la liste complète de votre équipe. Cliquez sur un membre pour voir le détail de ses demandes.', + placement: 'left', + }, + { + target: '[data-tour="historique-demandes"]', + content: '📋 L\'historique complet de toutes les demandes de votre équipe avec leur statut (validée, refusée, en attente).', + placement: 'top', + }, + { + target: '[data-tour="document-joint"]', + content: '📎 Si un document est joint à une demande (certificat médical par exemple), vous pouvez le consulter ici.', + placement: 'left', + }, + { + target: 'body', + content: ( +
+

🎉 Vous êtes prêt à gérer votre équipe !

+

Vous savez maintenant valider les demandes et suivre les absences de vos collaborateurs.

+
+

+ 💡 Astuce : Les données se mettent à jour automatiquement en temps réel. Vous recevrez des notifications pour chaque nouvelle demande. +

+
+
+ ), + placement: 'center', + } + ]; + } else { + // Pour les collaborateurs/apprentis + return [ + ...baseSteps, + { + target: '[data-tour="mon-equipe"]', + content: '👥 Consultez ici la liste de votre équipe. Vous pouvez voir les membres de votre service.', + placement: 'left', + }, + { + target: '[data-tour="membre-equipe"]', + content: '👤 Cliquez sur un membre pour voir le détail de ses informations et absences.', + placement: 'left', + }, + { + target: 'body', + content: ( +
+

✅ C'est tout pour cette section !

+

Vous pouvez maintenant consulter votre équipe facilement.

+
+

+ 💡 Besoin d'aide ? N'hésitez pas à contacter votre manager pour toute question. +

+
+
+ ), + placement: 'center', + } + ]; + } + } + + // ==================== CALENDAR ==================== + if (location.pathname === '/calendar') { + const baseSteps = [ + { + target: 'body', + content: ( +
+

📅 Bienvenue dans le calendrier !

+

Découvrez comment visualiser et gérer les congés {canViewAllFilters ? 'de toute l\'entreprise' : 'de votre équipe'}.

+
+ ), + placement: 'center', + disableBeacon: true, + }, + { + target: '[data-tour="pto-counter"]', + content: '📊 Votre solde PTO (Paid Time Off) total : somme de vos CP N-1, CP N et RTT disponibles.', + placement: 'bottom', + }, + { + target: '[data-tour="navigation-mois"]', + content: '◀️▶️ Naviguez entre les mois pour consulter les congés passés et à venir.', + placement: 'bottom', + } + ]; + + // Étapes pour les filtres selon le rôle + if (canViewAllFilters) { + baseSteps.push( + { + target: '[data-tour="filtres-btn"]', + content: '🔍 Accédez aux filtres pour affiner votre vue : société, campus, service, collaborateurs...', + placement: 'left', + }, + { + target: '[data-tour="filtre-societe"]', + content: '🏢 Filtrez par société pour voir uniquement les congés d\'une entité spécifique.', + placement: 'bottom', + }, + { + target: '[data-tour="filtre-campus"]', + content: '🏫 Filtrez par campus pour visualiser les absences par site géographique.', + placement: 'bottom', + }, + { + target: '[data-tour="filtre-service"]', + content: '👔 Filtrez par service pour voir les congés d\'un département spécifique.', + placement: 'bottom', + } + ); + } + + // Étapes communes pour tous + baseSteps.push( + { + target: '[data-tour="selection-collaborateurs"]', + content: '👥 Sélectionnez les collaborateurs que vous souhaitez afficher dans le calendrier. Pratique pour se concentrer sur certaines personnes !', + placement: 'top', + }, + { + target: '[data-tour="refresh-btn"]', + content: '🔄 Rafraîchissez manuellement les données. Mais rassurez-vous : elles se mettent à jour automatiquement en temps réel via SSE !', + placement: 'left', + }, + { + target: 'body', + content: ( +
+

📅 Sélectionner des dates

+

Vous pouvez sélectionner des dates directement dans le calendrier pour créer une demande de congé rapidement.

+
+ ), + placement: 'center', + }, + { + target: '[data-tour="calendar-grid"]', + content: '🖱️ Cliquez sur une date de début, puis sur une date de fin pour sélectionner une période. Un menu contextuel apparaîtra pour choisir le type de congé.', + placement: 'top', + }, + { + target: '[data-tour="legende"]', + content: '🎨 La légende vous aide à identifier les différents types de congés : validés (vert), en attente (orange), formation (bleu), etc.', + placement: 'top', + }, + { + target: 'body', + content: ( +
+

🎉 Vous maîtrisez le calendrier !

+

Vous savez maintenant visualiser les congés, filtrer par équipe et créer rapidement des demandes.

+
+

+ 💡 Astuce : Survolez une case de congé pour voir tous les détails (employé, type, période, statut). Sur mobile, appuyez sur la case ! +

+
+
+ ), + placement: 'center', + } + ); + + return baseSteps; + } + + return []; + }; + + // 🎯 Obtenir la clé localStorage selon la page + const getTutorialKey = () => { + if (location.pathname === '/dashboard') return 'dashboard'; + if (location.pathname === '/manager') return 'manager'; + if (location.pathname === '/calendar') return 'calendar'; + return ''; + }; + + // 🎯 Gérer la fin du tutoriel + const handleJoyrideCallback = (data) => { + const { status } = data; + const finishedStatuses = [STATUS.FINISHED, STATUS.SKIPPED]; + + if (finishedStatuses.includes(status)) { + setRunTour(false); + setDontShowAgain(false); + } + }; + + // Si on n'a pas d'étapes disponibles, ne rien afficher + if (availableSteps.length === 0) return null; + + return ( + { + const [showConfirmModal, setShowConfirmModal] = React.useState(false); + const tutorialKey = getTutorialKey(); + + const handleFinish = () => { + if (dontShowAgain) { + localStorage.setItem(`${tutorialKey}-tutorial-completed-${userId}`, 'true'); + } + setRunTour(false); + setDontShowAgain(false); + }; + + const handleSkip = () => { + if (dontShowAgain) { + setShowConfirmModal(true); + } else { + setRunTour(false); + setDontShowAgain(false); + } + }; + + const confirmSkip = () => { + localStorage.setItem(`${tutorialKey}-tutorial-completed-${userId}`, 'true'); + setShowConfirmModal(false); + setRunTour(false); + setDontShowAgain(false); + }; + + const cancelSkip = () => { + setShowConfirmModal(false); + setDontShowAgain(false); + }; + + return ( + <> + {/* Modal de confirmation */} + {showConfirmModal && ( +
{ + if (e.target === e.currentTarget) { + cancelSkip(); + } + }}> +
+
+ ⚠️ +
+

+ Ne plus afficher le tutoriel ? +

+

+ Êtes-vous sûr de vouloir désactiver définitivement ce tutoriel ? + {tutorialKey === 'dashboard' && ' Vous pourrez le réactiver plus tard en cliquant sur le bouton "Aide".'} +

+
+ + +
+
+
+ )} + + {/* Tooltip principal */} +
+
+ {step.content} +
+ + {/* Case à cocher "Ne plus afficher" */} +
+ setDontShowAgain(e.target.checked)} + style={{ + width: '18px', + height: '18px', + cursor: 'pointer', + accentColor: '#0891b2' + }} + /> + +
+ +
+ + Étape {index + 1} sur {size} + + +
+ {index > 0 && ( + + )} + + {!isLastStep && ( + + )} + + {isLastStep && ( + + )} + + +
+
+
+ + ); + }} + /> + ); +}; + +export default GlobalTutorial; \ No newline at end of file diff --git a/project/src/components/Layout.jsx b/project/src/components/Layout.jsx deleted file mode 100644 index 5f28270..0000000 --- a/project/src/components/Layout.jsx +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/project/src/components/MedicalDocuments.jsx b/project/src/components/MedicalDocuments.jsx index b8a0f0a..a0f6a87 100644 --- a/project/src/components/MedicalDocuments.jsx +++ b/project/src/components/MedicalDocuments.jsx @@ -15,7 +15,7 @@ const MedicalDocuments = ({ demandeId }) => { try { setLoading(true); - const response = await fetch(`http://localhost:3000/medical-documents/${demandeId}`); + const response = await fetch(`/api/medical-documents/${demandeId}`); const data = await response.json(); if (data.success) { @@ -116,7 +116,7 @@ const MedicalDocuments = ({ demandeId }) => {
{ + const fetchCounters = async () => { + if (!userId) return; + + setIsLoadingCounters(true); + try { + const response = await fetch( + `/api/getDetailedLeaveCounters?user_id=${userId}` + ); + const data = await response.json(); + + if (data.success) { + console.log('📊 Compteurs reçus:', data); + setCountersData(data); + } else { + console.error('❌ Erreur compteurs:', data.message); + } + } catch (error) { + console.error('❌ Erreur réseau compteurs:', error); + } finally { + setIsLoadingCounters(false); + } + }; + + fetchCounters(); + }, [userId]); + + // ⭐ Utiliser les données des compteurs + const safeCounters = countersData ? { + availableCP: parseFloat(countersData.data?.totalDisponible?.cp || 0), + availableRTT: parseFloat(countersData.data?.totalDisponible?.rtt || 0), + availableRecup: parseFloat(countersData.data?.totalDisponible?.recup || 0) + } : { + availableCP: 0, + availableRTT: 0, + availableRecup: 0 }; + console.log('📊 Compteurs disponibles:', safeCounters); + console.log('📊 Données complètes:', countersData); + useEffect(() => { if (preselectedStartDate || preselectedEndDate) { setFormData(prev => ({ @@ -44,7 +85,6 @@ const NewLeaveRequestModal = ({ } }, [preselectedStartDate, preselectedEndDate, preselectedType]); - // 🔹 Calcul automatique - JOURS OUVRÉS UNIQUEMENT (Lun-Ven) useEffect(() => { if (formData.startDate && formData.endDate) { const start = new Date(formData.startDate); @@ -70,8 +110,7 @@ const NewLeaveRequestModal = ({ debut: formData.startDate, fin: formData.endDate, joursOuvres: workingDays, - samedis: saturdays, - message: 'Les samedis ne sont comptés QUE si "Récup" est coché' + samedis: saturdays }); } }, [formData.startDate, formData.endDate]); @@ -105,20 +144,15 @@ const NewLeaveRequestModal = ({ const handlePeriodeChange = (type, periode) => { setPeriodeSelection(prev => ({ ...prev, [type]: periode })); - // ⭐ CORRECTION : Ajuster automatiquement pour TOUTES les situations if (formData.types.length === 1) { - // Si un seul type : ajuster selon la période if (periode === 'Matin' || periode === 'Après-midi') { setRepartition(prev => ({ ...prev, [type]: 0.5 })); } else { setRepartition(prev => ({ ...prev, [type]: totalDays })); } } else { - // Si plusieurs types : garder la répartition manuelle - // MAIS si c'est une période partielle sur une journée unique if (formData.startDate === formData.endDate && (periode === 'Matin' || periode === 'Après-midi')) { const currentRepartition = repartition[type] || 0; - // Suggérer 0.5 si pas encore défini if (currentRepartition === 0 || currentRepartition === 1) { setRepartition(prev => ({ ...prev, [type]: 0.5 })); } @@ -126,7 +160,6 @@ const NewLeaveRequestModal = ({ } }; - const handleFileUpload = (e) => { const files = Array.from(e.target.files); const validFiles = []; @@ -170,6 +203,15 @@ const NewLeaveRequestModal = ({ const validateForm = () => { console.log('\n🔍 === VALIDATION FORMULAIRE ==='); + // 🔥 AJOUTER CES LOGS + console.log('📊 countersData:', countersData); + console.log('📊 countersData.success:', countersData?.success); + console.log('📊 countersData.data:', countersData?.data); + console.log('📊 totalDisponible:', countersData?.data?.totalDisponible); + console.log('📊 totalDisponible.cp:', countersData?.data?.totalDisponible?.cp); + console.log('📊 safeCounters:', safeCounters); + + // Vérifications de base if (formData.types.length === 0) { setError('Veuillez sélectionner au moins un type de congé'); return false; @@ -200,58 +242,82 @@ const NewLeaveRequestModal = ({ return false; } - const hasRecup = formData.types.includes('Récup'); const hasABS = formData.types.includes('ABS'); - // ⭐ NOUVEAU : Calculer le total attendu en tenant compte des demi-journées - let expectedTotal = totalDays; - - // Si un seul type avec demi-journée sur une journée unique - if (formData.types.length === 1 && formData.startDate === formData.endDate) { - const type = formData.types[0]; - const periode = periodeSelection[type]; - - if ((type === 'CP' || type === 'RTT') && (periode === 'Matin' || periode === 'Après-midi')) { - expectedTotal = 0.5; - console.log('📊 Demi-journée détectée, expectedTotal = 0.5'); - } - } - - console.log('📊 Analyse:', { - joursOuvres: totalDays, - expectedTotal: expectedTotal, - samedis: saturdayCount, - typesCoches: formData.types, - hasRecup - }); - - if (hasRecup && saturdayCount === 0) { - setError('Une récupération nécessite au moins un samedi dans la période. Veuillez sélectionner une période incluant un samedi ou décocher "Récupération (samedi)".'); + if (hasABS && formData.types.length > 1) { + setError('Un arrêt maladie ne peut pas être mélangé avec d\'autres types de congés'); return false; } - if (saturdayCount > 0 && !hasRecup) { - console.log(`⚠️ ${saturdayCount} samedi(s) détecté(s) mais "Récup" non coché - Les samedis seront ignorés`); - } - if (hasABS && formData.medicalDocuments.length === 0) { setError('Un justificatif médical est obligatoire pour un arrêt maladie'); return false; } - // ⭐ VALIDATION RÉPARTITION AMÉLIORÉE - if (formData.types.length > 1) { - const sum = Object.values(repartition).reduce((a, b) => a + b, 0); + // VALIDATION DES SOLDES AVEC ANTICIPATION + // 🔥 CONDITION MODIFIÉE : Vérifier que les données sont bien chargées + if (!countersData || !countersData.data || !countersData.data.totalDisponible) { + console.error('❌ Données compteurs non disponibles pour validation !'); + setError('Erreur : Les compteurs ne sont pas chargés. Veuillez réessayer.'); + return false; + } - console.log('📊 Validation répartition:', { - somme: sum, - attendu: expectedTotal, - joursOuvres: totalDays, - samedisIgnores: !hasRecup ? saturdayCount : 0 + // Calculer les jours demandés par type + const joursDemandesParType = {}; + + if (formData.types.length === 1) { + const type = formData.types[0]; + const periode = periodeSelection[type] || 'Journée entière'; + + if (formData.startDate === formData.endDate && (periode === 'Matin' || periode === 'Après-midi')) { + joursDemandesParType[type] = 0.5; + } else { + joursDemandesParType[type] = totalDays; + } + } else { + formData.types.forEach(type => { + joursDemandesParType[type] = repartition[type] || 0; }); + } - if (Math.abs(sum - expectedTotal) > 0.01) { - setError(`La somme des jours répartis (${sum.toFixed(1)}j) doit être égale au total de jours ouvrés (${expectedTotal}j). ${saturdayCount > 0 && !hasRecup ? `Les ${saturdayCount} samedi(s) ne sont pas comptés.` : ''}`); + console.log('📊 Jours demandés:', joursDemandesParType); + console.log('📊 Soldes disponibles:', safeCounters); + + // Vérifier CP + if (joursDemandesParType['CP'] > 0) { + const cpDemande = joursDemandesParType['CP']; + const cpDisponible = safeCounters.availableCP; + + console.log(`🔍 CP: ${cpDemande}j demandés vs ${cpDisponible}j disponibles`); + + if (cpDemande > cpDisponible) { + setError(`Solde CP insuffisant (${cpDisponible.toFixed(2)}j disponibles avec anticipation, ${cpDemande}j demandés)`); + return false; + } + } + + // Vérifier RTT + if (joursDemandesParType['RTT'] > 0) { + const rttDemande = joursDemandesParType['RTT']; + const rttDisponible = safeCounters.availableRTT; + + console.log(`🔍 RTT: ${rttDemande}j demandés vs ${rttDisponible}j disponibles`); + + if (rttDemande > rttDisponible) { + setError(`Solde RTT insuffisant (${rttDisponible.toFixed(2)}j disponibles avec anticipation, ${rttDemande}j demandés)`); + return false; + } + } + + // Vérifier Récup + if (joursDemandesParType['Récup'] > 0) { + const recupDemande = joursDemandesParType['Récup']; + const recupDisponible = safeCounters.availableRecup; + + console.log(`🔍 Récup: ${recupDemande}j demandés vs ${recupDisponible}j disponibles`); + + if (recupDemande > recupDisponible) { + setError(`Solde Récup insuffisant (${recupDisponible.toFixed(2)}j disponibles, ${recupDemande}j demandés)`); return false; } } @@ -261,6 +327,7 @@ const NewLeaveRequestModal = ({ }; + const handleSubmit = async () => { setError(''); @@ -281,15 +348,13 @@ const NewLeaveRequestModal = ({ formDataToSend.append('Nom', userName); formDataToSend.append('Commentaire', formData.reason || ''); - // ⭐ CORRECTION : Calculer le NombreJours total correctement let totalJoursToSend = totalDays; - // Si un seul type avec demi-journée if (formData.types.length === 1 && formData.startDate === formData.endDate) { const type = formData.types[0]; const periode = periodeSelection[type]; - if ((type === 'CP' || type === 'RTT') && (periode === 'Matin' || periode === 'Après-midi')) { + if ((type === 'CP' || type === 'RTT' || type === 'Récup') && (periode === 'Matin' || periode === 'Après-midi')) { totalJoursToSend = 0.5; } } @@ -297,50 +362,31 @@ const NewLeaveRequestModal = ({ formDataToSend.append('NombreJours', totalJoursToSend); const repartitionArray = formData.types.map(type => { - if (type === 'Récup' && formData.types.length === 1) { - console.log(`📝 Récup seul: ${saturdayCount} samedi(s)`); - return { - TypeConge: type, - NombreJours: saturdayCount, - PeriodeJournee: 'Journée entière' - }; - } - - if (type === 'Récup' && formData.types.length > 1) { - const joursRecup = repartition[type] || saturdayCount; - console.log(`📝 Récup (répartition): ${joursRecup}j`); - return { - TypeConge: type, - NombreJours: joursRecup, - PeriodeJournee: 'Journée entière' - }; - } - - // ⭐ CORRECTION : Gérer demi-journées pour un seul type let nombreJours; + let periodeJournee = 'Journée entière'; + if (formData.types.length === 1) { - // Un seul type : utiliser soit 0.5 (demi-journée) soit totalDays - const periode = periodeSelection[type]; - if ((type === 'CP' || type === 'RTT') && + const periode = periodeSelection[type] || 'Journée entière'; + + if ((type === 'CP' || type === 'RTT' || type === 'Récup') && formData.startDate === formData.endDate && (periode === 'Matin' || periode === 'Après-midi')) { nombreJours = 0.5; + periodeJournee = periode; } else { nombreJours = totalDays; } } else { - // Plusieurs types : utiliser la répartition manuelle nombreJours = repartition[type] || 0; + periodeJournee = periodeSelection[type] || 'Journée entière'; } - console.log(`📝 ${type}: ${nombreJours}j (${periodeSelection[type] || 'Journée entière'})`); + console.log(`📝 ${type}: ${nombreJours}j (${periodeJournee})`); return { TypeConge: type, NombreJours: nombreJours, - PeriodeJournee: ['CP', 'RTT'].includes(type) - ? (periodeSelection[type] || 'Journée entière') - : 'Journée entière' + PeriodeJournee: ['CP', 'RTT', 'Récup'].includes(type) ? periodeJournee : 'Journée entière' }; }); @@ -353,7 +399,7 @@ const NewLeaveRequestModal = ({ formDataToSend.append('medicalDocuments', file); }); - const response = await fetch('http://localhost:3000/submitLeaveRequest', { + const response = await fetch('/api/submitLeaveRequest', { method: 'POST', body: formDataToSend }); @@ -376,7 +422,6 @@ const NewLeaveRequestModal = ({ } }; - const handleTypeToggle = (type) => { setFormData(prev => ({ ...prev, @@ -386,20 +431,74 @@ const NewLeaveRequestModal = ({ })); }; + const isTypeDisabled = (typeKey) => { + const hasABS = formData.types.includes('ABS'); + const hasOtherTypes = formData.types.some(t => t !== 'ABS'); + + if (hasABS && typeKey !== 'ABS') { + return true; + } + + if (hasOtherTypes && typeKey === 'ABS') { + return true; + } + + return false; + }; + + const getDisabledTooltip = (typeKey) => { + if (formData.types.includes('ABS') && typeKey !== 'ABS') { + return '⚠️ Un arrêt maladie ne peut pas être mélangé avec d\'autres types'; + } + if (formData.types.some(t => t !== 'ABS') && typeKey === 'ABS') { + return '⚠️ Un arrêt maladie ne peut pas être mélangé avec d\'autres types'; + } + return ''; + }; + + // ⭐ Inclure les détails des compteurs dans availableTypes + // ⭐ Inclure les détails des compteurs dans availableTypes const availableTypes = userRole === 'Apprenti' ? [ - { key: 'CP', label: 'Congés payés', available: safeCounters.availableCP }, + { + key: 'CP', + label: 'Congé(s) payé(s)', + // ✅ Afficher seulement le solde actuel (sans anticipé) + available: countersData?.data?.cpN?.solde || 0, + details: countersData?.data?.cpN + }, { key: 'ABS', label: 'Arrêt maladie' }, { key: 'Formation', label: 'Formation' }, - { key: 'Récup', label: 'Récupération (samedi)' }, + { + key: 'Récup', + label: 'Récupération(s)', + available: countersData?.data?.recupN?.solde || 0 + }, ] : [ - { key: 'CP', label: 'Congés payés', available: safeCounters.availableCP }, - { key: 'RTT', label: 'RTT', available: safeCounters.availableRTT }, - { key: 'ABS', label: 'Arrêt maladie' }, - { key: 'Récup', label: 'Récupération (samedi)' }, + { + key: 'CP', + label: 'Congé(s) payé(s)', + // ✅ Afficher seulement le solde actuel (sans anticipé) + available: countersData?.data?.cpN?.solde || 0, + details: countersData?.data?.cpN + }, + { + key: 'RTT', + label: 'RTT', + // ✅ Afficher seulement le solde actuel (sans anticipé) + available: countersData?.data?.rttN?.solde || 0, + details: countersData?.data?.rttN + }, + { + key: 'Récup', + label: 'Récupération', + available: countersData?.data?.recupN?.solde || 0 + }, + { key: 'ABS', label: 'Arrêt maladie' } ]; + return (
@@ -411,34 +510,75 @@ const NewLeaveRequestModal = ({
+ {/* ⭐ BLOC SOLDES DÉTAILLÉS */} + + + {/* Loading */} + {isLoadingCounters && ( +
+
+

Chargement des soldes...

+
+ )} +
- {availableTypes.map(type => ( - + ); + })}
+ + {formData.types.includes('ABS') && ( +
+ +

+ Un arrêt maladie ne peut pas être combiné avec d'autres types de congés. +

+
+ )}
@@ -468,12 +608,7 @@ const NewLeaveRequestModal = ({
- - - - {/* ⭐ SECTION PÉRIODE POUR UN SEUL TYPE */} - {/* ⭐ SECTION PÉRIODE POUR UN SEUL TYPE */} - {formData.types.length === 1 && ['CP', 'RTT'].includes(formData.types[0]) && ( + {formData.types.length === 1 && ['CP', 'RTT', 'Récup'].includes(formData.types[0]) && (

Période de la journée @@ -496,7 +631,6 @@ const NewLeaveRequestModal = ({ ))}

- {/* 🎯 AFFICHAGE DU NOMBRE DE JOURS */}
Durée sélectionnée : @@ -506,10 +640,8 @@ const NewLeaveRequestModal = ({ const periode = periodeSelection[type] || 'Journée entière'; if (formData.startDate === formData.endDate) { - // Journée unique return (periode === 'Matin' || periode === 'Après-midi') ? '0.5 jour' : '1 jour'; } else { - // Plusieurs jours return (periode === 'Matin' || periode === 'Après-midi') ? `${(totalDays - 0.5).toFixed(1)} jours` : `${totalDays} jour${totalDays > 1 ? 's' : ''}`; @@ -521,20 +653,17 @@ const NewLeaveRequestModal = ({
)} - - {/* ⭐ SECTION RÉPARTITION POUR PLUSIEURS TYPES */} - {/* ⭐ SECTION RÉPARTITION POUR PLUSIEURS TYPES */} {formData.types.length > 1 && totalDays > 0 && (

Répartition des {totalDays} jours ouvrés

- La somme doit être égale à {totalDays} jour(s) + Indiquez la répartition souhaitée (le système vérifiera automatiquement)

{formData.types.map((type) => { - const showPeriode = ['CP', 'RTT'].includes(type); + const showPeriode = ['CP', 'RTT', 'Récup'].includes(type); const currentValue = repartition[type] || 0; return ( @@ -547,11 +676,10 @@ const NewLeaveRequestModal = ({ type="number" step="0.5" min="0" - max={type === 'Récup' ? saturdayCount : totalDays} + max={totalDays} value={repartition[type] || ''} onChange={(e) => handleRepartitionChange(type, e.target.value)} className="w-24 px-2 py-1 border rounded text-right text-sm" - placeholder={type === 'Récup' ? `Max ${saturdayCount}` : ''} />
@@ -593,7 +721,6 @@ const NewLeaveRequestModal = ({
- {/* 🎯 AFFICHAGE DU NOMBRE DE JOURS POUR CE TYPE */}
Durée : @@ -611,7 +738,6 @@ const NewLeaveRequestModal = ({
)} -