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 = `
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}
` : ''} +${Nom} vous informe d'une période de formation.
+Période : ${datesPeriode}
+Durée : ${NombreJours} jour(s)
+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.
-Bonjour ${managerName},
+ ++ ${userName} a modifié sa demande de congé. +
- // Email de confirmation au collaborateur - sendMailGraph( - graphToken, - 'gtanoreply@ensup.eu', - userEmail, - 'Confirmation de modification', - ` -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.
-| Type : | +${typesConges} | +
| Du : | +${newStartDate} | +
| Au : | +${newEndDate} | +
| Jours : | +${businessDays} jour(s) | +
| Motif : | +${reason} | +
+ Cette demande est toujours en attente de validation. +
+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}`; +| 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} | +
Bonjour ${collabName},
- -- Votre demande de congé a bien été annulée. -
- -| Période : | -${datesPeriode} | -
| Durée totale : | -${request.NombreJours} jour(s) | -
| - Répartition : - | -|
+ Les compteurs de congés ont été restaurés si nécessaire. +
+
- ✅ Vos compteurs ont été restaurés
- Les jours de congé sont à nouveau disponibles dans vos soldes.
-
- 📧 Cet email est envoyé automatiquement, merci de ne pas y répondre. -
-Bonjour ${managerName},
- -- ${collabName} a annulé ${isValidated ? 'son congé validé' : 'sa demande de congé en attente'}. -
- -| Statut initial : | -${requestStatus} | -
| Période : | -${datesPeriode} | -
| Durée totale : | -${request.NombreJours} jour(s) | -
| - Répartition : - | -|
- ⚠️ 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. -
-Cette opération va réinitialiser TOUS les compteurs de congés selon les règles suivantes :
+Cette action est irréversible !
+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 '';
+ foreach ($data['log'] as $logLine) {
+ echo htmlspecialchars($logLine) . "\n";
+ }
+ echo '' . ($data['message'] ?? 'Erreur inconnue') . '
'; + 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') . "
"; + ?> + ++ + + +
+Découvrez toutes les fonctionnalités en quelques étapes. Ce tutoriel ne s'affichera qu'une seule fois.
-Découvrez maintenant vos différents soldes de congés disponibles.
-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. -
-Découvrez comment gérer {isEmployee ? 'votre équipe' : 'les demandes de congés de 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. -
-Vous pouvez maintenant consulter votre équipe facilement.
-- 💡 Besoin d'aide ? N'hésitez pas à contacter votre manager pour toute question. -
-Découvrez comment visualiser et gérer les congés {canViewAllFilters ? 'de toute l\'entreprise' : 'de votre équipe'}.
-Vous pouvez sélectionner des dates directement dans le calendrier pour créer une demande de congé rapidement.
-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 ! -
-- Ê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".'} -
-Chargement des soldes...
-- Un arrêt maladie ne peut pas être combiné avec d'autres types de congés. -
-- Indiquez la répartition souhaitée (le système vérifiera automatiquement) + La somme doit être égale à {totalDays} jour(s)
- Fichiers sélectionnés ({formData.medicalDocuments.length}) + Fichiers sélectionnés ({formData.medicalDocuments.length}) :
{formData.medicalDocuments.map((file, index) => ({error}
+{error}