diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..9494d37 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,32 @@ +# É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 800db47..a7b89be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,44 +1,12 @@ -version: "3.8" - -services: - backend: - build: - context: ./project/public/Backend - dockerfile: DockerfileGTA.backend - container_name: gta-backend - hostname: backend - ports: - - "8012:3000" - volumes: - - ./project/public/Backend/uploads:/app/uploads - networks: - mariadb_default: - aliases: - - backend - - gta-backend - restart: unless-stopped - extra_hosts: - - "host.docker.internal:host-gateway" - +services: frontend: - build: - context: ./project - dockerfile: DockerfileGTA.frontend - container_name: gta-frontend - hostname: frontend + image: ouijdaneim/gta-frontend:latest ports: - - "3013:80" - environment: - - VITE_API_URL=http://backend:3000 - networks: - mariadb_default: - aliases: - - frontend - - gta-frontend + - "3000:80" depends_on: - backend - restart: unless-stopped -networks: - mariadb_default: - external: true \ No newline at end of file + backend: + image: ouijdaneim/gta-backend:latest + ports: + - "8000:80" \ No newline at end of file diff --git a/project/DockerfileGTA.frontend b/project/DockerfileGTA.frontend deleted file mode 100644 index 6d5281f..0000000 --- a/project/DockerfileGTA.frontend +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index ff61371..0000000 --- a/project/convert-cert-docker.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -# 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 0617205..cabb7e0 100644 --- a/project/package-lock.json +++ b/project/package-lock.json @@ -15,13 +15,11 @@ "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": { @@ -1042,12 +1040,6 @@ "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", @@ -2008,16 +2000,6 @@ "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", @@ -3319,12 +3301,6 @@ "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", @@ -3357,12 +3333,6 @@ } } }, - "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", @@ -3377,6 +3347,7 @@ "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" @@ -3990,33 +3961,6 @@ "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", @@ -4461,12 +4405,6 @@ "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", @@ -5887,21 +5825,6 @@ "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", @@ -6419,17 +6342,6 @@ "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", @@ -6612,23 +6524,6 @@ "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", @@ -6765,55 +6660,6 @@ "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", @@ -6821,47 +6667,6 @@ "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", @@ -7126,18 +6931,6 @@ "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", @@ -7901,16 +7694,6 @@ "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", @@ -7921,7 +7704,9 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "dev": true, + "license": "0BSD", + "optional": true }, "node_modules/type-detect": { "version": "4.0.8", diff --git a/project/package.json b/project/package.json index 16db53c..aa21140 100644 --- a/project/package.json +++ b/project/package.json @@ -16,13 +16,11 @@ "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 deleted file mode 100644 index 6b5c34f..0000000 --- a/project/public/Backend/DockerfileGTA.backend +++ /dev/null @@ -1,24 +0,0 @@ -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-test.js"] diff --git a/project/public/Backend/package.json b/project/public/Backend/package.json deleted file mode 100644 index 4f1eec3..0000000 --- a/project/public/Backend/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "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 deleted file mode 100644 index cda356f..0000000 --- a/project/public/Backend/server-test.js +++ /dev/null @@ -1,111 +0,0 @@ -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 (à adapter avec tes variables d'env) -const dbConfig = { - host:'192.168.0.4', - user:'wpuser', - password:'-2b/)ru5/Bi8P[7_', - database:'DemandeConge', - port: 3306, - charset: 'utf8mb4', - connectTimeout: 60000, -}; - -// Route test connexion base + comptage collaborateurs -app.get('/api/db-status', async (req, res) => { - try { - const pool = mysql.createPool(dbConfig); - 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) => { - const conn = await pool.getConnection(); - try { - 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 (Avec IGNORE pour éviter crash duplicate) - // On utilise INSERT IGNORE ou ON DUPLICATE KEY UPDATE pour ne jamais planter - 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 || '' - ]); - - // Si insertId est 0 (car update), on refait un select - 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); - // On renvoie un succès fake pour ne pas bloquer le frontend - res.json({ - success: true, - localUserId: 1, - role: 'Secours' - }); - } finally { - if (conn) conn.release(); - } -}); - - -app.listen(PORT, () => { - console.log(`✅ ✅ ✅ SERVEUR TEST DÉMARRÉ SUR LE PORT ${PORT} ✅ ✅ ✅`); -}); diff --git a/project/public/Backend/server.js b/project/public/Backend/server.js index 050e451..59ce8a5 100644 --- a/project/public/Backend/server.js +++ b/project/public/Backend/server.js @@ -21,37 +21,24 @@ 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: ['http://localhost:3013', 'http://localhost:80', 'https://mygta.ensup-adm.net'], - credentials: true + origin: '*', + methods: ['GET', 'POST', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'] })); - 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); @@ -423,88 +410,13 @@ const notifyCollabClients = (event, targetUserId = null) => { } }; -const sseClients = new Set(); - -// 🔌 ROUTE SSE POUR LE CALENDRIER -app.get('/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('\n📥 === WEBHOOK REÇU (COLLABORATEURS) ==='); - console.log('Event:', payload.event); - console.log('Data:', JSON.stringify(payload.data, null, 2)); + console.log('📥 Webhook reçu:', payload.event); // Vérifier la signature if (!webhookManager.verifySignature(payload, signature)) { @@ -516,147 +428,134 @@ app.post('/api/webhook/receive', async (req, res) => { // Traiter selon le type d'événement switch (event) { - case EVENTS.COMPTEUR_UPDATED: - console.log('\n💰 === WEBHOOK COMPTEUR_UPDATED REÇU ==='); - console.log(` Collaborateur: ${data.collaborateurId}`); - console.log(` Type mise à jour: ${data.typeUpdate}`); - console.log(` Type congé: ${data.typeConge}`); - console.log(` Année: ${data.annee}`); + case EVENTS.DEMANDE_VALIDATED: + console.log(`📥 Validation reçue: Demande ${data.demandeId} - Statut: ${data.statut}`); - // ⭐ SI MODIFICATION RH, METTRE À JOUR LA BASE LOCALE - if (data.source === 'rh' && data.nouveauTotal !== undefined && data.nouveauSolde !== undefined) { - console.log('🔄 Synchronisation depuis RH...'); - console.log(` Nouveau Total: ${data.nouveauTotal}j`); - console.log(` Nouveau Solde: ${data.nouveauSolde}j`); + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); - 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; + // ⭐ GESTION DES COMPTEURS SELON LE STATUT + if (data.statut === 'Refusée' && data.collaborateurId) { + console.log(`❌ DEMANDE REFUSÉE - Restauration des soldes...`); - const [typeRow] = await conn.query( - 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', - [typeName] + // Restaurer les soldes via la fonction existante + const restoration = await restoreLeaveBalance( + conn, + data.demandeId, + data.collaborateurId ); - if (typeRow.length > 0) { - const typeCongeId = typeRow[0].Id; + console.log('✅ Restauration terminée:', restoration); - // 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(); + } else if (data.statut === 'Validée') { + console.log(`✅ DEMANDE VALIDÉE - Les jours ont déjà été déduits à la création`); } + + // ⭐ 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 LE CLIENT SSE DU COLLABORATEUR + // 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}`); + 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 || 'rh', 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('\n✏️ === WEBHOOK DEMANDE_UPDATED REÇU ==='); - console.log(` Demande: ${data.demandeId}`); - console.log(` Collaborateur: ${data.collaborateurId}`); + 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(); + } 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('\n🗑️ === WEBHOOK DEMANDE_DELETED REÇU ==='); - console.log(` Demande: ${data.demandeId}`); - console.log(` Collaborateur: ${data.collaborateurId}`); + 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(); + } notifyCollabClients({ type: 'demande-deleted-rh', demandeId: data.demandeId, timestamp: new Date().toISOString() }, data.collaborateurId); - - console.log(' 📢 Notification suppression envoyée'); break; default: @@ -671,132 +570,6 @@ 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); @@ -839,14 +612,9 @@ 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 @@ -862,226 +630,69 @@ async function getConfigurationRTT(conn, annee, typeContrat = '37h') { }; } - // 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); - + // 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 }; } catch (error) { console.error('Erreur getConfigurationRTT:', error); - return getConfigurationRTTDefaut(annee, typeContrat); + // Retour valeur par défaut en cas d'erreur + return { joursAnnuels: 10, acquisitionMensuelle: 0.833333 }; } } -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 avec la formule Excel exacte + * Calcule l'acquisition RTT en tenant compte du type de contrat et de l'année */ async function calculerAcquisitionRTT(conn, collaborateurId, dateReference = new Date()) { - const d = new Date(dateReference); - d.setHours(0, 0, 0, 0); - const annee = d.getFullYear(); + try { + const d = new Date(dateReference); + 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] - ); + // 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] + ); - 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; - const isApprenti = collabInfo[0].role === 'Apprenti'; + 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; - // 2️⃣ Apprentis = pas de RTT - if (isApprenti) { return { - acquisition: 0, - moisTravailles: 0, - config: { joursAnnuels: 0, acquisitionMensuelle: 0 }, + acquisition: Math.round(acquisition * 100) / 100, + moisTravailles: parseFloat(moisTravailles.toFixed(2)), + config: config, 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 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 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 + * Calcule l'acquisition CP (inchangé, mais pour cohérence) */ 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); + const moisTravailles = getMoisTravaillesCP(dateReference, dateEntree); + const acquisition = moisTravailles * (25 / 12); + return Math.round(acquisition * 100) / 100; } - // ======================================== // TÂCHES CRON // ======================================== @@ -1371,10 +982,8 @@ 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); @@ -1382,7 +991,6 @@ 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); @@ -1390,16 +998,12 @@ 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); @@ -1546,8 +1150,6 @@ async function checkLeaveBalance(conn, collaborateurId, repartition) { return { valide: insuffisants.length === 0, details: verification, insuffisants }; } - - // ======================================== // MISE À JOUR DE updateMonthlyCounters // ======================================== @@ -1557,191 +1159,55 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null const currentYear = today.getFullYear(); const updates = []; - // Récupérer les infos du collaborateur - const [collabInfo] = await conn.query(` - SELECT DateEntree, TypeContrat, CampusId, role - FROM CollaborateurAD WHERE id = ? - `, [collaborateurId]); + 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'; - 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) - // ====================================== + // ===== CP (inchangé) ===== 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; - - // 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]); + 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 || 0); - const ancienSolde = parseFloat(existingCP[0].Solde || 0); + const ancienTotal = parseFloat(existingCP[0].Total); + const ancienSolde = parseFloat(existingCP[0].Solde); const soldeReporte = parseFloat(existingCP[0].SoldeReporte || 0); + const incrementTotal = acquisitionCP - ancienTotal; + const nouveauSolde = ancienSolde + incrementTotal; - 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]); + await conn.query( + `UPDATE CompteurConges + SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() + WHERE Id = ?`, + [acquisitionCP, Math.max(0, nouveauSolde), existingCP[0].Id] + ); updates.push({ type: 'CP', exercice: exerciceCP, acquisitionCumulee: acquisitionCP, - increment: incrementAcquis, - nouveauSolde: nouveauSolde + increment: incrementTotal, + nouveauSolde: Math.max(0, 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, cpTypeId, currentYear, acquisitionCP, acquisitionCP]); - - console.log(` CP - Compteur créé: ${acquisitionCP.toFixed(2)}j`); + await conn.query( + `INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, NOW())`, + [collaborateurId, cpTypeId, currentYear, acquisitionCP, acquisitionCP] + ); updates.push({ type: 'CP', @@ -1753,189 +1219,64 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null } } - // ====================================== - // RTT - // ====================================== - if (!isApprenti) { - const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); - const acquisitionRTT = rttData.acquisition; + // ===== RTT (NOUVEAU avec gestion variable) ===== + 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']); + 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] + ); - if (rttType.length > 0) { - const rttTypeId = rttType[0].Id; + if (existingRTT.length > 0) { + const ancienTotal = parseFloat(existingRTT[0].Total); + const ancienSolde = parseFloat(existingRTT[0].Solde); + const incrementTotal = acquisitionRTT - ancienTotal; + const nouveauSolde = ancienSolde + incrementTotal; - // 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]); + await conn.query( + `UPDATE CompteurConges + SET Total = ?, Solde = ?, DerniereMiseAJour = NOW() + WHERE Id = ?`, + [acquisitionRTT, Math.max(0, nouveauSolde), existingRTT[0].Id] + ); - 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, + 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] + ); - 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 - }); - } + 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; } @@ -1943,7 +1284,7 @@ async function updateMonthlyCounters(conn, collaborateurId, dateReference = null // ROUTES API // ======================================== -app.post('/api/login', async (req, res) => { +app.post('/login', async (req, res) => { try { const { email, mot_de_passe, entraUserId, userPrincipalName } = req.body; const accessToken = req.headers.authorization?.replace('Bearer ', ''); @@ -2022,56 +1363,78 @@ app.post('/api/login', async (req, res) => { } }); -app.post('/api/check-user-groups', async (req, res) => { +app.post('/check-user-groups', async (req, res) => { try { const { userPrincipalName } = req.body; - console.log('🔍 Check user groups pour:', userPrincipalName); + const accessToken = req.headers.authorization?.replace('Bearer ', ''); - if (!userPrincipalName) return res.json({ authorized: false }); + if (!userPrincipalName || !accessToken) return res.json({ authorized: false, message: 'Email ou token manquant' }); - // Vérification simple : Si l'user est dans la base, c'est OK - const [users] = await pool.query('SELECT * FROM CollaborateurAD WHERE email = ?', [userPrincipalName]); + 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, + so.Nom as societe_nom + FROM CollaborateurAD ca + LEFT JOIN Services s ON ca.ServiceId = s.Id + LEFT JOIN Societe so ON ca.SocieteId = so.Id + WHERE ca.email = ? + LIMIT 1 + `, [userPrincipalName]); if (users.length > 0) { - const u = users[0]; + const user = users[0]; return res.json({ authorized: true, - role: u.role || 'Employe', - groups: [u.role || 'Employe'], - localUserId: u.id, + role: user.role, + groups: [user.role], + localUserId: user.id, user: { - id: u.id, - entraUserId: u.entraUserId, - prenom: u.prenom, - nom: u.nom, - email: u.email, - role: u.role + ...user, + societeId: user.SocieteId, + societeNom: user.societe_nom } }); } - // Si pas trouvé, on autorise quand même pour permettre l'initial-sync juste après - // C'est une astuce pour éviter le blocage "chicken & egg" - return res.json({ - authorized: true, // ON FORCE À TRUE POUR DÉBLOQUER - role: 'Nouveau', - groups: ['Nouveau'], - localUserId: null - }); + 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é' }); + + // ⭐ Insertion avec SocieteId par défaut (ajuster selon votre logique) + 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] + ); + + res.json({ + authorized: true, + role: 'Collaborateur', + groups: ['Collaborateur'], + localUserId: result.insertId, + user: { + id: result.insertId, + entraUserId: userInfo.id, + prenom: userInfo.givenName, + nom: userInfo.surname, + email: userInfo.mail, + service: userInfo.department, + role: 'Collaborateur', + societeId: null, + societeNom: null + } + }); } catch (error) { - console.error('❌ Erreur check-user-groups:', error.message); - res.json({ authorized: false, error: error.message }); + res.json({ authorized: false, message: 'Erreur serveur', error: error.message }); } }); -// ======================================== -// ✅ CODE CORRIGÉ POUR getDetailedLeaveCounters -// À remplacer dans server.js à partir de la ligne ~1600 -// ======================================== - - -app.get('/api/getDetailedLeaveCounters', async (req, res) => { +app.get('/getDetailedLeaveCounters', async (req, res) => { try { const userIdParam = req.query.user_id; @@ -2081,13 +1444,17 @@ app.get('/api/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, ca.description + so.Nom as societe_nom FROM CollaborateurAD ca LEFT JOIN Services s ON ca.ServiceId = s.Id LEFT JOIN Societe so ON ca.SocieteId = so.Id @@ -2107,31 +1474,20 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { const dateEntree = user.DateEntree; const typeContrat = user.TypeContrat || '37h'; - const today = new Date(); + const dateRefParam = req.query.dateRef; + const today = dateRefParam ? parseDateYYYYMMDD(dateRefParam) : new Date(); const currentYear = today.getFullYear(); const previousYear = currentYear - 1; - console.log(`\n📊 === CALCUL COMPTEURS pour ${user.prenom} ${user.nom} ===`); - console.log(` Date référence: ${today.toLocaleDateString('fr-FR')}`); - - // 🔍 DEBUG : Afficher les valeurs brutes de la BDD - console.log('\n📊 === VALEURS BRUTES CompteurConges ==='); - const [debugCounters] = await conn.query(` - SELECT tc.Nom, cc.Annee, cc.Total, cc.Solde, cc.SoldeReporte - FROM CompteurConges cc - JOIN TypeConge tc ON cc.TypeCongeId = tc.Id - WHERE cc.CollaborateurADId = ? - ORDER BY tc.Nom, cc.Annee DESC - `, [userId]); - - debugCounters.forEach(c => { - console.log(` ${c.Nom} ${c.Annee}: Total=${c.Total}j, Solde=${c.Solde}j, Report=${c.SoldeReporte}j`); - }); - console.log('=====================================\n'); + // ⭐ 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; 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, @@ -2141,30 +1497,35 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { email: user.email, service: user.service || 'Non défini', role: user.role, - description: user.description, typeContrat: typeContrat, - societeId: user.SocieteId, - societeNom: user.societe_nom || 'Non défini', + societeId: userInfo.SocieteId, + societeNom: userInfo.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(today), + exerciceCP: getExerciceCP(dateCalcul), 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, recup: 0, total: 0 } + totalDisponible: { cp: 0, rtt: 0, total: 0 } }; const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); - // ==================================== - // 1️⃣ CP N-1 (Report) - // ==================================== + // ===== CP N-1 (Reporté) ===== if (cpType.length > 0) { const [cpN1] = await conn.query(` SELECT Annee, SoldeReporte, Total, Solde @@ -2172,40 +1533,24 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, cpType[0].Id, previousYear]); - if (cpN1.length > 0 && parseFloat(cpN1[0].Solde || 0) > 0) { - const soldeReporte = parseFloat(cpN1[0].Solde || 0); - - // 🔥 Consommation N-1 depuis DeductionDetails - const [consommeN1] = 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 IN ('Année N-1', 'Anne N-1', 'Report N-1', 'Reporté N-1') - AND dd.TypeDeduction NOT IN ('Accum Récup', 'Accum Recup') - AND dc.Statut != 'Refusée' - `, [userId, cpType[0].Id, previousYear]); - - const pris = parseFloat(consommeN1[0]?.totalConsomme || 0); - const soldeActuel = Math.max(0, soldeReporte - pris); + 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); counters.cpN1 = { annee: previousYear, - exercice: `${previousYear}-${previousYear + 1}`, + exercice: `${previousYear - 1}-${previousYear}`, reporte: parseFloat(soldeReporte.toFixed(2)), pris: parseFloat(pris.toFixed(2)), solde: parseFloat(soldeActuel.toFixed(2)), pourcentageUtilise: soldeReporte > 0 ? parseFloat(((pris / soldeReporte) * 100).toFixed(1)) : 0 }; counters.totalDisponible.cp += counters.cpN1.solde; - - console.log(`✅ CP N-1: Reporté=${soldeReporte}j, Pris=${pris}j, Solde=${soldeActuel}j`); } else { counters.cpN1 = { annee: previousYear, - exercice: `${previousYear}-${previousYear + 1}`, + exercice: `${previousYear - 1}-${previousYear}`, reporte: 0, pris: 0, solde: 0, @@ -2213,239 +1558,183 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { }; } - // ==================================== - // 2️⃣ CP N (Exercice en cours) - // ==================================== - const cpMonthsCurrent = getMoisTravaillesCP(today, dateEntree); - const acquisCumuleeCP = parseFloat(calculerAcquisitionCP(today, dateEntree) || 0); - - console.log(` CP - Mois travaillés: ${cpMonthsCurrent.toFixed(2)}`); - console.log(` CP - Acquisition cumulée: ${acquisCumuleeCP.toFixed(2)}j`); - - // 🔥 RÉCUPÉRER LE SOLDE DEPUIS CompteurConges (source de vérité) - const [compteurCPN] = await conn.query(` - SELECT Solde, SoldeReporte, Total + // ===== CP N (Exercice en cours) ===== + const [cpN] = await conn.query(` + SELECT Annee, Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, cpType[0].Id, currentYear]); - let soldeActuelCP = 0; - let totalAcquis = acquisCumuleeCP; - - if (compteurCPN.length > 0) { - // ⭐ Utiliser le solde de la base (déjà ajusté par les déductions) - const soldeBDD = parseFloat(compteurCPN[0].Solde || 0); - const soldeReporte = parseFloat(compteurCPN[0].SoldeReporte || 0); - - // Solde actuel = Solde total - Report (pour avoir uniquement l'année N) - soldeActuelCP = Math.max(0, soldeBDD - soldeReporte); - - console.log(` CP N - Solde BDD: ${soldeBDD}j, Report: ${soldeReporte}j → Solde N: ${soldeActuelCP}j`); + // ⭐ 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; } else { - // Si pas de compteur, le solde = acquisition (aucune déduction) - soldeActuelCP = acquisCumuleeCP; - console.log(` CP N - Pas de compteur BDD → Solde = Acquisition: ${soldeActuelCP}j`); + acquisCumuleeCP = calculerAcquisitionCP(dateCalcul, dateEntree) || 0; } - // 🔥 CALCUL DE L'ANTICIPÉ - 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]); + // ⭐ SÉCURITÉ : S'assurer que c'est un nombre + acquisCumuleeCP = parseFloat(acquisCumuleeCP) || 0; - const cpAnticipeUtilise = parseFloat(anticipeUtiliseCP[0]?.totalConsomme || 0); - const cpAnticipeMax = Math.max(0, 25 - acquisCumuleeCP); - const cpAnticipeDisponible = Math.max(0, cpAnticipeMax - cpAnticipeUtilise); + 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); - console.log(` CP Anticipé: Max=${cpAnticipeMax}j, Utilisé=${cpAnticipeUtilise}j, Dispo=${cpAnticipeDisponible}j`); + counters.cpN = { + annee: currentYear, + exercice: getExerciceCP(dateCalcul), + totalAnnuel: 25.00, + moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)), + acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)), - counters.cpN = { - annee: currentYear, - exercice: getExerciceCP(today), - totalAnnuel: 25.00, - moisTravailles: parseFloat(cpMonthsCurrent.toFixed(2)), - acquisitionMensuelle: parseFloat((25 / 12).toFixed(2)), - acquis: parseFloat(totalAcquis.toFixed(2)), - pris: parseFloat((totalAcquis - soldeActuelCP).toFixed(2)), // Déduit du solde - solde: parseFloat(soldeActuelCP.toFixed(2)), // ⭐ Solde réel de la BDD - tauxAcquisition: parseFloat((cpMonthsCurrent / 12 * 100).toFixed(1)), - pourcentageUtilise: totalAcquis > 0 ? parseFloat(((totalAcquis - soldeActuelCP) / 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 - } - }; + // ⭐ MODIFICATION : Afficher l'acquisition à la date de calcul + acquis: parseFloat(acquisCumuleeCP.toFixed(2)), + pris: parseFloat(pris.toFixed(2)), + solde: parseFloat(soldeN.toFixed(2)), - counters.totalDisponible.cp += counters.cpN.solde + cpAnticipeDisponible; - - console.log(`✅ CP N: Acquis=${totalAcquis}j, Pris=${counters.cpN.pris}j, Solde=${soldeActuelCP}j`); + 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; + } } - // ==================================== - // 3️⃣ RTT N - // ==================================== + // ===== RTT N (même logique) ===== 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) - if (rttType.length > 0 && user.role !== 'Apprenti') { - const rttData = await calculerAcquisitionRTT(conn, userId, today); - 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); + } - console.log(` RTT - Config: ${rttConfig.joursAnnuels}j/an`); - console.log(` RTT - Acquisition: ${rttData.acquisition.toFixed(2)}j`); + // ⭐ SÉCURITÉ : S'assurer que acquisition est un nombre + rttData.acquisition = parseFloat(rttData.acquisition) || 0; - // 🔥 RÉCUPÉRER LE SOLDE DEPUIS CompteurConges - const [compteurRTT] = await conn.query(` - SELECT Solde, Total + const [rttN] = await conn.query(` + SELECT Annee, Total, Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? `, [userId, rttType[0].Id, currentYear]); - let soldeActuelRTT = 0; + 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); - if (compteurRTT.length > 0) { - // ⭐ Utiliser le solde de la base - soldeActuelRTT = parseFloat(compteurRTT[0].Solde || 0); - console.log(` RTT - Solde BDD: ${soldeActuelRTT}j`); + 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; } else { - // Si pas de compteur, solde = acquisition - soldeActuelRTT = rttData.acquisition; - console.log(` RTT - Pas de compteur BDD → Solde = Acquisition: ${soldeActuelRTT}j`); + 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; } - // 🔥 CALCUL DE L'ANTICIPÉ RTT - const [anticipeUtiliseRTT] = 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, rttType[0].Id, currentYear]); - - const rttAnticipeUtilise = parseFloat(anticipeUtiliseRTT[0]?.totalConsomme || 0); - const rttAnticipeMax = Math.max(0, rttConfig.joursAnnuels - rttData.acquisition); - const rttAnticipeDisponible = Math.max(0, rttAnticipeMax - rttAnticipeUtilise); - - console.log(` RTT Anticipé: Max=${rttAnticipeMax}j, Utilisé=${rttAnticipeUtilise}j, Dispo=${rttAnticipeDisponible}j`); - - 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((rttData.acquisition - soldeActuelRTT).toFixed(2)), - solde: parseFloat(soldeActuelRTT.toFixed(2)), // ⭐ Solde réel de la BDD - tauxAcquisition: parseFloat((rttData.moisTravailles / 12 * 100).toFixed(1)), - pourcentageUtilise: rttData.acquisition > 0 ? parseFloat(((rttData.acquisition - soldeActuelRTT) / rttData.acquisition * 100).toFixed(1)) : 0, - joursRestantsAAcquerir: parseFloat((rttConfig.joursAnnuels - rttData.acquisition).toFixed(2)), - anticipe: { - acquisPrevu: parseFloat(rttAnticipeMax.toFixed(2)), - pris: parseFloat(rttAnticipeUtilise.toFixed(2)), - disponible: parseFloat(rttAnticipeDisponible.toFixed(2)), - depassement: rttAnticipeUtilise > rttAnticipeMax ? parseFloat((rttAnticipeUtilise - rttAnticipeMax).toFixed(2)) : 0 - } - }; - - counters.totalDisponible.rtt += counters.rttN.solde + rttAnticipeDisponible; - - console.log(`✅ RTT N: Acquis=${rttData.acquisition}j, Pris=${counters.rttN.pris}j, Solde=${soldeActuelRTT}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" - }; - - // ==================================== - // 4️⃣ RÉCUP - // ==================================== - const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); - - if (recupType.length > 0) { - // 🔥 Récupérer les accumulations - 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]); - - // 🔥 Récupérer les consommations - const [consomRecup] = await conn.query(` - SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsom - FROM DeductionDetails dd - JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id - WHERE dc.CollaborateurADId = ? - AND dd.TypeCongeId = ? - AND dd.Annee = ? - AND dd.TypeDeduction IN ('Récup Dosée', 'Recup Dosee', 'Récup Posée') - AND dc.Statut != 'Refusée' - `, [userId, recupType[0].Id, currentYear]); - - const acquis = parseFloat(accumRecup[0]?.totalAccum || 0); - const pris = parseFloat(consomRecup[0]?.totalConsom || 0); - const solde = Math.max(0, acquis - pris); - - counters.recupN = { - annee: currentYear, - acquis: parseFloat(acquis.toFixed(2)), - pris: parseFloat(pris.toFixed(2)), - solde: parseFloat(solde.toFixed(2)), - message: "Jours de récupération" - }; - - counters.totalDisponible.recup = counters.recupN.solde; - - console.log(`✅ Récup: Acquis=${acquis}j, Pris=${pris}j, Solde=${solde}j`); - } else { - counters.recupN = { - annee: currentYear, - acquis: 0, + counters.rttN1 = { + annee: previousYear, + reporte: 0, pris: 0, solde: 0, - message: "Jours de récupération" + pourcentageUtilise: 0, + message: "Les RTT ne sont pas reportables d'une année sur l'autre" }; - counters.totalDisponible.recup = 0; } - // Recalculer le TOTAL - counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt + counters.totalDisponible.recup; + counters.totalDisponible.total = counters.totalDisponible.cp + counters.totalDisponible.rtt; + // ===== RÉCUP (NOUVEAU) ===== + 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]); - console.log(`\n✅ TOTAL FINAL: ${counters.totalDisponible.total.toFixed(2)}j disponibles`); - console.log(` CP: ${counters.totalDisponible.cp.toFixed(2)}j`); - console.log(` RTT: ${counters.totalDisponible.rtt.toFixed(2)}j`); - console.log(` Récup: ${counters.totalDisponible.recup.toFixed(2)}j\n`); + if (recupN.length > 0) { + const total = parseFloat(recupN[0].Total || 0); + const solde = parseFloat(recupN[0].Solde || 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; + } + } conn.release(); - res.json({ success: true, message: 'Compteurs détaillés récupérés avec succès', - data: counters, - availableCP: counters.totalDisponible.cp, - availableRTT: counters.totalDisponible.rtt, - availableRecup: counters.totalDisponible.recup + data: counters }); } catch (error) { console.error('Erreur getDetailedLeaveCounters:', error); @@ -2457,7 +1746,7 @@ app.get('/api/getDetailedLeaveCounters', async (req, res) => { } }); -app.post('/api/reinitializeAllCounters', async (req, res) => { +app.post('/reinitializeAllCounters', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -2639,7 +1928,7 @@ app.post('/api/reinitializeAllCounters', async (req, res) => { } }); -app.post('/api/updateCounters', async (req, res) => { +app.post('/updateCounters', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; @@ -2656,7 +1945,7 @@ app.post('/api/updateCounters', async (req, res) => { } }); -app.post('/api/updateAllCounters', async (req, res) => { +app.post('/updateAllCounters', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -2776,6 +2065,7 @@ 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 const [deductions] = await conn.query( `SELECT dd.TypeCongeId, dd.Annee, dd.TypeDeduction, dd.JoursUtilises, tc.Nom as TypeNom FROM DeductionDetails dd @@ -2797,135 +2087,35 @@ 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 (Année: ${Annee})`); + console.log(`\n🔍 Traitement: ${TypeNom} - ${TypeDeduction} - ${JoursUtilises}j`); - // ⭐ NOUVEAU : Gestion des Récup posées - if (TypeDeduction === 'Récup Posée') { - console.log(`🔄 Restauration Récup posée: +${JoursUtilises}j`); + // 🔹 CAS SPÉCIAL : Récupération accumulée (RETIRER les jours) + if (TypeDeduction === 'Accum Récup') { + console.log(`❌ Annulation accumulation ${TypeNom}: -${JoursUtilises}j`); const [compteur] = await conn.query( - `SELECT Id, Solde FROM CompteurConges + `SELECT Id, Total, 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 Solde = ?, + SET Total = GREATEST(0, Total - ?), + Solde = GREATEST(0, Solde - ?), DerniereMiseAJour = NOW() WHERE Id = ?`, - [nouveauSolde, compteur[0].Id] + [JoursUtilises, JoursUtilises, compteur[0].Id] ); restorations.push({ type: TypeNom, annee: Annee, typeDeduction: TypeDeduction, - joursRestores: JoursUtilises + joursRetires: JoursUtilises }); - console.log(`✅ Récup restaurée: ${ancienSolde} → ${nouveauSolde}`); - } - continue; - } - - // 🔹 N+1 Anticipé - ⭐ RESTAURATION CORRECTE - 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 CORRECTE - 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 (cas rare) - 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`); + console.log(`✅ Récup retirée: ${compteur[0].Solde} → ${Math.max(0, compteur[0].Solde - JoursUtilises)}`); } continue; } @@ -2992,10 +2182,6 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { } } - // ⭐ IMPORTANT : Recalculer les soldes anticipés après restauration - console.log(`\n🔄 Recalcul des soldes anticipés...`); - await updateSoldeAnticipe(conn, collaborateurId); - console.log(`\n✅ Restauration terminée: ${restorations.length} opérations\n`); return { @@ -3009,7 +2195,8 @@ async function restoreLeaveBalance(conn, demandeCongeId, collaborateurId) { throw error; } } -app.get('/api/testProrata', async (req, res) => { + +app.get('/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' }); @@ -3055,7 +2242,7 @@ app.get('/api/testProrata', async (req, res) => { } }); -app.post('/api/fixAllCounters', async (req, res) => { +app.post('/fixAllCounters', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -3106,7 +2293,7 @@ app.post('/api/fixAllCounters', async (req, res) => { } }); -app.post('/api/processEndOfYear', async (req, res) => { +app.post('/processEndOfYear', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; @@ -3133,7 +2320,7 @@ app.post('/api/processEndOfYear', async (req, res) => { } }); -app.post('/api/processEndOfExercice', async (req, res) => { +app.post('/processEndOfExercice', async (req, res) => { const conn = await pool.getConnection(); try { const { collaborateur_id } = req.body; @@ -3160,7 +2347,7 @@ app.post('/api/processEndOfExercice', async (req, res) => { } }); -app.get('/api/getAcquisitionDetails', async (req, res) => { +app.get('/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 } }; @@ -3170,7 +2357,7 @@ app.get('/api/getAcquisitionDetails', async (req, res) => { } }); -app.get('/api/getLeaveCounters', async (req, res) => { +app.get('/getLeaveCounters', async (req, res) => { try { const userId = parseInt(req.query.user_id || 0); const data = {}; @@ -3184,7 +2371,7 @@ app.get('/api/getLeaveCounters', async (req, res) => { } }); -app.get('/api/getEmploye', async (req, res) => { +app.get('/getEmploye', async (req, res) => { try { const id = parseInt(req.query.id || 0); if (id <= 0) return res.json({ success: false, message: 'ID invalide' }); @@ -3303,7 +2490,7 @@ app.get('/api/getEmploye', async (req, res) => { } }); -app.get('/api/getEmployeRequest', async (req, res) => { +app.get('/getEmployeRequest', async (req, res) => { try { const id = parseInt(req.query.id || 0); if (id <= 0) return res.json({ success: false, message: 'ID invalide' }); @@ -3344,7 +2531,7 @@ app.get('/api/getEmployeRequest', async (req, res) => { } }); -app.get('/api/getRequests', async (req, res) => { +app.get('/getRequests', async (req, res) => { try { const userId = req.query.user_id; if (!userId) return res.json({ success: false, message: 'ID utilisateur manquant' }); @@ -3379,7 +2566,7 @@ app.get('/api/getRequests', async (req, res) => { let fileUrl = null; if (row.TypeConges && row.TypeConges.includes('Congé maladie') && row.DocumentJoint) { - fileUrl = `/uploads/${path.basename(row.DocumentJoint)}`; + fileUrl = `http://localhost:3000/uploads/${path.basename(row.DocumentJoint)}`; } return { @@ -3414,7 +2601,7 @@ app.get('/api/getRequests', async (req, res) => { }); } }); -app.get('/api/getAllTeamRequests', async (req, res) => { +app.get('/getAllTeamRequests', async (req, res) => { try { const managerId = req.query.SuperieurId; if (!managerId) return res.json({ success: false, message: 'Paramètre SuperieurId manquant' }); @@ -3426,7 +2613,7 @@ app.get('/api/getAllTeamRequests', async (req, res) => { } }); -app.get('/api/getPendingRequests', async (req, res) => { +app.get('/getPendingRequests', async (req, res) => { try { const managerId = req.query.manager_id; if (!managerId) return res.json({ success: false, message: 'ID manager manquant' }); @@ -3441,7 +2628,7 @@ app.get('/api/getPendingRequests', async (req, res) => { } }); -app.get('/api/getTeamMembers', async (req, res) => { +app.get('/getTeamMembers', async (req, res) => { try { const managerId = req.query.manager_id; if (!managerId) return res.json({ success: false, message: 'ID manager manquant' }); @@ -3455,7 +2642,7 @@ app.get('/api/getTeamMembers', async (req, res) => { } }); -app.get('/api/getNotifications', async (req, res) => { +app.get('/getNotifications', async (req, res) => { try { const userIdParam = req.query.user_id; @@ -3515,7 +2702,7 @@ app.get('/api/getNotifications', async (req, res) => { } }); -app.post('/api/markNotificationRead', async (req, res) => { +app.post('/markNotificationRead', async (req, res) => { try { const { notificationId } = req.body; if (!notificationId || notificationId <= 0) return res.status(400).json({ success: false, message: 'ID notification invalide' }); @@ -3525,598 +2712,8 @@ app.post('/api/markNotificationRead', async (req, res) => { res.status(500).json({ success: false, message: 'Erreur', error: error.message }); } }); -// À ajouter avant app.listen() -/** - * 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) => { +app.post('/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -4134,6 +2731,7 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), 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); @@ -4153,31 +2751,34 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), }); } - // ⭐ VALIDATION DE LA RÉPARTITION + // ⭐ NOUVEAU : 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); - // ⭐ Ne compter que CP, RTT ET RÉCUP dans la répartition + // ⭐ Calculer la somme de la répartition const sommeRepartition = Repartition.reduce((sum, r) => { - if (r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup') { + // Ne compter que CP et RTT (pas ABS ni Formation ni Récup) + if (r.TypeConge === 'CP' || r.TypeConge === 'RTT') { return sum + parseFloat(r.NombreJours || 0); } return sum; }, 0); - console.log('Somme répartition CP+RTT+Récup:', sommeRepartition.toFixed(2)); + console.log('Somme répartition CP+RTT:', sommeRepartition.toFixed(2)); - // ⭐ VALIDATION : La somme doit correspondre au total - const hasCountableLeave = Repartition.some(r => - r.TypeConge === 'CP' || r.TypeConge === 'RTT' || r.TypeConge === 'Récup' - ); + // ⭐ VALIDATION : La somme doit correspondre au total (tolérance 0.01j) + const hasCountableLeave = Repartition.some(r => r.TypeConge === 'CP' || r.TypeConge === 'RTT'); 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); @@ -4189,26 +2790,59 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), return res.json({ success: false, - message: `Erreur de répartition : la somme (${sommeRepartition.toFixed(2)}j) ne correspond pas au total (${NombreJours}j)` + 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 + } }); } + // ⭐ 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'); - // ⭐ Récup n'est PAS une demande auto-validée + // ⭐ Détection si c'est uniquement une formation 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); @@ -4222,51 +2856,102 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), } // ======================================== - // ÉTAPE 1 : Vérification des soldes AVANT tout (MODE MIXTE AVEC ANTICIPATION N+1) + // ÉTAPE 1 : Vérification des soldes AVANT tout // ======================================== if (isAD && collaborateurId && !isFormationOnly) { - console.log('\n🔍 Vérification des soldes en mode mixte avec anticipation...'); - console.log('Date début:', DateDebut); + console.log('\n🔍 Vérification des soldes (avec anticipation)...'); const [userRole] = await conn.query('SELECT role FROM CollaborateurAD WHERE id = ?', [collaborateurId]); const isApprenti = userRole.length > 0 && userRole[0].role === 'Apprenti'; - // ⭐ CORRECTION : Passer la date de début pour détecter N+1 - // ✅ APRÈS (avec anticipation) - const checkResult = await checkLeaveBalanceWithAnticipation( - conn, - collaborateurId, - Repartition, - DateDebut - ); + 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(); - // 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(); + return res.json({ + success: false, + message: `❌ Les apprentis ne peuvent pas poser de RTT` + }); + } - // 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'); - return res.json({ - success: false, - message: `❌ Solde(s) insuffisant(s):\n${messagesErreur}`, - details: checkResult.details, - insuffisants: checkResult.insuffisants - }); + 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é`); + } } - - console.log('✅ Tous les soldes sont suffisants (incluant anticipation si nécessaire)\n'); + console.log('✅ Soldes suffisants (avec anticipation si nécessaire)'); } - // ======================================== - // ÉTAPE 2 : CRÉER LA DEMANDE + // ÉTAPE 2 : CRÉER LA DEMANDE EN PREMIER // ======================================== console.log('\n📝 Création de la demande...'); @@ -4274,24 +2959,22 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), for (const rep of Repartition) { const code = rep.TypeConge; - // Ne pas inclure ABS et Formation dans les typeIds principaux - if (code === 'ABS' || code === 'Formation') { + // Ne pas inclure ABS, Formation, Récup dans les typeIds principaux + if (code === 'ABS' || code === 'Formation' || code === 'Récup') { continue; } - const name = code === 'CP' ? 'Congé payé' : - code === 'RTT' ? 'RTT' : - code === 'Récup' ? 'Récupération' : code; - + const name = code === 'CP' ? 'Congé payé' : 'RTT'; 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/Récup, prendre le premier type de la répartition + // ⭐ Si aucun type CP/RTT, 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' : 'Congé payé'; + firstType === 'ABS' ? 'Congé maladie' : + firstType === 'Récup' ? 'Récupération' : 'Congé payé'; const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); if (typeRow.length > 0) { @@ -4332,7 +3015,7 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), VALUES (?, ?, ?, ?, ?, NOW())`, [demandeId, file.originalname, file.path, file.mimetype, file.size] ); - console.log(` ✓ ${file.originalname}`); + console.log(` ✓ ${file.originalname} (${(file.size / 1024).toFixed(2)} KB)`); } } @@ -4356,13 +3039,13 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), 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' + rep.PeriodeJournee || 'Journée entière' // ✅ NOUVELLE COLONNE ] ); console.log(` ✓ ${name}: ${rep.NombreJours}j (${rep.PeriodeJournee || 'Journée entière'})`); @@ -4370,52 +3053,85 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), } // ======================================== - // ÉTAPE 5 : Déduction des compteurs CP/RTT/RÉCUP (AVEC ANTICIPATION N+1) + // ÉTAPE 5 : ACCUMULATION DES RÉCUP (maintenant demandeId existe) // ======================================== if (isAD && collaborateurId && !isFormationOnly) { - console.log('\n📉 Déduction des compteurs (avec anticipation N+1)...'); + 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...'); for (const rep of Repartition) { - if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation') { + if (rep.TypeConge === 'ABS' || rep.TypeConge === 'Formation' || rep.TypeConge === 'Récup') { 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 deductLeaveBalanceWithAnticipation( + const result = await deductLeaveBalanceWithTracking( conn, collaborateurId, typeRow[0].Id, rep.NombreJours, - demandeId, - DateDebut + demandeId ); console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`); @@ -4427,24 +3143,20 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), } } - await updateSoldeAnticipe(conn, collaborateurId); - console.log('✅ Déductions terminées\n'); + console.log('✅ Déductions terminées'); } // ======================================== - // É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}`; - + // ÉTAPE 7 : Créer notification pour formation 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', + 'Success', // ✅ Valeur correcte de l'enum '✅ Formation validée automatiquement', `Votre période de formation ${datesPeriode} a été validée automatiquement.`, demandeId @@ -4454,7 +3166,7 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), } // ======================================== - // ÉTAPE 7 : Récupérer les managers + // ÉTAPE 8 : Récupérer les managers // ======================================== let managers = []; if (isAD) { @@ -4465,13 +3177,24 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), [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 8 : Notifier les clients SSE + // ÉTAPE 9 : Notifier les clients SSE // ======================================== if (isFormationOnly && isAD && collaborateurId) { notifyCollabClients({ @@ -4480,46 +3203,77 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), 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 + // ENVOI DES EMAILS (code inchangé) // ======================================== const accessToken = await getGraphToken(); if (accessToken) { - const fromEmail = 'gtanoreply@ensup.eu'; + const fromEmail = 'noreply@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 === 'Récup' ? 'Récupération' : rep.TypeConge; + rep.TypeConge; return `${typeNom}: ${rep.NombreJours}j`; }).join(' | '); if (isFormationOnly) { - // Email formation - const subjectCollab = '✅ Formation enregistrée et validée'; + const subjectCollab = '✅ Votre saisie de période de formation a été enregistrée'; const bodyCollab = `
-
-

✅ Formation validée

+
+

✅ Formation enregistrée

-
-

Bonjour ${Nom},

-

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

-

Période : ${datesPeriode}

-

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

+
+

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}

` : ''} +
`; 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 = `
@@ -4576,6 +3330,7 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), } 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)) { @@ -4596,8 +3351,7 @@ app.post('/api/submitLeaveRequest', uploadMedical.array('medicalDocuments', 5), } }); - -app.get('/api/download-medical/:documentId', async (req, res) => { +app.get('/download-medical/:documentId', async (req, res) => { try { const { documentId } = req.params; const conn = await pool.getConnection(); @@ -4628,7 +3382,7 @@ app.get('/api/download-medical/:documentId', async (req, res) => { }); // Récupérer les documents d'une demande -app.get('/api/medical-documents/:demandeId', async (req, res) => { +app.get('/medical-documents/:demandeId', async (req, res) => { try { const { demandeId } = req.params; const conn = await pool.getConnection(); @@ -4660,7 +3414,7 @@ app.get('/api/medical-documents/:demandeId', async (req, res) => { res.status(500).json({ success: false, message: 'Erreur serveur' }); } }); -app.post('/api/validateRequest', async (req, res) => { +app.post('/validateRequest', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -4751,7 +3505,7 @@ app.post('/api/validateRequest', async (req, res) => { console.log('2. Token obtenu ?', accessToken ? 'OUI' : 'NON'); if (accessToken && request.collaborateur_email) { - const fromEmail = 'gtanoreply@ensup.eu'; + const fromEmail = 'noreply@ensup.eu'; const collaborateurNom = `${request.prenom} ${request.nom}`; const validateurNom = `${validator[0].prenom} ${validator[0].nom}`; @@ -4879,7 +3633,7 @@ app.post('/api/validateRequest', async (req, res) => { } }); -app.get('/api/testRestoration', async (req, res) => { +app.get('/testRestoration', async (req, res) => { const conn = await pool.getConnection(); try { const { demande_id, collab_id } = req.query; @@ -4959,76 +3713,15 @@ 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('/api/getTeamLeaves', async (req, res) => { +app.get('/getTeamLeaves', async (req, res) => { try { - let { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query; + const { user_id: userIdParam, role: roleParam, selectedCampus, selectedSociete, selectedService } = req.query; console.log(`🔍 Paramètres reçus: user_id=${userIdParam}, role=${roleParam}, selectedCampus=${selectedCampus}`); @@ -5098,141 +3791,34 @@ app.get('/api/getTeamLeaves', async (req, res) => { let query, params; const filters = {}; - - // ======================================== // CAS 1: PRESIDENT, ADMIN, RH // ======================================== - // ======================================== - // 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}`); + if (role === 'president' || role === 'admin' || role === 'rh') { + console.log("CAS 1: President/Admin/RH - Vue globale"); - // ======================================== - // 🔧 LISTE COMPLÈTE DES FILTRES DISPONIBLES - // ======================================== - - // 1️⃣ SOCIÉTÉS (toutes disponibles) - const [societesList] = await conn.query(` - SELECT DISTINCT Nom - FROM Societe - ORDER BY Nom - `); - filters.societes = societesList.map(s => s.Nom); - console.log('📊 Sociétés disponibles:', filters.societes); - - // 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); + const [campusList] = await conn.query(`SELECT DISTINCT Nom FROM Campus ORDER BY Nom`); 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); - } + const [societesList] = await conn.query(`SELECT DISTINCT Nom FROM Societe ORDER BY Nom`); + filters.societes = societesList.map(s => s.Nom); - // 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); + const [servicesList] = await conn.query(`SELECT DISTINCT Nom FROM Services ORDER BY Nom`); filters.services = servicesList.map(s => s.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); + 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 + `); filters.employees = employeesList.map(e => ({ name: e.fullname, @@ -5241,207 +3827,216 @@ app.get('/api/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 ${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); + 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 = []; } - - // ======================================== - // CAS 3: COLLABORATEUR + // CAS 2: DIRECTEUR/DIRECTRICE DE CAMPUS // ======================================== - // Dans la route /getTeamLeaves, modifiez la section CAS 3: COLLABORATEUR - // ======================================== - // CAS 3: COLLABORATEUR - // ======================================== - // ======================================== - // CAS 3: COLLABORATEUR - // ======================================== - else if (role === 'collaborateur' || role === 'validateur' || role === 'apprenti') { - console.log("CAS 3: Collaborateur/Apprenti avec filtres avancés"); + else if (role === '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}`); - const serviceNom = userInfo.serviceNom || 'Non défini'; - const campusNom = userInfo.campusNom || 'Non défini'; - const societeNom = userInfo.societeNom || 'Non défini'; + filters.societes = ['Ensup', 'Ensup Solution et Support']; - 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 servicesQuery; let servicesParams = []; - if (selectedSociete && selectedSociete !== 'all') { - servicesQuery += ` AND ca.SocieteId = (SELECT Id FROM Societe WHERE Nom = ? LIMIT 1)`; - servicesParams.push(selectedSociete); + 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 (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); - // ⭐ 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 employeesQuery; let employeesParams = []; - let employeesConditions = []; - // ⭐ N'ajouter les filtres QUE si différents de "all" - if (selectedSociete && selectedSociete !== 'all') { - employeesConditions.push('so.Nom = ?'); - employeesParams.push(selectedSociete); + 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]; + } } - 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, @@ -5449,74 +4044,343 @@ app.get('/api/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); - // ⭐ QUERY DES CONGÉS (avec mêmes filtres conditionnels) - let queryConditions = `WHERE dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`; - params = []; - let congesConditions = []; + let whereConditions = [`dc.Statut IN ('Validé', 'Validée', 'Valide', 'En attente')`]; + let whereParams = []; - // ⭐ 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 (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); } if (selectedService && selectedService !== 'all') { - congesConditions.push('s.Nom = ?'); - params.push(selectedService); - } - - if (congesConditions.length > 0) { - queryConditions += ` AND ${congesConditions.join(' AND ')}`; + 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 - ${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); + 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; } - + // ======================================== + // 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 @@ -5726,83 +4590,154 @@ app.get('/api/getTeamLeaves', async (req, res) => { -// ================================================ -// ROUTE DE SYNCHRONISATION INITIALE (CORRIGÉE) -// ================================================ - - -app.post('/api/initial-sync', async (req, res) => { - const conn = await pool.getConnection(); +app.post('/initial-sync', async (req, res) => { try { - const email = req.body.mail || req.body.userPrincipalName; - const entraId = req.body.id; + const accessToken = await getGraphToken(); + if (!accessToken) return res.json({ success: false, message: 'Impossible obtenir token' }); - console.log('🔄 Initial Sync pour:', email); + if (req.body.userPrincipalName || req.body.mail) { + const userEmail = req.body.mail || req.body.userPrincipalName; + const entraUserId = req.body.id; - // 1. Chercher user - const [users] = await conn.query('SELECT * FROM CollaborateurAD WHERE email = ?', [email]); + console.log(`🔄 Synchronisation utilisateur: ${userEmail}`); - 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 (Avec IGNORE pour éviter crash duplicate) - // On utilise INSERT IGNORE ou ON DUPLICATE KEY UPDATE pour ne jamais planter - 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() + // ⭐ Insertion avec SocieteId (ajuster selon votre logique) + await pool.query(` + INSERT INTO CollaborateurAD + (entraUserId, prenom, nom, email, service, description, role, SocieteId) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + prenom=?, nom=?, email=?, service=?, description=? `, [ - entraId, - email, - req.body.givenName || '', - req.body.surname || '' + entraUserId, + req.body.givenName, + req.body.surname, + userEmail, + req.body.department, + req.body.jobTitle, + 'Collaborateur', + null, // ⭐ À ajuster selon votre logique métier + req.body.givenName, + req.body.surname, + userEmail, + req.body.department, + req.body.jobTitle ]); - // Si insertId est 0 (car update), on refait un select - 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'; + const [userRows] = await pool.query(` + SELECT + ca.id as localUserId, + ca.entraUserId, + ca.prenom, + ca.nom, + ca.email, + ca.role, + s.Nom as service, + ca.TypeContrat as typeContrat, + ca.DateEntree as dateEntree, + ca.description, + ca.CampusId, + ca.SocieteId, + so.Nom as societe_nom + FROM CollaborateurAD ca + LEFT JOIN Services s ON ca.ServiceId = s.Id + LEFT JOIN Societe so ON ca.SocieteId = so.Id + WHERE ca.email = ? + `, [userEmail]); + + if (userRows.length === 0) { + return res.json({ + success: false, + message: 'Utilisateur synchronisé mais introuvable en BDD' + }); } - console.log('✅ User créé/récupéré:', userId); + + const userData = userRows[0]; + + console.log(`✅ Utilisateur synchronisé:`, userData); + + return res.json({ + success: true, + message: 'Utilisateur synchronisé', + localUserId: userData.localUserId, + role: userData.role, + service: userData.service, + typeContrat: userData.typeContrat, + dateEntree: userData.dateEntree, + societeId: userData.SocieteId, + societeNom: userData.societe_nom, + user: userData + }); } + // Full sync + console.log('🔄 Full sync de tous les membres du groupe...'); + + const groupResponse = await axios.get( + `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`, + { headers: { Authorization: `Bearer ${accessToken}` } } + ); + const group = groupResponse.data; + + 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; + + let usersInserted = 0; + for (const m of members) { + if (!m.mail) continue; + + await pool.query(` + INSERT INTO CollaborateurAD ( + entraUserId, prenom, nom, email, service, description, role, SocieteId + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + prenom=?, nom=?, email=?, service=?, description=? + `, [ + m.id, + m.givenName || '', + m.surname || '', + 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 + ]); + + usersInserted++; + } + + console.log(`✅ Full sync terminée: ${usersInserted} utilisateurs`); + res.json({ success: true, - localUserId: userId, - role: userRole + message: 'Full synchronisation terminée', + groupe_sync: group.displayName, + users_sync: usersInserted }); } catch (error) { - console.error('❌ CRASH initial-sync:', error); - // On renvoie un succès fake pour ne pas bloquer le frontend - res.json({ - success: true, - localUserId: 1, - role: 'Secours' + console.error('❌ Erreur sync:', error); + res.status(500).json({ + success: false, + message: 'Erreur sync', + error: error.message }); - } finally { - if (conn) conn.release(); } }); - - // ======================================== // NOUVELLES ROUTES ADMINISTRATION RTT // ======================================== -app.get('/api/getAllCollaborateurs', async (req, res) => { +app.get('/getAllCollaborateurs', async (req, res) => { try { const [collaborateurs] = await pool.query(` SELECT @@ -5839,7 +4774,7 @@ app.get('/api/getAllCollaborateurs', async (req, res) => { } }); -app.post('/api/updateTypeContrat', async (req, res) => { +app.post('/updateTypeContrat', async (req, res) => { try { const { collaborateur_id, type_contrat } = req.body; @@ -5891,7 +4826,7 @@ app.post('/api/updateTypeContrat', async (req, res) => { } }); -app.get('/api/getConfigurationRTT', async (req, res) => { +app.get('/getConfigurationRTT', async (req, res) => { try { const annee = parseInt(req.query.annee || new Date().getFullYear()); const [configs] = await pool.query( @@ -5907,7 +4842,7 @@ app.get('/api/getConfigurationRTT', async (req, res) => { } }); -app.post('/api/updateConfigurationRTT', async (req, res) => { +app.post('/updateConfigurationRTT', async (req, res) => { try { const { annee, typeContrat, joursAnnuels } = req.body; if (!annee || !typeContrat || !joursAnnuels) { @@ -5930,11 +4865,11 @@ app.post('/api/updateConfigurationRTT', async (req, res) => { } }); -app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, res) => { - let connection; +app.post('/updateRequest', async (req, res) => { + const conn = await pool.getConnection(); + try { - console.log('📥 Body reçu:', req.body); - console.log('📎 Fichiers reçus:', req.files); + await conn.beginTransaction(); const { requestId, @@ -5946,399 +4881,306 @@ app.post('/api/updateRequest', upload.array('medicalDocuments', 5), async (req, userId, userEmail, userName, - accessToken + accessToken, + repartition // ⭐ NOUVEAU - Ajout de la répartition } = req.body; - // Validation - if (!requestId || !leaveType || !startDate || !endDate || !businessDays || !userId) { - return res.status(400).json({ - success: false, - message: '❌ Données manquantes' - }); - } + console.log('\n📝 === MODIFICATION DEMANDE ==='); + console.log('Demande ID:', requestId); + console.log('Utilisateur:', userName); + console.log('Nouvelle répartition:', repartition); - connection = await pool.getConnection(); - await connection.beginTransaction(); - - console.log('🔍 ÉTAPE 1: Récupération de la demande originale...'); - - // Récupérer la demande originale - const [originalRequest] = await connection.query( + // 1. Vérifier que la demande existe et est "En attente" + const [existingRequest] = await conn.query( 'SELECT * FROM DemandeConge WHERE Id = ? AND CollaborateurADId = ?', [requestId, userId] ); - if (originalRequest.length === 0) { - await connection.rollback(); + if (existingRequest.length === 0) { + await conn.rollback(); + conn.release(); return res.status(404).json({ success: false, - message: '❌ Demande introuvable' + message: 'Demande introuvable' }); } - const original = originalRequest[0]; + const request = existingRequest[0]; - console.log('🔙 ÉTAPE 2: Suppression des anciennes déductions...'); - - // Restaurer les compteurs de l'ancienne demande - if (original.TypeCongeId !== 3) { // Pas pour Arrêt maladie - const [oldRepartition] = await connection.query( - 'SELECT * FROM DeductionDetails WHERE DemandeCongeId = ?', - [requestId] - ); - - console.log('📊 Ancienne répartition trouvée:', oldRepartition); - - if (oldRepartition && oldRepartition.length > 0) { - for (const rep of oldRepartition) { - const typeCongeId = rep.TypeCongeId; - const annee = rep.Annee; - const joursUtilises = rep.JoursUtilises; - - // Restaurer dans CompteurConges - await connection.query( - `UPDATE CompteurConges - SET Solde = Solde + ?, - DerniereMiseAJour = NOW() - WHERE CollaborateurADId = ? - AND TypeCongeId = ? - AND Annee = ?`, - [joursUtilises, userId, typeCongeId, annee] - ); - console.log(`✅ Restauré ${joursUtilises}j dans TypeCongeId=${typeCongeId}, Annee=${annee}`); - } - } - - // Supprimer l'ancienne répartition - await connection.query( - 'DELETE FROM DeductionDetails WHERE DemandeCongeId = ?', - [requestId] - ); + 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('📝 ÉTAPE 3: Mise à jour de la demande...'); + // 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 + }); + } - // Mettre à jour la demande - await connection.query( + // 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]); + + // 4. METTRE À JOUR LA DEMANDE + console.log('\n📝 ÉTAPE 3: Mise à jour de la demande...'); + await conn.query( `UPDATE DemandeConge - SET TypeCongeId = ?, - DateDebut = ?, + SET DateDebut = ?, DateFin = ?, Commentaire = ?, - NombreJours = ?, - Statut = 'En attente', - DateValidation = NOW() + NombreJours = ? WHERE Id = ?`, - [leaveType, startDate, endDate, reason || '', businessDays, requestId] + [startDate, endDate, reason || null, businessDays, requestId] ); - console.log('📊 ÉTAPE 4: Calcul de la nouvelle répartition...'); + // 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; - let newRepartition = []; - - // Calculer la nouvelle répartition (seulement pour CP et RTT) - if (parseInt(leaveType) === 1 || parseInt(leaveType) === 2) { - // Récupérer les compteurs actuels pour l'année en cours - const currentYear = new Date().getFullYear(); - const previousYear = currentYear - 1; - - // Récupérer depuis CompteurConges - const [countersN] = await connection.query( - 'SELECT Solde, SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, leaveType, currentYear] - ); - - const [countersN1] = await connection.query( - 'SELECT SoldeReporte FROM CompteurConges WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?', - [userId, leaveType, previousYear] - ); - - const soldeN = countersN.length > 0 ? parseFloat(countersN[0].Solde || 0) : 0; - const soldeN1 = countersN1.length > 0 ? parseFloat(countersN1[0].SoldeReporte || 0) : 0; - - console.log(`💡 Soldes disponibles: N-1=${soldeN1}j, N=${soldeN}j`); - - // Calculer la répartition - if (parseInt(leaveType) === 1) { // Congé payé - const joursNecessaires = parseFloat(businessDays); - - // Vérifier si les soldes sont suffisants - if (soldeN1 + soldeN < joursNecessaires) { - throw new Error(`Solde insuffisant: ${(soldeN1 + soldeN).toFixed(2)}j disponibles, ${joursNecessaires}j demandés`); - } - - // Utiliser d'abord N-1, puis N - let reste = joursNecessaires; - - if (reste > 0 && soldeN1 > 0) { - const joursN1 = Math.min(reste, soldeN1); - newRepartition.push({ - typeCongeId: leaveType, - annee: previousYear, - jours: joursN1, - typeDeduction: 'Report N-1' - }); - reste -= joursN1; - } - - if (reste > 0 && soldeN > 0) { - const joursN = Math.min(reste, soldeN); - newRepartition.push({ - typeCongeId: leaveType, - annee: currentYear, - jours: joursN, - typeDeduction: 'Année N' - }); - reste -= joursN; - } - - } else if (parseInt(leaveType) === 2) { // RTT - const joursNecessaires = parseFloat(businessDays); - - if (soldeN < joursNecessaires) { - throw new Error(`Solde RTT insuffisant: ${soldeN.toFixed(2)}j disponibles, ${joursNecessaires}j demandés`); - } - - newRepartition = [{ - typeCongeId: leaveType, - annee: currentYear, - jours: joursNecessaires, - typeDeduction: 'Année N' - }]; - } - - console.log('📊 Nouvelle répartition calculée:', JSON.stringify(newRepartition, null, 2)); - - // Vérification finale - if (!Array.isArray(newRepartition) || newRepartition.length === 0) { - console.error('❌ ERREUR: newRepartition invalide:', newRepartition); - throw new Error('Répartition invalide'); - } - - console.log('📉 ÉTAPE 5: Déduction des nouveaux compteurs...'); - - // Déduire les nouveaux compteurs - for (const rep of newRepartition) { - // Validation de chaque élément - if (!rep || typeof rep !== 'object' || !rep.typeCongeId || !rep.annee || rep.jours === undefined) { - console.error('❌ Élément invalide:', rep); - throw new Error(`Élément de répartition invalide: ${JSON.stringify(rep)}`); - } - - // Sauvegarder dans DeductionDetails - await connection.query( - `INSERT INTO DeductionDetails (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, ?, ?)`, - [requestId, rep.typeCongeId, rep.annee, rep.typeDeduction, rep.jours] + const [typeRow] = await conn.query( + 'SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', + [name] ); - // Déduire du compteur - await connection.query( - `UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?), - DerniereMiseAJour = NOW() - WHERE CollaborateurADId = ? - AND TypeCongeId = ? - AND Annee = ?`, - [rep.jours, userId, rep.typeCongeId, rep.annee] - ); - console.log(`✅ Déduit ${rep.jours}j de TypeCongeId=${rep.typeCongeId}, Annee=${rep.annee}`); + 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' + ] + ); + console.log(` ✓ ${name}: ${rep.NombreJours}j`); + } } } - console.log('📧 ÉTAPE 6: Envoi des emails...'); + // 6. ⭐ DÉDUIRE LES NOUVEAUX COMPTEURS + console.log('\n📉 ÉTAPE 5: Déduction des nouveaux compteurs...'); + const currentYear = new Date().getFullYear(); - // Récupérer les infos du manager - 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 = ?`, + 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`); + }); + } + } + } + + // 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(); + const managerEmail = hierarchie[0]?.managerEmail; + const managerName = hierarchie[0] ? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}` : 'Manager'; - // Envoyer les emails (sans bloquer la réponse) - if (accessToken && managerInfo.length > 0) { - const manager = managerInfo[0]; + await conn.commit(); + console.log('\n✅ Modification terminée avec succès\n'); - // Obtenir un token Graph - const graphToken = await getGraphToken(); + // 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 (graphToken) { - // 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)
-
-

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)); + 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 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)); - } else { - console.warn('⚠️ Impossible d\'obtenir un token Graph - emails non envoyés'); +
+

✨ 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); } } + conn.release(); + res.json({ success: true, - message: '✅ Demande modifiée avec succès', - repartition: newRepartition + message: 'Demande modifiée avec succès' }); } catch (error) { - if (connection) { - await connection.rollback(); - } + await conn.rollback(); + if (conn) conn.release(); console.error('❌ Erreur updateRequest:', error); res.status(500).json({ success: false, - message: error.message || 'Erreur lors de la modification' + message: 'Erreur serveur lors de la modification', + error: error.message }); - } 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('/api/deleteRequest', async (req, res) => { +app.post('/deleteRequest', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); @@ -6354,16 +5196,15 @@ app.post('/api/deleteRequest', async (req, res) => { }); } - console.log('\n🗑️ === ANNULATION DEMANDE ==='); + console.log('\n🗑️ === SUPPRESSION DEMANDE ==='); console.log('Demande ID:', requestId); console.log('User ID:', userId); - // 1️⃣ Vérifier que la demande existe + // 1. Vérifier que la demande existe et appartient à l'utilisateur const [existingRequest] = await conn.query( - `SELECT d.*, - DATE_FORMAT(d.DateDebut, '%Y-%m-%d') as DateDebut, - DATE_FORMAT(d.DateFin, '%Y-%m-%d') as DateFin + `SELECT d.*, tc.Nom as TypeConge FROM DemandeConge d + LEFT JOIN TypeConge tc ON d.TypeCongeId = tc.Id WHERE d.Id = ? AND d.CollaborateurADId = ?`, [requestId, userId] ); @@ -6379,39 +5220,11 @@ app.post('/api/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(`📅 Date début: ${dateDebut.toLocaleDateString('fr-FR')}`); - console.log(`📅 Aujourd'hui: ${aujourdhui.toLocaleDateString('fr-FR')}`); - // ❌ BLOQUER SI DATE DÉJÀ PASSÉE OU AUJOURD'HUI - if (dateDebut <= aujourdhui) { - 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 COMPLÈTE - 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]); - - console.log(`📊 Répartition de la demande:`, repartition); - - // 3️⃣ RESTAURER LES COMPTEURS (sauf si déjà Refusée/Annulée) - if (requestStatus !== 'Refusée' && requestStatus !== 'Annulée') { + // 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); @@ -6420,27 +5233,11 @@ app.post('/api/deleteRequest', async (req, res) => { } } catch (restoreError) { console.error('❌ Erreur restauration:', restoreError.message); - await conn.rollback(); - conn.release(); - return res.status(500).json({ - success: false, - message: 'Erreur lors de la restauration des compteurs', - error: restoreError.message - }); + // Ne pas bloquer la suppression si la restauration échoue } } - // 4️⃣ RÉCUPÉRER INFOS COLLABORATEUR ET MANAGER - const [collabInfo] = await conn.query( - 'SELECT email, prenom, nom FROM CollaborateurAD WHERE id = ?', - [userId] - ); - - const collabEmail = collabInfo.length > 0 ? collabInfo[0].email : userEmail; - const collabName = collabInfo.length > 0 - ? `${collabInfo[0].prenom} ${collabInfo[0].nom}` - : userName; - + // 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 @@ -6455,204 +5252,93 @@ app.post('/api/deleteRequest', async (req, res) => { ? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}` : 'Manager'; - // 5️⃣ 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] - ); + // 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]); - console.log(`✅ Demande ${requestId} marquée comme Annulée`); + // 5. Supprimer la demande + await conn.query('DELETE FROM DemandeConge WHERE Id = ?', [requestId]); + console.log(`✅ Demande ${requestId} supprimée définitivement`); await conn.commit(); - conn.release(); - // 6️⃣ ENVOI DES EMAILS - let emailsSent = { collaborateur: false, manager: false }; - const graphToken = await getGraphToken(); + // 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é. +

- if (graphToken) { - 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}`; +
+

📋 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}
+
- // 📊 FORMATER LA RÉPARTITION POUR L'EMAIL - 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 : -
+

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

+
+ ` + }, + toRecipients: [{ emailAddress: { address: managerEmail } }], + saveToSentItems: false + } + }; - ${requestStatus !== 'Refusée' && requestStatus !== 'Annulée' ? ` -
-

- ✅ Vos compteurs ont été restaurés
- Les jours de congé sont à nouveau disponibles dans vos soldes. -

-
- ` : ''} + await axios.post('https://graph.microsoft.com/v1.0/me/sendMail', emailBody, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }); -
-

- 📧 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); - } + console.log('📧 Email de notification envoyé au manager'); + } catch (emailError) { + console.error('❌ Erreur email manager (non bloquant):', emailError.message); } - - // 📧 EMAIL AU 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é en attente'}. -

- -
-

📋 Demande annulée

- - - - - - - - - - - - - - - - - ${repartitionText} -
Statut initial :${requestStatus}
Période :${datesPeriode}
Durée totale :${request.NombreJours} jour(s)
- Répartition : -
-
- - ${isValidated ? ` -
-

- ⚠️ Attention : Ce congé avait été validé.
- Les compteurs du collaborateur ont été automatiquement restaurés. -

-
- ` : ` -
-

- ℹ️ Cette demande était en attente de validation.
- Les compteurs ont été restauré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); - } - } else if (!managerEmail) { - console.log('⚠️ Email manager introuvable'); - } - } else { - console.warn('⚠️ Impossible d\'obtenir un token Graph - emails non envoyés'); } - // RETOURNER LA RÉPONSE + conn.release(); + res.json({ success: true, - message: 'Demande annulée avec succès', - counterRestored: requestStatus !== 'Refusée' && requestStatus !== 'Annulée', - emailsSent: emailsSent, - managerNotified: requestStatus !== 'Refusée' && requestStatus !== 'Annulée', - repartition: repartition.map(r => ({ - type: r.TypeNom, - jours: r.NombreJours, - periode: r.PeriodeJournee - })) + message: 'Demande supprimée avec succès', + counterRestored: ['En attente', 'Valid', 'Validé', 'Valide'].includes(requestStatus) }); } catch (error) { @@ -6661,12 +5347,14 @@ app.post('/api/deleteRequest', async (req, res) => { console.error('❌ Erreur deleteRequest:', error); res.status(500).json({ success: false, - message: 'Erreur lors de l\'annulation', + message: 'Erreur lors de la suppression', error: error.message }); } }); -app.get('/api/exportCompteurs', async (req, res) => { + + +app.get('/exportCompteurs', async (req, res) => { try { const dateRef = req.query.dateRef || new Date().toISOString().split('T')[0]; const conn = await pool.getConnection(); @@ -6764,1006 +5452,11 @@ app.get('/api/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('/api/getCongesAnticipes', async (req, res) => { +app.get('/getCongesAnticipes', async (req, res) => { try { const userIdParam = req.query.user_id; @@ -7946,84 +5639,7 @@ app.get('/api/getCongesAnticipes', 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) => { +app.get('/getStatistiquesCompteurs', async (req, res) => { try { const conn = await pool.getConnection(); const currentYear = new Date().getFullYear(); @@ -8148,7 +5764,7 @@ async function hasCompteRenduAccess(userId) { // Récupérer les jours du mois // GET - Récupérer les données du compte-rendu -app.get('/api/getCompteRenduActivites', async (req, res) => { +app.get('/getCompteRenduActivites', async (req, res) => { const { user_id, annee, mois } = req.query; try { @@ -8209,7 +5825,7 @@ app.get('/api/getCompteRenduActivites', async (req, res) => { // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE -app.post('/api/saveCompteRenduJour', async (req, res) => { +app.post('/saveCompteRenduJour', async (req, res) => { const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body; try { @@ -8308,7 +5924,7 @@ app.post('/api/saveCompteRenduJour', async (req, res) => { }); // POST - Saisie en masse avec AUTO-VERROUILLAGE -app.post('/api/saveCompteRenduMasse', async (req, res) => { +app.post('/saveCompteRenduMasse', async (req, res) => { const { user_id, annee, mois, jours, rh_override } = req.body; try { @@ -8403,7 +6019,7 @@ app.post('/api/saveCompteRenduMasse', async (req, res) => { } }); -app.post('/api/deverrouillerJour', async (req, res) => { +app.post('/deverrouillerJour', async (req, res) => { const { user_id, date, rh_user_id } = req.body; try { @@ -8437,7 +6053,7 @@ app.post('/api/deverrouillerJour', async (req, res) => { // POST - Verrouiller (RH uniquement) -app.post('/api/verrouillerCompteRendu', async (req, res) => { +app.post('/verrouillerCompteRendu', async (req, res) => { const { user_id, annee, mois, rh_user_id } = req.body; try { @@ -8472,7 +6088,7 @@ app.post('/api/verrouillerCompteRendu', async (req, res) => { }); // POST - Déverrouiller (RH uniquement) -app.post('/api/deverrouillerCompteRendu', async (req, res) => { +app.post('/deverrouillerCompteRendu', async (req, res) => { const { user_id, annee, mois, rh_user_id } = req.body; try { @@ -8508,7 +6124,7 @@ app.post('/api/deverrouillerCompteRendu', async (req, res) => { }); // GET - Stats annuelles -app.get('/api/getStatsAnnuelles', async (req, res) => { +app.get('/getStatsAnnuelles', async (req, res) => { const { user_id, annee } = req.query; try { @@ -8541,7 +6157,7 @@ app.get('/api/getStatsAnnuelles', async (req, res) => { }); // GET - Export PDF (RH uniquement) -app.get('/api/exportCompteRenduPDF', async (req, res) => { +app.get('/exportCompteRenduPDF', async (req, res) => { const { user_id, annee, mois } = req.query; try { @@ -8583,208 +6199,6 @@ app.get('/api/exportCompteRenduPDF', async (req, res) => { res.status(500).json({ success: false, message: error.message }); } }); -// 📊 ROUTE POUR L'ESPACE RH - Tous les compteurs détaillés -app.get('/api/getAllDetailedCounters', async (req, res) => { - try { - console.log('📊 Récupération de TOUS les compteurs détaillés pour RH'); - - const conn = await pool.getConnection(); - - // Récupérer tous les collaborateurs actifs - const [collaborateurs] = await conn.query(` - SELECT DISTINCT ca.id, ca.prenom, ca.nom, ca.email, - ca.role, ca.TypeContrat, ca.DateEntree, - s.Nom as service - FROM CollaborateurAD ca - LEFT JOIN Services s ON ca.ServiceId = s.Id - WHERE ca.Actif = 1 OR ca.Actif IS NULL - ORDER BY ca.nom, ca.prenom - `); - - console.log(`👥 ${collaborateurs.length} collaborateurs trouvés`); - - const resultats = []; - const currentYear = new Date().getFullYear(); - const previousYear = currentYear - 1; - - for (const collab of collaborateurs) { - try { - // Récupérer les compteurs détaillés de ce collaborateur - // en utilisant la MÊME logique que getDetailedLeaveCounters - 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']); - const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1`, ['Récupération']); - - // CP N - if (cpType.length > 0) { - const acquisCP = calculerAcquisitionCP(new Date(), collab.DateEntree); - - const [consommeN] = 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 IN ('Année N', 'Année N', 'Année actuelle N') - AND dd.TypeDeduction NOT IN ('Accum. Récup', 'Accum. Recup', 'N Anticipé') - AND dc.Statut != 'Refusé' - `, [collab.id, cpType[0].Id, currentYear]); - - const pris = parseFloat(consommeN.totalConsomme || 0); - const soldeDisponible = Math.max(0, acquisCP - pris); - - resultats.push({ - collaborateurId: collab.id, - employe: `${collab.prenom} ${collab.nom}`, - email: collab.email, - service: collab.service || 'Non assigné', - typeConge: 'Congé payé', - annee: currentYear, - total: parseFloat(acquisCP.toFixed(2)), - solde: parseFloat(soldeDisponible.toFixed(2)), - consomme: parseFloat(pris.toFixed(2)), - role: collab.role, - typeContrat: collab.TypeContrat - }); - } - - // CP N-1 - if (cpType.length > 0) { - const [cpN1] = await conn.query(` - SELECT Annee, SoldeReporte, Total, Solde - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collab.id, cpType[0].Id, previousYear]); - - if (cpN1.length > 0 && parseFloat(cpN1[0].Solde || 0) > 0) { - const soldeReporte = parseFloat(cpN1[0].Solde || 0); - - const [consommeN1] = 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 IN ('Année N-1', 'Année N-1', 'Report N-1') - AND dc.Statut != 'Refusé' - `, [collab.id, cpType[0].Id, previousYear]); - - const pris = parseFloat(consommeN1.totalConsomme || 0); - const soldeActuel = Math.max(0, soldeReporte - pris); - - resultats.push({ - collaborateurId: collab.id, - employe: `${collab.prenom} ${collab.nom}`, - email: collab.email, - service: collab.service || 'Non assigné', - typeConge: 'Congé payé', - annee: previousYear, - total: parseFloat(soldeReporte.toFixed(2)), - solde: parseFloat(soldeActuel.toFixed(2)), - consomme: parseFloat(pris.toFixed(2)), - role: collab.role, - typeContrat: collab.TypeContrat - }); - } - } - - // RTT N - if (rttType.length > 0 && collab.role !== 'Apprenti') { - const rttData = await calculerAcquisitionRTT(conn, collab.id, new Date()); - - const [consommeRTT] = 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 NOT IN ('Accum. Récup', 'Accum. Recup', 'Récup Dosée', 'N Anticipé') - AND dc.Statut != 'Refusé' - `, [collab.id, rttType[0].Id, currentYear]); - - const pris = parseFloat(consommeRTT.totalConsomme || 0); - const soldeDisponible = Math.max(0, rttData.acquisition - pris); - - resultats.push({ - collaborateurId: collab.id, - employe: `${collab.prenom} ${collab.nom}`, - email: collab.email, - service: collab.service || 'Non assigné', - typeConge: 'RTT', - annee: currentYear, - total: parseFloat(rttData.acquisition.toFixed(2)), - solde: parseFloat(soldeDisponible.toFixed(2)), - consomme: parseFloat(pris.toFixed(2)), - role: collab.role, - typeContrat: collab.TypeContrat - }); - } - - // Récupérations N - if (recupType.length > 0) { - 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é' - `, [collab.id, recupType[0].Id, currentYear]); - - const [consomRecup] = await conn.query(` - SELECT COALESCE(SUM(dd.JoursUtilises), 0) as totalConsom - FROM DeductionDetails dd - JOIN DemandeConge dc ON dd.DemandeCongeId = dc.Id - WHERE dc.CollaborateurADId = ? - AND dd.TypeCongeId = ? - AND dd.Annee = ? - AND dd.TypeDeduction IN ('Récup Dosée', 'Recup Dosee') - AND dc.Statut != 'Refusé' - `, [collab.id, recupType[0].Id, currentYear]); - - const acquis = parseFloat(accumRecup.totalAccum || 0); - const pris = parseFloat(consomRecup.totalConsom || 0); - const solde = Math.max(0, acquis - pris); - - if (acquis > 0 || pris > 0 || solde > 0) { - resultats.push({ - collaborateurId: collab.id, - employe: `${collab.prenom} ${collab.nom}`, - email: collab.email, - service: collab.service || 'Non assigné', - typeConge: 'Récupération', - annee: currentYear, - total: parseFloat(acquis.toFixed(2)), - solde: parseFloat(solde.toFixed(2)), - consomme: parseFloat(pris.toFixed(2)), - role: collab.role, - typeContrat: collab.TypeContrat - }); - } - } - - } catch (collabError) { - console.error(`❌ Erreur pour ${collab.prenom} ${collab.nom}:`, collabError.message); - } - } - - conn.release(); - - console.log(`✅ ${resultats.length} compteurs retournés`); - res.json(resultats); - - } catch (error) { - console.error('❌ Erreur getAllDetailedCounters:', error); - res.status(500).json({ error: error.message }); - } -}); - - @@ -8792,10 +6206,40 @@ app.get('/api/getAllDetailedCounters', async (req, res) => { // DÉMARRAGE DU SERVEUR // ======================================== -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)}`); +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'); }); \ 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 deleted file mode 100644 index 8e0a24c..0000000 Binary files a/project/public/Backend/uploads/medical/medical-1763460916289-157625577.pdf and /dev/null differ diff --git a/project/public/Backend/useSSENotifications.js b/project/public/Backend/useSSENotifications.js deleted file mode 100644 index 879e222..0000000 --- a/project/public/Backend/useSSENotifications.js +++ /dev/null @@ -1,38 +0,0 @@ -// 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 40b1bcf..22fd15b 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 - CORRIGÉE) +// webhook-utils.js (VERSION ES MODULES) // Pour projets avec "type": "module" dans package.json import axios from 'axios'; @@ -65,7 +65,6 @@ 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 new file mode 100644 index 0000000..bbcc787 --- /dev/null +++ b/project/public/php/Dockerfile.backend @@ -0,0 +1,14 @@ +# 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 new file mode 100644 index 0000000..eaa44e4 --- /dev/null +++ b/project/public/php/check-user-groups.php @@ -0,0 +1,147 @@ +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 new file mode 100644 index 0000000..a3ab16f --- /dev/null +++ b/project/public/php/db.php @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..8a65dad --- /dev/null +++ b/project/public/php/getAllTeamRequests.php @@ -0,0 +1,103 @@ +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 new file mode 100644 index 0000000..844feae --- /dev/null +++ b/project/public/php/getEmploye.php @@ -0,0 +1,52 @@ +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 new file mode 100644 index 0000000..bdb7358 --- /dev/null +++ b/project/public/php/getEmployeRequest.php @@ -0,0 +1,66 @@ +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 new file mode 100644 index 0000000..21d331a --- /dev/null +++ b/project/public/php/getLeaveCounters.php @@ -0,0 +1,163 @@ +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 new file mode 100644 index 0000000..6854373 --- /dev/null +++ b/project/public/php/getNotifications.php @@ -0,0 +1,75 @@ +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 new file mode 100644 index 0000000..92a7941 --- /dev/null +++ b/project/public/php/getPendingRequests.php @@ -0,0 +1,159 @@ +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 new file mode 100644 index 0000000..b5a02d2 --- /dev/null +++ b/project/public/php/getRequests.php @@ -0,0 +1,133 @@ +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 new file mode 100644 index 0000000..f10b181 --- /dev/null +++ b/project/public/php/getTeamLeaves.php @@ -0,0 +1,228 @@ +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 new file mode 100644 index 0000000..e6a910b --- /dev/null +++ b/project/public/php/getTeamMembers.php @@ -0,0 +1,116 @@ +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 new file mode 100644 index 0000000..ba8857c --- /dev/null +++ b/project/public/php/initial-sync.php @@ -0,0 +1,104 @@ +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 new file mode 100644 index 0000000..cbb2e1f --- /dev/null +++ b/project/public/php/login.php @@ -0,0 +1,152 @@ +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 new file mode 100644 index 0000000..bd737d0 --- /dev/null +++ b/project/public/php/manualResetCounters.php @@ -0,0 +1,116 @@ + + + + + + + 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 new file mode 100644 index 0000000..b4b8158 --- /dev/null +++ b/project/public/php/markNotificationRead.php @@ -0,0 +1,62 @@ +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 new file mode 100644 index 0000000..60e4cd9 --- /dev/null +++ b/project/public/php/resetLeaveCounters.php @@ -0,0 +1,228 @@ +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 new file mode 100644 index 0000000..5595b95 --- /dev/null +++ b/project/public/php/submitLeaveRequest.php @@ -0,0 +1,293 @@ +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 new file mode 100644 index 0000000..e69de29 diff --git a/project/public/php/validateRequest.php b/project/public/php/validateRequest.php new file mode 100644 index 0000000..74124ba --- /dev/null +++ b/project/public/php/validateRequest.php @@ -0,0 +1,157 @@ +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 0c402e0..8c57e02 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, useAuth } from './context/AuthContext'; // ⭐ Ajout de useAuth +import { AuthProvider } from './context/AuthContext'; import Dashboard from './pages/Dashboard'; import Login from './pages/Login'; import Requests from './pages/Requests'; @@ -9,103 +9,87 @@ 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'; -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 */} - } /> - - - ); -} +import CompteRenduActivites from './pages/CompteRenduActivite'; // ⭐ Ajout 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 8ab94fc..e9e1b02 100644 --- a/project/src/AuthConfig.js +++ b/project/src/AuthConfig.js @@ -1,47 +1,22 @@ // 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", - 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}`, + 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" }, cache: { - cacheLocation: "sessionStorage", + cacheLocation: "sessionStorage", storeAuthStateInCookie: false, - }, + } }; -// --- Permissions Graph --- export const loginRequest = { scopes: [ "User.Read", - "User.Read.All", - "Group.Read.All", - "GroupMember.Read.All", - "Mail.Send", - ], + "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. + ] }; - -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 e1feb9b..d74ca34 100644 --- a/project/src/components/EditLeaveRequestModal.jsx +++ b/project/src/components/EditLeaveRequestModal.jsx @@ -178,44 +178,30 @@ const EditLeaveRequestModal = ({ try { const formDataToSend = new FormData(); - // ⭐ Ajouter tous les champs texte AVANT les fichiers - formDataToSend.append('requestId', request.id.toString()); - formDataToSend.append('leaveType', leaveType.toString()); + formDataToSend.append('requestId', request.id); + formDataToSend.append('leaveType', parseInt(leaveType)); formDataToSend.append('startDate', startDate); formDataToSend.append('endDate', endDate); - formDataToSend.append('reason', reason || ''); - formDataToSend.append('userId', userId.toString()); + formDataToSend.append('reason', reason); + formDataToSend.append('userId', userId); 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.toString()); + formDataToSend.append('businessDays', daysToSend); - // ⭐ Documents médicaux EN DERNIER - if (medicalDocuments.length > 0) { - medicalDocuments.forEach((file) => { - formDataToSend.append('medicalDocuments', file); - }); - } - - // ⭐ DEBUG : Vérifier le contenu - console.log('📤 FormData à envoyer:'); - for (let pair of formDataToSend.entries()) { - console.log(pair[0], ':', pair[1]); - } - - const response = await fetch('/updateRequest', { - method: 'POST', - // ⭐ NE PAS mettre de Content-Type, le navigateur le fera automatiquement avec boundary - body: formDataToSend + // ⭐ Documents médicaux + medicalDocuments.forEach((file) => { + formDataToSend.append('medicalDocuments', file); }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + const response = await fetch('http://localhost:3000/updateRequest', { + method: 'POST', + body: formDataToSend + }); const data = await response.json(); @@ -236,7 +222,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 deleted file mode 100644 index 20c2f96..0000000 --- a/project/src/components/GlobalTutorial.jsx +++ /dev/null @@ -1,728 +0,0 @@ -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 new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/project/src/components/Layout.jsx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project/src/components/MedicalDocuments.jsx b/project/src/components/MedicalDocuments.jsx index 19fda69..b8a0f0a 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(`/medical-documents/${demandeId}`); + const response = await fetch(`http://localhost:3000/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( - `/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 + const safeCounters = { + availableCP: availableLeaveCounters?.availableCP ?? 0, + availableRTT: availableLeaveCounters?.availableRTT ?? 0 }; - console.log('📊 Compteurs disponibles:', safeCounters); - console.log('📊 Données complètes:', countersData); - useEffect(() => { if (preselectedStartDate || preselectedEndDate) { setFormData(prev => ({ @@ -85,6 +44,7 @@ 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); @@ -110,7 +70,8 @@ const NewLeaveRequestModal = ({ debut: formData.startDate, fin: formData.endDate, joursOuvres: workingDays, - samedis: saturdays + samedis: saturdays, + message: 'Les samedis ne sont comptés QUE si "Récup" est coché' }); } }, [formData.startDate, formData.endDate]); @@ -144,15 +105,20 @@ 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 })); } @@ -160,6 +126,7 @@ const NewLeaveRequestModal = ({ } }; + const handleFileUpload = (e) => { const files = Array.from(e.target.files); const validFiles = []; @@ -203,15 +170,6 @@ 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; @@ -242,82 +200,58 @@ const NewLeaveRequestModal = ({ return false; } + const hasRecup = formData.types.includes('Récup'); const hasABS = formData.types.includes('ABS'); - if (hasABS && formData.types.length > 1) { - setError('Un arrêt maladie ne peut pas être mélangé avec d\'autres types de congés'); + // ⭐ 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)".'); 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 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; - } + // ⭐ VALIDATION RÉPARTITION AMÉLIORÉE + if (formData.types.length > 1) { + const sum = Object.values(repartition).reduce((a, b) => a + b, 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; + console.log('📊 Validation répartition:', { + somme: sum, + attendu: expectedTotal, + joursOuvres: totalDays, + samedisIgnores: !hasRecup ? saturdayCount : 0 }); - } - 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)`); + 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.` : ''}`); return false; } } @@ -327,7 +261,6 @@ const NewLeaveRequestModal = ({ }; - const handleSubmit = async () => { setError(''); @@ -348,13 +281,15 @@ 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' || type === 'Récup') && (periode === 'Matin' || periode === 'Après-midi')) { + if ((type === 'CP' || type === 'RTT') && (periode === 'Matin' || periode === 'Après-midi')) { totalJoursToSend = 0.5; } } @@ -362,31 +297,50 @@ 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) { - const periode = periodeSelection[type] || 'Journée entière'; - - if ((type === 'CP' || type === 'RTT' || type === 'Récup') && + // Un seul type : utiliser soit 0.5 (demi-journée) soit totalDays + const periode = periodeSelection[type]; + if ((type === 'CP' || type === 'RTT') && 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 (${periodeJournee})`); + console.log(`📝 ${type}: ${nombreJours}j (${periodeSelection[type] || 'Journée entière'})`); return { TypeConge: type, NombreJours: nombreJours, - PeriodeJournee: ['CP', 'RTT', 'Récup'].includes(type) ? periodeJournee : 'Journée entière' + PeriodeJournee: ['CP', 'RTT'].includes(type) + ? (periodeSelection[type] || 'Journée entière') + : 'Journée entière' }; }); @@ -399,7 +353,7 @@ const NewLeaveRequestModal = ({ formDataToSend.append('medicalDocuments', file); }); - const response = await fetch('/submitLeaveRequest', { + const response = await fetch('http://localhost:3000/submitLeaveRequest', { method: 'POST', body: formDataToSend }); @@ -422,6 +376,7 @@ const NewLeaveRequestModal = ({ } }; + const handleTypeToggle = (type) => { setFormData(prev => ({ ...prev, @@ -431,74 +386,20 @@ 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)', - // ✅ Afficher seulement le solde actuel (sans anticipé) - available: countersData?.data?.cpN?.solde || 0, - details: countersData?.data?.cpN - }, + { key: 'CP', label: 'Congés payés', available: safeCounters.availableCP }, { key: 'ABS', label: 'Arrêt maladie' }, { key: 'Formation', label: 'Formation' }, - { - key: 'Récup', - label: 'Récupération(s)', - available: countersData?.data?.recupN?.solde || 0 - }, + { 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' } + { 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)' }, ]; - return (
@@ -510,75 +411,34 @@ const NewLeaveRequestModal = ({
- {/* ⭐ BLOC SOLDES DÉTAILLÉS */} - - - {/* Loading */} - {isLoadingCounters && ( -
-
-

Chargement des soldes...

-
- )} -
- {availableTypes.map(type => { - const disabled = isTypeDisabled(type.key); - const tooltip = getDisabledTooltip(type.key); - - return ( - + ))}
- - {formData.types.includes('ABS') && ( -
- -

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

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

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

+ {/* 🎯 AFFICHAGE DU NOMBRE DE JOURS */}
Durée sélectionnée : @@ -640,8 +506,10 @@ 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' : ''}`; @@ -653,17 +521,20 @@ 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

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

{formData.types.map((type) => { - const showPeriode = ['CP', 'RTT', 'Récup'].includes(type); + const showPeriode = ['CP', 'RTT'].includes(type); const currentValue = repartition[type] || 0; return ( @@ -676,10 +547,11 @@ const NewLeaveRequestModal = ({ type="number" step="0.5" min="0" - max={totalDays} + max={type === 'Récup' ? saturdayCount : 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}` : ''} />
@@ -721,6 +593,7 @@ const NewLeaveRequestModal = ({
+ {/* 🎯 AFFICHAGE DU NOMBRE DE JOURS POUR CE TYPE */}
Durée : @@ -738,6 +611,7 @@ const NewLeaveRequestModal = ({
)} +