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