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

✅ Formation validée

+
+

✅ Formation enregistrée

-
-

Bonjour ${Nom},

-

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

-

Période : ${datesPeriode}

-

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

+
+

Bonjour ${Nom},

+

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

+
+

Type : Formation

+

Période : ${datesPeriode}

+

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

+ ${Commentaire ? `

Description : ${Commentaire}

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

📚 Formation enregistrée

+
+
+

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

+

Période : ${datesPeriode}

+

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

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

🔄 Modification de demande

-
-
-

Bonjour ${manager.Prenom},

-

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

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

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

-
- ` : ''} -

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

-
-

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

-
-
-
- ` - ).catch(err => console.error('❌ Erreur email manager:', err)); + if (compteurExisting.length > 0) { + await conn.query(` + UPDATE CompteurConges + SET Total = Total + ?, + Solde = Solde + ?, + DerniereMiseAJour = NOW() + WHERE Id = ? + `, [recupJours, recupJours, compteurExisting[0].Id]); + } else { + await conn.query(` + INSERT INTO CompteurConges + (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, DerniereMiseAJour) + VALUES (?, ?, ?, ?, ?, 0, NOW()) + `, [userId, recupType[0].Id, currentYear, recupJours, recupJours]); + } - // Email de confirmation au collaborateur - sendMailGraph( - graphToken, - 'gtanoreply@ensup.eu', - userEmail, - '✅ Confirmation de modification', - ` -
-
-

✅ Demande modifiée

-
-
-

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

-

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

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

Elle est maintenant en attente de validation.

-
-

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

-
-
-
- ` - ).catch(err => console.error('❌ Erreur email collaborateur:', err)); + await conn.query(` + INSERT INTO DeductionDetails + (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) + VALUES (?, ?, ?, 'Accum Récup', ?) + `, [requestId, recupType[0].Id, currentYear, recupJours]); + + console.log(` ✓ Récup: +${recupJours}j accumulés`); + } + continue; + } + + // CP et RTT: DÉDUCTION + const name = rep.TypeConge === 'CP' ? 'Congé payé' : 'RTT'; + const [typeRow] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', [name]); + + if (typeRow.length > 0) { + const result = await deductLeaveBalanceWithTracking( + conn, + userId, + typeRow[0].Id, + rep.NombreJours, + requestId + ); + + console.log(` ✓ ${name}: ${rep.NombreJours}j déduits`); + if (result.details && result.details.length > 0) { + result.details.forEach(d => { + console.log(` - ${d.type} (${d.annee}): ${d.joursUtilises}j`); + }); + } } } + // 7. Récupérer les infos pour l'email + const [hierarchie] = await conn.query( + `SELECT h.SuperieurId, m.email as managerEmail, m.prenom as managerPrenom, m.nom as managerNom + FROM HierarchieValidationAD h + LEFT JOIN CollaborateurAD m ON h.SuperieurId = m.id + WHERE h.CollaborateurId = ?`, + [userId] + ); + + const managerEmail = hierarchie[0]?.managerEmail; + const managerName = hierarchie[0] ? `${hierarchie[0].managerPrenom} ${hierarchie[0].managerNom}` : 'Manager'; + + await conn.commit(); + console.log('\n✅ Modification terminée avec succès\n'); + + // 8. Envoyer les emails (après commit) + if (managerEmail && accessToken) { + try { + const newStartDate = new Date(startDate).toLocaleDateString('fr-FR'); + const newEndDate = new Date(endDate).toLocaleDateString('fr-FR'); + const typesConges = repartition.map(r => `${r.TypeConge}: ${r.NombreJours}j`).join(' | '); + + const emailBody = { + message: { + subject: `🔄 Modification de demande de congé - ${userName}`, + body: { + contentType: "HTML", + content: ` +
+
+

🔄 Modification de demande

+
+ +
+

Bonjour ${managerName},

+ +

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

+ +
+

✨ Nouvelles informations

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

+ Cette demande est toujours en attente de validation. +

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

🗑️ Suppression de demande

+
+ +
+

Bonjour ${managerName},

+ +

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

- if (graphToken) { - const [collabInfo] = await conn.query( - 'SELECT email, prenom, nom FROM CollaborateurAD WHERE id = ?', - [userId] - ); +
+

📋 Demande supprimée

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

✅ Demande annulée

-
- -
-

Bonjour ${collabName},

- -

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

- -
-

📋 Demande annulée

- - - - - - - - - - - - - ${repartitionText} -
Période :${datesPeriode}
Durée totale :${request.NombreJours} jour(s)
- Répartition : -
+

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

+
+ ` + }, + toRecipients: [{ emailAddress: { address: managerEmail } }], + saveToSentItems: false + } + }; - ${restorationStats.count > 0 ? ` -
-

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

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

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

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

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

-
- -
-

Bonjour ${managerName},

- -

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

- -
-

📋 Demande annulée

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

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

-
- ` : ''} - -
-

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

-
-
-
- `; - - await sendMailGraph(graphToken, 'gtanoreply@ensup.eu', managerEmail, subjectManager, bodyManager); - emailsSent.manager = true; - console.log('✅ Email envoyé au manager'); - } catch (emailError) { - console.error('❌ Erreur email manager:', emailError.message); - } + console.log('📧 Email de notification envoyé au manager'); + } catch (emailError) { + console.error('❌ Erreur email manager (non bloquant):', emailError.message); } } + conn.release(); + res.json({ success: true, - message: 'Demande annulée avec succès', - restoration: restorationStats, - emailsSent: emailsSent + message: 'Demande supprimée avec succès', + counterRestored: ['En attente', 'Valid', 'Validé', 'Valide'].includes(requestStatus) }); } catch (error) { await conn.rollback(); + if (conn) conn.release(); console.error('❌ Erreur deleteRequest:', error); res.status(500).json({ success: false, - message: 'Erreur lors de l\'annulation', + message: 'Erreur lors de la suppression', error: error.message }); - } finally { - if (conn) conn.release(); } }); -app.get('/api/exportCompteurs', async (req, res) => { + +app.get('/exportCompteurs', async (req, res) => { try { const dateRef = req.query.dateRef || new Date().toISOString().split('T')[0]; const conn = await pool.getConnection(); @@ -6789,1006 +5452,11 @@ app.get('/api/exportCompteurs', async (req, res) => { } }); - -function isInPeriodeAnticipation(dateDebut, typeConge) { - const date = new Date(dateDebut); - const year = date.getFullYear(); - const month = date.getMonth() + 1; // 1-12 - - if (typeConge === 'CP') { - // CP : 01/06 année N → 31/05 année N+1 - // Période anticipation : du 01/06 de l'année suivante - return month >= 6; // Si >= juin, c'est pour l'exercice N+1 - } else if (typeConge === 'RTT') { - // RTT : 01/01 année N → 31/12 année N - // Pas d'anticipation possible car année civile - return month >= 1 && month <= 12; - } - - return false; -} - -function getAnneeCompteur(dateDebut, typeConge) { - const date = new Date(dateDebut); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - - if (typeConge === 'CP') { - // Si date entre 01/06 et 31/12 → année N - // Si date entre 01/01 et 31/05 → année N-1 (exercice précédent) - return month >= 6 ? year : year - 1; - } else { - // RTT : toujours année civile - return year; - } -} - -/** - * Vérifie la disponibilité des soldes pour une demande - * Retourne : { available: boolean, details: {}, useN1: boolean } - */ - - -async function checkSoldesDisponiblesMixte(conn, collaborateurId, repartition, dateDebut, isApprenti) { - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const currentYear = today.getFullYear(); - const dateDemandeObj = new Date(dateDebut); - dateDemandeObj.setHours(0, 0, 0, 0); - - const demandeYear = dateDemandeObj.getFullYear(); - const demandeMonth = dateDemandeObj.getMonth() + 1; - - console.log('\n🔍 === CHECK SOLDES MIXTE (AVEC ANTICIPATION) ==='); - console.log('📅 Date AUJOURD\'HUI:', today.toISOString().split('T')[0]); - console.log('📅 Date DEMANDE:', dateDebut); - console.log('📅 Année demande:', demandeYear, '/ Mois:', demandeMonth); - console.log('📅 Année actuelle:', currentYear); - - let totalDisponible = 0; - let totalNecessaire = 0; - const details = {}; - - for (const rep of repartition) { - const typeCode = rep.TypeConge; - const joursNecessaires = parseFloat(rep.NombreJours || 0); - - // Ignorer ABS et Formation - if (typeCode === 'ABS' || typeCode === 'Formation') { - continue; - } - - totalNecessaire += joursNecessaires; - - if (typeCode === 'CP') { - // ⭐ RÉCUPÉRER LES INFOS COLLABORATEUR - const [collabInfo] = await conn.query( - `SELECT DateEntree FROM CollaborateurAD WHERE id = ?`, - [collaborateurId] - ); - const dateEntree = collabInfo[0]?.DateEntree; - - // ⭐ CALCULER L'ACQUISITION JUSQU'À LA DATE DEMANDÉE - const acquisALaDate = calculerAcquisitionCP(dateDemandeObj, dateEntree); - - console.log('💰 Acquisition CP à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j'); - - // ⭐ RÉCUPÉRER LE REPORTÉ N-1 - const previousYear = currentYear - 1; - const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); - const cpTypeId = cpType[0].Id; - - const [compteurN1] = await conn.query(` - SELECT SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, cpTypeId, previousYear]); - - const reporteN1 = compteurN1.length > 0 ? parseFloat(compteurN1[0].SoldeReporte || 0) : 0; - - // ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ (toutes demandes validées ou en attente) - const [totalPose] = await conn.query(` - SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose - FROM DemandeConge dc - JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - WHERE dc.CollaborateurADId = ? - AND tc.Nom = 'Congé payé' - AND dc.Statut IN ('Validée', 'En attente') - AND dc.DateDebut <= ? - `, [collaborateurId, dateDebut]); - - const dejaPose = parseFloat(totalPose[0].totalPose || 0); - - // ⭐ SOLDE RÉEL = Reporté N-1 + Acquisition - Déjà posé - const soldeReel = reporteN1 + acquisALaDate - dejaPose; - - console.log('💰 Soldes CP détaillés:', { - reporteN1: reporteN1.toFixed(2), - acquisALaDate: acquisALaDate.toFixed(2), - dejaPose: dejaPose.toFixed(2), - soldeReel: soldeReel.toFixed(2) - }); - - details.CP = { - reporteN1: reporteN1, - acquisALaDate: acquisALaDate, - dejaPose: dejaPose, - soldeReel: soldeReel, - necessaire: joursNecessaires - }; - - totalDisponible += Math.max(0, soldeReel); - - if (soldeReel < joursNecessaires) { - return { - available: false, - message: `Solde CP insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`, - details, - manque: joursNecessaires - soldeReel - }; - } - - } else if (typeCode === 'RTT') { - if (isApprenti) { - return { - available: false, - message: 'Les apprentis ne peuvent pas poser de RTT', - details - }; - } - - // ⭐ CALCUL RTT (utiliser la fonction existante) - const rttData = await calculerAcquisitionRTT(conn, collaborateurId, dateDemandeObj); - const acquisALaDate = rttData.acquisition; - - console.log('💰 Acquisition RTT à la date', dateDebut, ':', acquisALaDate.toFixed(2), 'j'); - - // ⭐ RÉCUPÉRER CE QUI A DÉJÀ ÉTÉ POSÉ - const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); - const rttTypeId = rttType[0].Id; - - const [totalPose] = await conn.query(` - SELECT COALESCE(SUM(dct.NombreJours), 0) as totalPose - FROM DemandeConge dc - JOIN DemandeCongeType dct ON dc.Id = dct.DemandeCongeId - JOIN TypeConge tc ON dct.TypeCongeId = tc.Id - WHERE dc.CollaborateurADId = ? - AND tc.Nom = 'RTT' - AND dc.Statut IN ('Validée', 'En attente') - AND dc.DateDebut <= ? - `, [collaborateurId, dateDebut]); - - const dejaPose = parseFloat(totalPose[0].totalPose || 0); - - // ⭐ SOLDE RÉEL = Acquisition - Déjà posé - const soldeReel = acquisALaDate - dejaPose; - - console.log('💰 Soldes RTT détaillés:', { - acquisALaDate: acquisALaDate.toFixed(2), - dejaPose: dejaPose.toFixed(2), - soldeReel: soldeReel.toFixed(2) - }); - - details.RTT = { - acquisALaDate: acquisALaDate, - dejaPose: dejaPose, - soldeReel: soldeReel, - necessaire: joursNecessaires - }; - - totalDisponible += Math.max(0, soldeReel); - - if (soldeReel < joursNecessaires) { - return { - available: false, - message: `Solde RTT insuffisant (${Math.max(0, soldeReel).toFixed(2)}j disponibles avec anticipation, ${joursNecessaires}j demandés)`, - details, - manque: joursNecessaires - soldeReel - }; - } - - } else if (typeCode === 'Récup') { - const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); - if (recupType.length === 0) continue; - - const recupTypeId = recupType[0].Id; - - const [compteur] = await conn.query( - `SELECT Solde FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, recupTypeId, currentYear] - ); - - const soldeRecup = compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; - - console.log('💰 Solde Récup:', soldeRecup.toFixed(2), 'j'); - - details.Recup = { - soldeN: soldeRecup, - total: soldeRecup, - necessaire: joursNecessaires - }; - - totalDisponible += Math.min(joursNecessaires, soldeRecup); - - if (soldeRecup < joursNecessaires) { - return { - available: false, - message: `Solde Récupération insuffisant (${soldeRecup.toFixed(2)}j disponibles, ${joursNecessaires}j demandés)`, - details, - manque: joursNecessaires - soldeRecup - }; - } - } - } - - console.log('\n✅ Check final:', { - totalDisponible: totalDisponible.toFixed(2), - totalNecessaire: totalNecessaire.toFixed(2), - available: totalDisponible >= totalNecessaire - }); - - return { - available: totalDisponible >= totalNecessaire, - details, - totalDisponible, - totalNecessaire - }; -} -// ======================================== -// FONCTIONS HELPER -// ======================================== - -async function getSoldesCP(conn, collaborateurId, dateEntree, includeN1Anticipe = false) { - const currentYear = new Date().getFullYear(); - const previousYear = currentYear - 1; - - console.log(`\n📊 getSoldesCP - includeN1Anticipe: ${includeN1Anticipe}`); - - const [cpType] = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Congé payé' LIMIT 1`); - const typeCongeId = cpType[0].Id; - - // N-1 (reporté) - const [compteursN1] = await conn.query( - `SELECT Solde, SoldeReporte FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, typeCongeId, previousYear] - ); - - const soldeN1 = compteursN1.length > 0 ? parseFloat(compteursN1[0].SoldeReporte || 0) : 0; - - // N (actuel) - const [compteursN] = await conn.query( - `SELECT Solde, SoldeReporte, Total FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, typeCongeId, currentYear] - ); - - const soldeN = compteursN.length > 0 - ? parseFloat(compteursN[0].Solde || 0) - parseFloat(compteursN[0].SoldeReporte || 0) - : 0; - const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0; - - // Anticipation N - const finExerciceN = new Date(currentYear + 1, 4, 31); // 31 mai N+1 - const acquisTotaleN = calculerAcquisitionCP(finExerciceN, dateEntree); - const soldeAnticipeN = Math.max(0, acquisTotaleN - totalAcquisN); - - console.log(' N-1:', soldeN1); - console.log(' N:', soldeN); - console.log(' Anticipé N:', soldeAnticipeN); - - // ⭐ Anticipation N+1 (si demandé) - let soldeAnticipeN1 = 0; - if (includeN1Anticipe) { - const debutExerciceN1 = new Date(currentYear + 1, 5, 1); // 01 juin N+1 - const finExerciceN1 = new Date(currentYear + 2, 4, 31); // 31 mai N+2 - - let dateCalculN1 = debutExerciceN1; - if (dateEntree && new Date(dateEntree) > debutExerciceN1) { - dateCalculN1 = new Date(dateEntree); - } - - const acquisTotaleN1 = calculerAcquisitionCP(finExerciceN1, dateCalculN1); - soldeAnticipeN1 = acquisTotaleN1; - - console.log(' Anticipé N+1:', soldeAnticipeN1); - } - - return { soldeN1, soldeN, soldeAnticipeN, soldeAnticipeN1 }; -} - -async function getSoldesRTT(conn, collaborateurId, typeContrat, dateEntree) { - const currentYear = new Date().getFullYear(); - - const rttType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'RTT' LIMIT 1`); - const typeCongeId = rttType[0].Id; - - const compteursN = await conn.query( - `SELECT Solde, Total FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, typeCongeId, currentYear] - ); - - const soldeN = compteursN.length > 0 ? parseFloat(compteursN[0].Solde || 0) : 0; - const totalAcquisN = compteursN.length > 0 ? parseFloat(compteursN[0].Total || 0) : 0; - - // Calcul anticipation N - const finAnneeN = new Date(currentYear, 11, 31); // 31 déc N - const rttDataTotalN = await calculerAcquisitionRTT(conn, collaborateurId, finAnneeN); - const soldeAnticipeN = Math.max(0, rttDataTotalN.acquisition - totalAcquisN); - - return { soldeN, soldeAnticipeN }; -} - -async function getSoldesRecup(conn, collaborateurId) { - const currentYear = new Date().getFullYear(); - - const recupType = await conn.query(`SELECT Id FROM TypeConge WHERE Nom = 'Récupération' LIMIT 1`); - if (recupType.length === 0) return 0; - - const typeCongeId = recupType[0].Id; - - const compteur = await conn.query( - `SELECT Solde FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, typeCongeId, currentYear] - ); - - return compteur.length > 0 ? parseFloat(compteur[0].Solde || 0) : 0; -} - -app.get('/api/getAnticipationDisponible', async (req, res) => { - try { - const { userid, dateDebut } = req.query; - - if (!userid || !dateDebut) { - return res.json({ success: false, message: 'Paramètres manquants' }); - } - - const conn = await pool.getConnection(); - - const [collabInfo] = await conn.query( - `SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?`, - [userid] - ); - - const dateEntree = collabInfo.DateEntree; - const isApprenti = collabInfo.role === 'Apprenti'; - - // Déterminer si c'est une demande N+1 - const dateDemandeObj = new Date(dateDebut); - const currentYear = new Date().getFullYear(); - const demandeYear = dateDemandeObj.getFullYear(); - const demandeMonth = dateDemandeObj.getMonth() + 1; - - const isN1 = (demandeYear === currentYear + 1 && demandeMonth >= 6) || - (demandeYear === currentYear + 2 && demandeMonth <= 5); - - // Calculer les soldes avec anticipation - const soldesCP = await getSoldesCP(conn, userid, dateEntree, isN1); - const soldesRTT = isApprenti ? { soldeN: 0, soldeAnticipeN: 0 } : - await getSoldesRTT(conn, userid, collabInfo.TypeContrat, dateEntree); - const soldesRecup = await getSoldesRecup(conn, userid); - - conn.release(); - - res.json({ - success: true, - isN1Request: isN1, - CP: { - actuel: soldesCP.soldeN1 + soldesCP.soldeN, - anticipeN: soldesCP.soldeAnticipeN, - anticipeN1: isN1 ? soldesCP.soldeAnticipeN1 : 0, - total: soldesCP.soldeN1 + soldesCP.soldeN + soldesCP.soldeAnticipeN + (isN1 ? soldesCP.soldeAnticipeN1 : 0) - }, - RTT: { - actuel: soldesRTT.soldeN, - anticipeN: soldesRTT.soldeAnticipeN, - total: soldesRTT.soldeN + soldesRTT.soldeAnticipeN - }, - Recup: { - actuel: soldesRecup, - total: soldesRecup - } - }); - - } catch (error) { - console.error('Erreur getAnticipationDisponible:', error); - res.status(500).json({ success: false, message: error.message }); - } -}); - - - -async function deductLeaveBalanceWithN1(conn, collaborateurId, typeCongeId, nombreJours, demandeCongeId, dateDebut) { - const currentYear = new Date().getFullYear(); - const previousYear = currentYear - 1; - const nextYear = currentYear + 1; - - let joursRestants = nombreJours; - const deductions = []; - - const dateDemandeObj = new Date(dateDebut); - const demandeYear = dateDemandeObj.getFullYear(); - const demandeMonth = dateDemandeObj.getMonth() + 1; - - // Déterminer le type de congé - const [typeRow] = await conn.query('SELECT Nom FROM TypeConge WHERE Id = ?', [typeCongeId]); - const typeNom = typeRow[0].Nom; - const isCP = typeNom === 'Congé payé'; - - // Vérifier si demande pour N+1 - let useN1 = false; - if (isCP) { - useN1 = (demandeYear === nextYear && demandeMonth >= 6) || - (demandeYear === nextYear + 1 && demandeMonth <= 5); - } else { - useN1 = demandeYear === nextYear; - } - - console.log(`\n💰 Déduction ${typeNom}: ${nombreJours}j (useN1: ${useN1})`); - - if (useN1) { - // ORDRE N+1 : N+1 anticipé → N anticipé → N actuel → N-1 - - // 1. N+1 Anticipé (priorité absolue) - const compteurN1Anticipe = await conn.query( - `SELECT Id, SoldeAnticipe FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, typeCongeId, nextYear] - ); - - if (compteurN1Anticipe.length === 0 && joursRestants > 0) { - // Créer le compteur N+1 si inexistant - await conn.query( - `INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, IsAnticipe) - VALUES (?, ?, ?, 0, 0, 0, 0, 0)`, - [collaborateurId, typeCongeId, nextYear] - ); - } - - // Récupérer à nouveau après création - const compteurN1A = await conn.query( - `SELECT Id, SoldeAnticipe FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?`, - [collaborateurId, typeCongeId, nextYear] - ); - - if (compteurN1A.length > 0) { - const aDeduire = Math.min(joursRestants, joursRestants); // Tous les jours restants - - if (aDeduire > 0) { - await conn.query( - `UPDATE CompteurConges - SET SoldeAnticipe = SoldeAnticipe + ?, IsAnticipe = 1 - WHERE Id = ?`, - [aDeduire, compteurN1A[0].Id] - ); - - await conn.query( - `INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'N+1 Anticipé', ?)`, - [demandeCongeId, typeCongeId, nextYear, aDeduire] - ); - - deductions.push({ - annee: nextYear, - type: 'N+1 Anticipé', - joursUtilises: aDeduire - }); - - joursRestants -= aDeduire; - console.log(`✓ N+1 Anticipé: ${aDeduire}j - reste: ${joursRestants}j`); - } - } - // 2. N anticipé - if (joursRestants > 0) { - const [compteurN_Anticipe] = await conn.query(` - SELECT Id, SoldeAnticipe - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, currentYear]); - - if (compteurN_Anticipe.length > 0) { - const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0); - const aDeduire = Math.min(soldeNA, joursRestants); - - if (aDeduire > 0) { - await conn.query(` - UPDATE CompteurConges - SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?) - WHERE Id = ? - `, [aDeduire, compteurN_Anticipe[0].Id]); - - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'N Anticipé', ?) - `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); - - deductions.push({ - annee: currentYear, - type: 'N Anticipé', - joursUtilises: aDeduire, - soldeAvant: soldeNA - }); - - joursRestants -= aDeduire; - console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`); - } - } - } - - // 3. N actuel - if (joursRestants > 0) { - const [compteurN] = await conn.query(` - SELECT Id, Solde, SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, currentYear]); - - if (compteurN.length > 0) { - const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0); - const aDeduire = Math.min(soldeN, joursRestants); - - if (aDeduire > 0) { - await conn.query(` - UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?) - WHERE Id = ? - `, [aDeduire, compteurN[0].Id]); - - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Année N', ?) - `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); - - deductions.push({ - annee: currentYear, - type: 'Année N', - joursUtilises: aDeduire, - soldeAvant: soldeN - }); - - joursRestants -= aDeduire; - console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`); - } - } - } - - // 4. N-1 reporté - if (joursRestants > 0) { - const [compteurN1] = await conn.query(` - SELECT Id, SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, previousYear]); - - if (compteurN1.length > 0) { - const soldeN1 = parseFloat(compteurN1[0].SoldeReporte || 0); - const aDeduire = Math.min(soldeN1, joursRestants); - - if (aDeduire > 0) { - await conn.query(` - UPDATE CompteurConges - SET SoldeReporte = GREATEST(0, SoldeReporte - ?), - Solde = GREATEST(0, Solde - ?) - WHERE Id = ? - `, [aDeduire, aDeduire, compteurN1[0].Id]); - - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Reporté N-1', ?) - `, [demandeCongeId, typeCongeId, previousYear, aDeduire]); - - deductions.push({ - annee: previousYear, - type: 'Reporté N-1', - joursUtilises: aDeduire, - soldeAvant: soldeN1 - }); - - joursRestants -= aDeduire; - console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`); - } - } - } - - } else { - // ORDRE NORMAL : N-1 → N → N anticipé - - // 1. Reporté N-1 - const [compteurN1] = await conn.query(` - SELECT Id, SoldeReporte, Solde - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, previousYear]); - - if (compteurN1.length > 0 && compteurN1[0].SoldeReporte > 0) { - const soldeN1 = parseFloat(compteurN1[0].SoldeReporte); - const aDeduire = Math.min(soldeN1, joursRestants); - - if (aDeduire > 0) { - await conn.query(` - UPDATE CompteurConges - SET SoldeReporte = GREATEST(0, SoldeReporte - ?), - Solde = GREATEST(0, Solde - ?) - WHERE Id = ? - `, [aDeduire, aDeduire, compteurN1[0].Id]); - - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Reporté N-1', ?) - `, [demandeCongeId, typeCongeId, previousYear, aDeduire]); - - deductions.push({ - annee: previousYear, - type: 'Reporté N-1', - joursUtilises: aDeduire, - soldeAvant: soldeN1 - }); - - joursRestants -= aDeduire; - console.log(` ✓ Reporté N-1: ${aDeduire}j (reste: ${joursRestants}j)`); - } - } - - // 2. Année N - if (joursRestants > 0) { - const [compteurN] = await conn.query(` - SELECT Id, Solde, SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, currentYear]); - - if (compteurN.length > 0) { - const soldeN = parseFloat(compteurN[0].Solde) - parseFloat(compteurN[0].SoldeReporte || 0); - const aDeduire = Math.min(soldeN, joursRestants); - - if (aDeduire > 0) { - await conn.query(` - UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?) - WHERE Id = ? - `, [aDeduire, compteurN[0].Id]); - - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'Année N', ?) - `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); - - deductions.push({ - annee: currentYear, - type: 'Année N', - joursUtilises: aDeduire, - soldeAvant: soldeN - }); - - joursRestants -= aDeduire; - console.log(` ✓ Année N: ${aDeduire}j (reste: ${joursRestants}j)`); - } - } - } - - // 3. N anticipé - if (joursRestants > 0) { - const [compteurN_Anticipe] = await conn.query(` - SELECT Id, SoldeAnticipe - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, typeCongeId, currentYear]); - - if (compteurN_Anticipe.length > 0) { - const soldeNA = parseFloat(compteurN_Anticipe[0].SoldeAnticipe || 0); - const aDeduire = Math.min(soldeNA, joursRestants); - - if (aDeduire > 0) { - await conn.query(` - UPDATE CompteurConges - SET SoldeAnticipe = GREATEST(0, SoldeAnticipe - ?) - WHERE Id = ? - `, [aDeduire, compteurN_Anticipe[0].Id]); - - await conn.query(` - INSERT INTO DeductionDetails - (DemandeCongeId, TypeCongeId, Annee, TypeDeduction, JoursUtilises) - VALUES (?, ?, ?, 'N Anticipé', ?) - `, [demandeCongeId, typeCongeId, currentYear, aDeduire]); - - deductions.push({ - annee: currentYear, - type: 'N Anticipé', - joursUtilises: aDeduire, - soldeAvant: soldeNA - }); - - joursRestants -= aDeduire; - console.log(` ✓ N Anticipé: ${aDeduire}j (reste: ${joursRestants}j)`); - } - } - } - } - - return { - success: joursRestants === 0, - joursDeduitsTotal: nombreJours - joursRestants, - joursNonDeduits: joursRestants, - details: deductions, - useN1: useN1 - }; -} - -/** - * Met à jour les soldes anticipés pour un collaborateur - * Appelée après chaque mise à jour de compteur ou soumission de demande - */ -async function updateSoldeAnticipe(conn, collaborateurId) { - const today = new Date(); - today.setHours(0, 0, 0, 0); - const currentYear = today.getFullYear(); - - console.log(`\n🔄 Mise à jour soldes anticipés pour collaborateur ${collaborateurId}`); - - const [collab] = await conn.query( - 'SELECT DateEntree, TypeContrat, role FROM CollaborateurAD WHERE id = ?', - [collaborateurId] - ); - - if (collab.length === 0) { - console.log(' ❌ Collaborateur non trouvé'); - return; - } - - const dateEntree = collab[0].DateEntree; - const typeContrat = collab[0].TypeContrat || '37h'; - const isApprenti = collab[0].role === 'Apprenti'; - - // ===== CP ANTICIPÉ ===== - const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); - - if (cpType.length > 0) { - const cpAnticipe = calculerAcquisitionCPAnticipee(today, dateEntree); - - // Vérifier si le compteur existe - const [compteurCP] = await conn.query(` - SELECT Id FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, cpType[0].Id, currentYear]); - - if (compteurCP.length > 0) { - await conn.query(` - UPDATE CompteurConges - SET SoldeAnticipe = ?, - DerniereMiseAJour = NOW() - WHERE Id = ? - `, [cpAnticipe, compteurCP[0].Id]); - } else { - // Créer le compteur s'il n'existe pas - const acquisCP = calculerAcquisitionCP(today, dateEntree); - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, ?, NOW()) - `, [collaborateurId, cpType[0].Id, currentYear, acquisCP, acquisCP, cpAnticipe]); - } - - console.log(` ✓ CP Anticipé: ${cpAnticipe.toFixed(2)}j`); - } - - // ===== RTT ANTICIPÉ ===== - if (!isApprenti) { - const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); - - if (rttType.length > 0) { - const rttAnticipe = await calculerAcquisitionRTTAnticipee(conn, collaborateurId, today); - - // Vérifier si le compteur existe - const [compteurRTT] = await conn.query(` - SELECT Id FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [collaborateurId, rttType[0].Id, currentYear]); - - if (compteurRTT.length > 0) { - await conn.query(` - UPDATE CompteurConges - SET SoldeAnticipe = ?, - DerniereMiseAJour = NOW() - WHERE Id = ? - `, [rttAnticipe, compteurRTT[0].Id]); - } else { - // Créer le compteur s'il n'existe pas - const rttData = await calculerAcquisitionRTT(conn, collaborateurId, today); - await conn.query(` - INSERT INTO CompteurConges - (CollaborateurADId, TypeCongeId, Annee, Total, Solde, SoldeReporte, SoldeAnticipe, DerniereMiseAJour) - VALUES (?, ?, ?, ?, ?, 0, ?, NOW()) - `, [collaborateurId, rttType[0].Id, currentYear, rttData.acquisition, rttData.acquisition, rttAnticipe]); - } - - console.log(` ✓ RTT Anticipé: ${rttAnticipe.toFixed(2)}j`); - } - } - - console.log(` ✅ Soldes anticipés mis à jour\n`); -} - -/** - * GET /getSoldesAnticipes - * Retourne les soldes actuels ET anticipés pour un collaborateur - */ -app.get('/api/getSoldesAnticipes', async (req, res) => { - try { - const userIdParam = req.query.user_id; - const dateRefParam = req.query.date_reference; - - if (!userIdParam) { - return res.json({ success: false, message: 'ID utilisateur manquant' }); - } - - const conn = await pool.getConnection(); - - // Déterminer l'ID - const isUUID = userIdParam.length > 10 && userIdParam.includes('-'); - const userQuery = ` - SELECT ca.id, ca.prenom, ca.nom, ca.DateEntree, ca.TypeContrat, ca.role - FROM CollaborateurAD ca - WHERE ${isUUID ? 'ca.entraUserId' : 'ca.id'} = ? - AND (ca.Actif = 1 OR ca.Actif IS NULL) - `; - - const [userInfo] = await conn.query(userQuery, [userIdParam]); - - if (userInfo.length === 0) { - conn.release(); - return res.json({ success: false, message: 'Utilisateur non trouvé' }); - } - - const user = userInfo[0]; - const userId = user.id; - const dateEntree = user.DateEntree; - const typeContrat = user.TypeContrat || '37h'; - const isApprenti = user.role === 'Apprenti'; - - const dateReference = dateRefParam ? new Date(dateRefParam) : new Date(); - dateReference.setHours(0, 0, 0, 0); - const currentYear = dateReference.getFullYear(); - - // ===== CP ===== - const [cpType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Congé payé']); - - let cpData = { - acquis: 0, - solde: 0, - anticipe: 0, - totalDisponible: 0 - }; - - if (cpType.length > 0) { - // Acquisition actuelle - const acquisCP = calculerAcquisitionCP(dateReference, dateEntree); - - // Anticipé - const anticipeCP = calculerAcquisitionCPAnticipee(dateReference, dateEntree); - - // Solde en base (avec consommations déduites) - const [compteurCP] = await conn.query(` - SELECT Solde, SoldeReporte, SoldeAnticipe - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, cpType[0].Id, currentYear]); - - // Reporté N-1 - const [compteurCPN1] = await conn.query(` - SELECT Solde as SoldeReporte - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, cpType[0].Id, currentYear - 1]); - - const soldeN1 = compteurCPN1.length > 0 ? parseFloat(compteurCPN1[0].SoldeReporte || 0) : 0; - const soldeN = compteurCP.length > 0 ? parseFloat(compteurCP[0].Solde || 0) : acquisCP; - - cpData = { - acquis: parseFloat(acquisCP.toFixed(2)), - soldeN1: parseFloat(soldeN1.toFixed(2)), - soldeN: parseFloat((soldeN - soldeN1).toFixed(2)), - soldeTotal: parseFloat(soldeN.toFixed(2)), - anticipe: parseFloat(anticipeCP.toFixed(2)), - totalDisponible: parseFloat((soldeN + anticipeCP).toFixed(2)) - }; - } - - // ===== RTT ===== - let rttData = { - acquis: 0, - solde: 0, - anticipe: 0, - totalDisponible: 0, - isApprenti: isApprenti - }; - - if (!isApprenti) { - const [rttType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['RTT']); - - if (rttType.length > 0) { - // Acquisition actuelle - const rttCalc = await calculerAcquisitionRTT(conn, userId, dateReference); - const acquisRTT = rttCalc.acquisition; - - // Anticipé - const anticipeRTT = await calculerAcquisitionRTTAnticipee(conn, userId, dateReference); - - // Solde en base - const [compteurRTT] = await conn.query(` - SELECT Solde, SoldeAnticipe - FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, rttType[0].Id, currentYear]); - - const soldeRTT = compteurRTT.length > 0 ? parseFloat(compteurRTT[0].Solde || 0) : acquisRTT; - - rttData = { - acquis: parseFloat(acquisRTT.toFixed(2)), - solde: parseFloat(soldeRTT.toFixed(2)), - anticipe: parseFloat(anticipeRTT.toFixed(2)), - totalDisponible: parseFloat((soldeRTT + anticipeRTT).toFixed(2)), - config: rttCalc.config, - typeContrat: typeContrat - }; - } - } - - // ===== RÉCUP ===== - const [recupType] = await conn.query('SELECT Id FROM TypeConge WHERE Nom = ? LIMIT 1', ['Récupération']); - let recupData = { solde: 0 }; - - if (recupType.length > 0) { - const [compteurRecup] = await conn.query(` - SELECT Solde FROM CompteurConges - WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ? - `, [userId, recupType[0].Id, currentYear]); - - recupData.solde = compteurRecup.length > 0 ? parseFloat(compteurRecup[0].Solde || 0) : 0; - } - - conn.release(); - - res.json({ - success: true, - user: { - id: user.id, - nom: `${user.prenom} ${user.nom}`, - typeContrat: typeContrat, - dateEntree: dateEntree ? formatDateWithoutUTC(dateEntree) : null - }, - dateReference: dateReference.toISOString().split('T')[0], - cp: cpData, - rtt: rttData, - recup: recupData, - totalGeneral: { - disponibleActuel: parseFloat((cpData.soldeTotal + rttData.solde + recupData.solde).toFixed(2)), - disponibleAvecAnticipe: parseFloat((cpData.totalDisponible + rttData.totalDisponible + recupData.solde).toFixed(2)) - } - }); - - } catch (error) { - console.error('Erreur getSoldesAnticipes:', error); - res.status(500).json({ success: false, message: 'Erreur serveur', error: error.message }); - } -}); /** * GET /getCongesAnticipes * Calcule les congés anticipés disponibles pour un collaborateur */ -app.get('/api/getCongesAnticipes', async (req, res) => { +app.get('/getCongesAnticipes', async (req, res) => { try { const userIdParam = req.query.user_id; @@ -7971,84 +5639,7 @@ app.get('/api/getCongesAnticipes', async (req, res) => { } }); -/** - * Calcule l'acquisition CP ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'exercice) - */ -function calculerAcquisitionCPAnticipee(dateReference = new Date(), dateEntree = null) { - const d = new Date(dateReference); - d.setHours(0, 0, 0, 0); - - const annee = d.getFullYear(); - const mois = d.getMonth() + 1; - - // 1️⃣ Déterminer la fin de l'exercice CP (31/05) - let finExercice; - if (mois >= 6) { - finExercice = new Date(annee + 1, 4, 31); // 31/05/N+1 - } else { - finExercice = new Date(annee, 4, 31); // 31/05/N - } - finExercice.setHours(0, 0, 0, 0); - - // 2️⃣ Calculer l'acquisition actuelle - const acquisActuelle = calculerAcquisitionCP(d, dateEntree); - - // 3️⃣ Calculer l'acquisition totale à fin d'exercice - const acquisTotaleFinExercice = calculerAcquisitionCP(finExercice, dateEntree); - - // 4️⃣ Anticipée = Totale - Actuelle (plafonnée à 25) - const acquisAnticipee = Math.min(25, acquisTotaleFinExercice) - acquisActuelle; - - return Math.max(0, Math.round(acquisAnticipee * 100) / 100); -} - -/** - * Calcule l'acquisition RTT ANTICIPÉE (ce qui reste à acquérir jusqu'à fin d'année) - */ -async function calculerAcquisitionRTTAnticipee(conn, collaborateurId, dateReference = new Date()) { - const d = new Date(dateReference); - d.setHours(0, 0, 0, 0); - const annee = d.getFullYear(); - - // 1️⃣ Récupérer les infos du collaborateur - const [collabInfo] = await conn.query( - `SELECT TypeContrat, DateEntree, role FROM CollaborateurAD WHERE id = ?`, - [collaborateurId] - ); - - if (collabInfo.length === 0) { - return 0; - } - - const typeContrat = collabInfo[0].TypeContrat || '37h'; - const isApprenti = collabInfo[0].role === 'Apprenti'; - - // 2️⃣ Apprentis = pas de RTT - if (isApprenti) { - return 0; - } - - // 3️⃣ Récupérer la configuration RTT - const config = await getConfigurationRTT(conn, annee, typeContrat); - - // 4️⃣ Calculer l'acquisition actuelle - const rttActuel = await calculerAcquisitionRTT(conn, collaborateurId, d); - const acquisActuelle = rttActuel.acquisition; - - // 5️⃣ Calculer l'acquisition totale à fin d'année (31/12) - const finAnnee = new Date(annee, 11, 31); - finAnnee.setHours(0, 0, 0, 0); - - const rttFinAnnee = await calculerAcquisitionRTT(conn, collaborateurId, finAnnee); - const acquisTotaleFinAnnee = rttFinAnnee.acquisition; - - // 6️⃣ Anticipée = Totale - Actuelle (plafonnée au max annuel) - const acquisAnticipee = Math.min(config.joursAnnuels, acquisTotaleFinAnnee) - acquisActuelle; - - return Math.max(0, Math.round(acquisAnticipee * 100) / 100); -} - -app.get('/api/getStatistiquesCompteurs', async (req, res) => { +app.get('/getStatistiquesCompteurs', async (req, res) => { try { const conn = await pool.getConnection(); const currentYear = new Date().getFullYear(); @@ -8173,7 +5764,7 @@ async function hasCompteRenduAccess(userId) { // Récupérer les jours du mois // GET - Récupérer les données du compte-rendu -app.get('/api/getCompteRenduActivites', async (req, res) => { +app.get('/getCompteRenduActivites', async (req, res) => { const { user_id, annee, mois } = req.query; try { @@ -8234,7 +5825,7 @@ app.get('/api/getCompteRenduActivites', async (req, res) => { // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE // POST - Sauvegarder un jour avec AUTO-VERROUILLAGE -app.post('/api/saveCompteRenduJour', async (req, res) => { +app.post('/saveCompteRenduJour', async (req, res) => { const { user_id, date, jour_travaille, repos_quotidien, repos_hebdo, commentaire, rh_override } = req.body; try { @@ -8333,7 +5924,7 @@ app.post('/api/saveCompteRenduJour', async (req, res) => { }); // POST - Saisie en masse avec AUTO-VERROUILLAGE -app.post('/api/saveCompteRenduMasse', async (req, res) => { +app.post('/saveCompteRenduMasse', async (req, res) => { const { user_id, annee, mois, jours, rh_override } = req.body; try { @@ -8428,7 +6019,7 @@ app.post('/api/saveCompteRenduMasse', async (req, res) => { } }); -app.post('/api/deverrouillerJour', async (req, res) => { +app.post('/deverrouillerJour', async (req, res) => { const { user_id, date, rh_user_id } = req.body; try { @@ -8462,7 +6053,7 @@ app.post('/api/deverrouillerJour', async (req, res) => { // POST - Verrouiller (RH uniquement) -app.post('/api/verrouillerCompteRendu', async (req, res) => { +app.post('/verrouillerCompteRendu', async (req, res) => { const { user_id, annee, mois, rh_user_id } = req.body; try { @@ -8497,7 +6088,7 @@ app.post('/api/verrouillerCompteRendu', async (req, res) => { }); // POST - Déverrouiller (RH uniquement) -app.post('/api/deverrouillerCompteRendu', async (req, res) => { +app.post('/deverrouillerCompteRendu', async (req, res) => { const { user_id, annee, mois, rh_user_id } = req.body; try { @@ -8533,7 +6124,7 @@ app.post('/api/deverrouillerCompteRendu', async (req, res) => { }); // GET - Stats annuelles -app.get('/api/getStatsAnnuelles', async (req, res) => { +app.get('/getStatsAnnuelles', async (req, res) => { const { user_id, annee } = req.query; try { @@ -8566,7 +6157,7 @@ app.get('/api/getStatsAnnuelles', async (req, res) => { }); // GET - Export PDF (RH uniquement) -app.get('/api/exportCompteRenduPDF', async (req, res) => { +app.get('/exportCompteRenduPDF', async (req, res) => { const { user_id, annee, mois } = req.query; try { @@ -8611,18 +6202,44 @@ app.get('/api/exportCompteRenduPDF', async (req, res) => { - - - - // ======================================== // DÉMARRAGE DU SERVEUR // ======================================== -app.listen(PORT, '0.0.0.0', () => { - console.log('✅ ✅ ✅ SERVEUR PRINCIPAL DÉMARRÉ ✅ ✅ ✅'); - console.log(`📡 Port: ${PORT}`); - console.log(`🗄️ Base: ${dbConfig.database}@${dbConfig.host}`); - console.log(`⏰ Cron jobs: activés`); - console.log(`🌐 CORS origins: ${JSON.stringify(dbConfig)}`); +app.listen(PORT, () => { + console.log(`✅ Serveur démarré sur http://localhost:${PORT}`); + console.log('📋 Routes disponibles:'); + console.log(' POST /login'); + console.log(' POST /check-user-groups'); + console.log(' GET /getDetailedLeaveCounters'); + console.log(' POST /updateCounters'); + console.log(' POST /updateAllCounters'); + console.log(' POST /reinitializeAllCounters'); + console.log(' GET /testProrata'); + console.log(' POST /fixAllCounters'); + console.log(' POST /submitLeaveRequest'); + console.log(' POST /validateRequest'); + console.log(' GET /getRequests'); + console.log(' GET /getNotifications'); + console.log(' POST /markNotificationRead'); + console.log(' GET /getPendingRequests'); + console.log(' GET /getTeamMembers'); + console.log(' GET /getTeamLeaves'); + console.log(' GET /getAllTeamRequests'); + console.log(' GET /getEmployeRequest'); + console.log(' POST /initial-sync'); + console.log(''); + console.log(' 🆕 ROUTES ADMIN RTT:'); + console.log(' GET /getAllCollaborateurs'); + console.log(' POST /updateTypeContrat'); + console.log(' GET /getConfigurationRTT'); + console.log(' POST /updateConfigurationRTT'); + console.log(' GET /exportCompteurs'); + console.log(' GET /getStatistiquesCompteurs'); + console.log(''); + console.log('⏰ Tâches CRON actives:'); + console.log(' 📅 Mise à jour mensuelle: 1er de chaque mois à 00h01'); + console.log(' 📸 Arrêtés mensuels: Dernier jour de chaque mois à 23h55'); // ⭐ NOUVEAU + console.log(' 🎆 Fin d\'année RTT: 31 décembre à 23h59'); + console.log(' 📅 Fin d\'exercice CP: 31 mai à 23h59'); }); \ No newline at end of file diff --git a/project/public/Backend/uploads/medical/medical-1763460916289-157625577.pdf b/project/public/Backend/uploads/medical/medical-1763460916289-157625577.pdf deleted file mode 100644 index 8e0a24ca87d0da25fd31ae2f34d230f873e1b775..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34701 zcmeFXbyQVr*EhT=5fD)jkuK@(2BkwnLb};>v*~UD0qF)&Lb|&_y1UtQcQ@a{bIyIA z^W5JU_xFwQ{`Wo(gSEV_xn|FK&EG{PCoD?GM9+#$cD{NGMP?#mB(l;oMdsy2W>9pt z1`{y|=|XhPt&ETvK7noROsp)47#>$M$mtq^?TDCxWhJCJ=Kl5v7>odtj@OEm#*~WlKaUq-*J*3%roCwKA~R2mkHO?TF>f~k=IEzP5<{w2+C&Hh#)GJ})} zKwF*TQFu*2HYTa1t^7 z;k*D>45C(#I{(9MAD^Yc1}3_KR!&6fjP#5|9Q0hwjEsyNL`dmE5i|#b4gNcQMOzb#|GrAd*2?<7uadX= zts8)+_m-Ac0MF{0e>3W%`~1@&e~2Pt4z>V%frtUHq`d_oDPV%=o|Dq|zLd5i^ zV;k8q6EXi`>VJFIZ(IMhSdNv5`A^%+u@N!<%ie$Hm){QX*V+$YOCyLe-~=pxCB8r1 z41w_^%+bsiU8|X!Dvifwv3d z+rNt11p+2VcKREg;x&Mx~zmzY^Ma2BuEB@w^zhmmJ1%FzHosEeDh}Qom zaZK`j5gnIJJ9JRHB#m@<&?{bhREMf6Bs^pA%7)7fz7 z?R+PpF~e+X(%HEzWJ>$`AkEXk{c`j`pz%u8`R;y@eSn|EGb4g^;DG-==;0p9Fl9)1 zSJ6K=b4I~%vm@$x#367O{^0I@vls4pd)+PDbTd0jsc*?QxJG&yTb(c7DS40@Y zRLMR2lAo#H%Hyoa^QzbC;WTj4vFUtJJHEeYduI}QH*$Y}5+^Vhb$g@jb`cR}7^5Yi zk#=4Xw-DCjw!uD&=Ozj669FbR$X2XMxE|iv=?zQ_=d#PHu`1YK!}&(2|E$1e zHazlJH`Ic#Vzf)nMBsS5sXSuF40=%zt9TaC(SVP5cTq|v%D2ge=;4EEoc~N)-h{n* zkx)z@))jg@*GYSBp3vD$wyK#k7rOQ4{H=ln%U;_P&dMMCeQ?guH?Bo4=ToMVh@%6} zJSxm(O8SyuqcIKVaq*lPko(RD`9b#coQm($=N1Q}soNO!RiqWZMp6ss!9lfb`vtE(G* z?V?iOd$@9?OBlLv7{|9}268LiP1Nn3)zP7ShbuZ-iffp}XT*j+Ut#7^U{SziX4e*x zaGox0scG3U;uLE?IB`Zhp;r==hDQ4}iZ2MuWLy&t?~-Y@+rU-6xF_g&eQ{r(29LT^ zG4?YTJavgW_B<@SRMoUQn}tFohdtMVAQQq+lgSjB28*)T{7oK}adX@JFa&j6BRm4< zq_W-$ZmT^ZtC_0zZ%ScO5-Q~rP9j$O8v)}21-nBWjae?ZM*hG$FPH&wd1>a7@6(3%FqcKfOck8@BcI||H^7+M5^${ovSJ!gRhF1Oo zQFy)@v)Pk1Q(e0O)j(g;D^=~Yczl=(K==Y0wj$Zf(0yWx2S zQa{Z4c(I7uEZ0GI(RHaa6>)CI#<4V7*AutB_KgDS`=tQy?)^pcHZLfgwD>L#4OH2~ zBl;9!=oK{Se4oeA1?r?JIe3U3%S`tE5LvjGRhgYV@bjJ@*CurzN(*YF;kwaF#}&|5 z=Wi`is9wXx2q@(ZHs&zQOdgTrub+9xaoSr&tRlqq(GDV(V(X0LAil`wd}FoG^)1tT zjaL{3M#VoCPCFUK*IV-49_7ot_dROfGuK{c!K$JIKDF&2(Aq~c1!MZ$pAt-_hj=Oo zr*lgv-jgKXTEoCQ^-jo(<|!()Xp$JVGV#njSC~op1=bO7ey68HuwK?VD=m@t*-5kmUhIY!?L@ot=q zF%DzB(qL~X_211cFzEzEQe>lQPqR52*)RR-6V3f7irVna*by!dqoCN15be`}Fq)Q= z8`-M)OxsV!A#S;j(kQLn`sWd=aOQVvQm+LAH77{Dr-RPms$#lfE6T7Jd($0x;pyHZ z)FWTORjp_eeZjVf;Bja+sr|`BZQ4q`hae?<7An%ClR?mZN91SAEEt+dAXM`7M4Q5K z2z;)Zk(oS)L>U2bUVUY|*mHgw?@@dM_bQTtvkC=eW_Lt`DTi2}4(S4q38u9a(&GcS z;c#+fV{E0T=HD;h4W}gfcjBMmkrK;ml zRisc?X>1|7=m4#Hw{I4nGPg!r#lu-s2L=9waMkpR0LM1lruj;E+a_K-ZAaTctwJ5I za;zHY$0)cO?WK0~lsU}Q*Foa=6pm=B6prDR6pra_sPHoTVQ*yipVQ0iqX(k|!qzeT zLf+lqTtB5RY52x?Sogz_=n_RQo!rO?ZVNXIuARpgF-)8C0I$g4g50m`h{Fy(tVuiz zA*|EnH9l7-j}={*V}M;)_6dp5hLe#rg)@Z;g|iC-xkIF5heM?HSR?;p!M(!4(vQoo z9d~|2mzzGf$_?bhmf~Ey0-f-pw1r+uONy%bQX@I}kOib@GXdMVA9m1u?&9;~nWf?_ zNTWtN|LuxxB3{s)ZSmdv-DlR^KkBFXP90|POzs?xUf&Ure_65)?80WZ9eN?9{MrBK zj|S1D#(%u+>)Yo(Q2VGFaCvNhT&vO0PklgCGhua4{@b2qvy? zZA()yZZp)e!o|L7G}kPS5yvi29QGjKf06YvB*iahpo|k$J$zoB@~vn@LC5DE+5mPc z8Fv$7^Y@f@A~LcerO92dt0JJ>OLWg6YLHwFcpR0O1(`~3Z!Eer#gu2hgtraxcP$rE zh`b9gm4v>c7cywr&Doz9H_91?b9b~$%kgS$eqML)$|B10`Gwrtn@6V`;_DO=Oy9*j z=^=$KiiUE`WZFZ-YK4RLeau;dMBwREQkU z1NX7(R8W3B5sB1Kw2u1Lve2(SPUB1@>V5ix4j8tn@kq{FqY|2mjUzen`t5zaCrm|+ zPyE8UG{sK^r6&~xiyr6=mG3TPsP(^ZO&f_zC}-hx(-~F}kDuLgyltmZ4x8InwHGf} zKHfvRk{pH0bLA#fxQ#gvlyBCw4tF7mMi+EmD&(Yfxft;qt>Z2t37BO2l+)Hrm`8~& zqDYwr@1hNUoqeJ7RrS;(bwVQsH`}r3kh88x?Q2w>OHpow-Ayh%f~mGWrIEv#7G|DY zyIKq##kmrkdc6lhv}Sv0(quzcrPF+f)xgdM)x1)cdObmpbI4@F;YNMoLZ0xOo`AgQ zOG<`R5qpaBlh4QbIXoq<5`3}jMZqo~qUyix*Ar0x1ePc7*L$Q6rjDOB@8reb=WnJ~ zoQnBWOKuHPA1QHpC`Jw9*!9T>v0Z;mZ_J$tmpoU6hB@2(^keu`rFQkh)?g2M zTeJgK<|(mx^j-$qqEBIVtR-QR`L5C3FOYXSb;U35hn=#L`*l^%x`AtT-kpc*WNkIy zj7#brIOP*+;nkb?r4xOZ^V>q-)03w!rJ6T<9HQXee_r3*N8_ixPW`+d)YFhkuTs$U zmLJh-546=!%mt$Zp^V(G(TuTT{jwU~{2W_v|LFV+E`I~(>%M%WIl^}>_EQt*`AQ8u z-zPN03Mt;Nd%u(!+O}R({p6P+$*Xp|{aMv?t6C~a<ab3anZxvji0yMDMx3yu8-M-E|spkRhX)8(41vUd~fSP zm_!@-)xecQA)SX~4|eh|K8otmJ-vhHRK8KEsp@I_Zr7Jio?VwuG1|ir73&6?vXlg& zDKArPW^PEGl!ZO*e^E^3u2hFz!aIr;aY5Ye#1Y3NyC&lT8({3Y245fi^t9`HBC&|u z*(;<44+q`ACb{qD!uC=g;H%~k>Jv|d;OUO?3v8lONd3g$M_RU{mGfzH^N=H>vN;TY zIVYzzmC5SU)W~J3$G;J@gbWL>m?$qqthRvnT8V46TpiQ#ndBwe`42yxyW9G}20{J` z>}3HdDTqZ#;6>dD&MTHDL;l}iEa({x>pk7C%BIVCRcXfy`dXe|5?j=IGyf5(G>8?9 zywBVG5wjsim$DxAcJCvitrs{<9L~+SA^e)?OO+9(f7--L6H2qZ+yXw~%ett5FCL=W zyzz3p3*v7GiSO6rK|5yUN60Vl7q{hBwLk7P@Ky>GjT9yL4V`lZPI)MRo1{zhTvhvo zZceCctb7A258^t{>$orW5Qr=1wRvlyuX5*K3c1w{27n&w2iy=^k*(0M&<;54mwBG> z)$%Fw)e6v^UduLgvPqsSdMM;HtShx@6m6-qJJ4;8%I}zj9gqs~4ZwGpqFvJ=KcUeRxQMX@Z{eTjg4yG?V1g{X{vu$ofU_-B1nVhFGs3HflD^iLqwVy}c zyI>1Z&~r7eYkf|AqB*`4JxN>{S9?C>q0LbZe;mALFJ+0eUBvRxbgu`~eU}~T+WxfT z7>Ai9vv5?P-j)&THXx;yXw9P$GEP)M?3@cBfxCTY#+^?trTh&+VM!tsTlOY861gw` zV+H!5u#%tf2V%PNl1wr;;ju_X2kAO-QG%qT^;VRq=A7tHyV370rc9HSqkFWJ&az&5 zlJjrEmRnftZ+{WM>aR4w{N??mqQXL8r$U6D5%MnmIB8p8q|TO`%hyuDocbwdiXsFK zVhCH9dGhMCt67&v&&bWbt;*JkEnwuh_DSERWGVc!UiRi6gQGL{==-_G!|I%GW?s=q z3kK@U&3qgjoqZOzH26hE@ir+Zc^n0_JRku~?Iy&FpRL z&E-%|J|9i=`{jd-z@9ETo2nE!a#p36sny?+{6l?2Qtrh_fRQ7OMXBrrvJKyxsTmbd z3a@(UA%AtqxF1phZEoo`dv$QOfnie$1?nBqj2PCs53=9JH0~mCEz+VgzY3j03GTJD z=r}LhiO_gyGvfQijOg#+)H54$6c|VadtXsx)+Wqm!idRN?4`&|H+_AW{2j~H+2-m( zX3r<;PsiJ|DxMTz!g8W<)Q0Joeo|UGw{0=fSH}&}pURJPF8$t}ZWr(g!Z}wcydk?&MB17 z8@}n;vzhJYJ50469?Xc9j!ZVUAW7lsPf|zy5~Xul9rA!1B_ZXZKtE#5gJ(^-{Hayw z%i@4l&CepTr&vKPCIP#lE6BojvmDjqQ!R$;uuN`@^XZE)WRZwPb8;8qX_>k^LL?c; zpilS}-iD)l`O#=&(#LGr^i3GZv*YE{KyGmBd?M4c*ZAj$sGU08Xz^{s$e&)Nu;@Ik z%u3-BQKG{q>}7c2vZL3)iPSF6-FbW!M$d0~QH9|5DND2>eNs~x)9K71UfMy&!scaQ z7(0!n^}wQ~sA`{P3P@-+qsVufdm%8o7Cd1?B}p5nZ<7hoF^@7^T7BpJMeFu z=ICqfIZQ&qna({mPv*Yy#n9n%NAl!-f6-3+qB}%0)_)fl_cL;S>oa6y>8Ht&89@>l zwiv-Wm2LD17jI*tLQM_6m>@{LFWhhUapiR3rto?L){>|Rr@m>k{A^$R(2R*785yfd zKMxZntP#l0a|B0A#Pt3}nIX$5HP4sU{h!q51?mOxj0;lA84fA3W6q6)DphcY`DN3( z?U0DJp>(h3FW*~Gy?1(-{nf9=&pu~kjjR%FzTgvn!c5E2i%@c&)?X2kLuOc<%-#wb zFp~}`{{4~FblFohZPeaMMlo0Q4750<$KM;3s8Sa8K5!=#cS~%OOEN=mR6og^qt%Tfn z_@agezlcmq*cav!WbAz@Ie&~bRMr)YIzYXF#eVEOW*R5>QeS-<(>9V>b#nc)1&$jLFFkRt&r|C;J=>sk87h~0{cG^ZNjTP4r%VhI5F0>8mvCOVtHb}Y-u4-c; z2=s6-XsMmMh;3j`p^6EbIr*=m^q)ORM*fbDYhUt`q2IxW_RM7y9bPZsj+NKfK4P{Y zHzL8X4(+27)z_{vTikfAtduh_Rmk_5AYpj;snpOn{tZ?yU7Os-;4w*p2d$KsHEOpM z?RiX+IrQW*E8b!rL2S*NfAp;v_vh6G$rGuRZGJ$)`AN}is5vPfyqLHb77#qM`GRt| z)F)TCZQmP4puWI?Z(1box-@=Q&oL$)E?%J`DL*bEdn&0t+o2#(9N}k_=C)#vDaVEP z6eGFx3*2woV5NBsu2ojdgGw{W(qK#}6++R-fU&iRx0t34nA5E&_aI#tzNSgi;dChm zOp(y1NiMdVtuMHR6A^!o`OBdzYM#Y2Y+-85X~-xWRp#=DTCir~1)4UFa8XAUiV#wMV7BZz4q>rSoy;~~v8eUZVb2S+eAks16DbIyMGOpFbC@re`5a5$k|nC~ zZUSmgY!RQ%=4Dxv?6?iTu=~T1L7b-bo;Jot9IS{mc~S|hOByd=H6odhD^rTTptP1F zJ(aEP)tqNvAzbq&G!UJ#Ztoin&Y6by+uZaE;wy(&{n~LDt&sZ@m0Y8v zLRXUSye!w+j1_Y5)F~y^l(|ajx1`A=sNcNoR1IQ%|Nd)e;1=0?4yBA-4dj#nb*#D_ zGKSIC$Q-^(ZRVXhjv871F;it-^U0Q{AxN3Bs=^;*aA59&nP00pNX^;My)JMuij5uH zV^8&?_{tth-@|nHzNS_&AQvvMquWS=K;ZQa<=tK_vx#iaklHB|*^9M1^v+n1rB|?l zC*mwSmZ~4gbl+G}#^%wVQ|=H}kA4i8iML?E7J5>aqvk?_vdk)lls}PMs#}t@wpi3t zP_H)GZ~Y?FI9cDQSt-mBlSwf`ZUSY53Td01GowAvGP(rmgUoSijFu`6QUq^vE=opv zy_~a5SP3^1eil{nQ~-{I#b@JiQd3^XopJm6$02Mm^%i*qV3@^wre}?|i*VB9R0k zUWXht`o&C}-F82L(a@qfzOAquM&3*)Z@g|9jgiP!2)r%jZBCV+1IyN0L)m$HXX}zt zyKoGXr@nH8L!D2#y38#}N8_yySziX!#n$6#)7GkX$4>Uc-NS7KbwO@zAqFfYZSfuE zJ|X|^@~V{MXpdCIqiw++8Js5Wp zGiRClrY6xErOx3H$W>Lh?tRS#E^iL@F7mthH=xkl+d$8|JFCg^3)dNe#9RhS4t|;P zVpzWKJ&m`Xo~>EX+xtP)kcSl1%W>-~fN9fxB_92gI7W z=VbqH2+w~a#s33T1c1`tfZ}fm5I`1;3)~Q35R@ijkO3e`UGu-K`;CbH1)KeW&;JV{f6 zusNWvKYsX4gc$(Ebs@GU03^=D$xhG8&dKp;UPdCie?QW3vCuQKaB{J85^*sx(KE4e zay`P83dXtyCcw=P=0A-4Z;^fjvj1fDzkQp9lb(&0nT!2V8Nj#y{m8~j&(6uo#mGv; z!ORS>$Nn3j|JSGg@UXvq`tO%2{*5XBZNc9!M*N9BGBa^9J!1O5o$Wu-;omnCkQtOs z3?6Z0Ms@&576+Rc83Q*9em@9VnOoT^SnKM8fg24HLPCPNcED{5=0{N;FMbFDSh*b7 zR>;c2+R73@+KD*-KOmjE^H;9;JN0(Mw;HmK)GW72dCR6w3NIh3kqgBV zDyv>p+~djUF?CM_5J+KbZ|h;aWVDQZ{_~mmr?Wg?A(r{(`kC0K*-zY~7S7PpKC#kV zMA|x7keA~uq<#sm7Y@wY_jS;+OJH)2O{f5O7dO6tk|ShfM4+6+75s6Ywl4!v;C`OP z{&fdR&Ie4{j@;$iXb#2;jb1V34dIdGCY7kVieODITZzfkfeLR9!1LV z25-zlc{ZRnziJ;AySomMK_Ky%v&^%I@RBH-Z@E++aCNmeMe1x~C!EWdEejYCP+Sn` zYrT6m4k@7~#a4q9OA?REO1aJc7{{}nO8m~Y^#VYaWO;{1zGEAE$y;k^Gy?)bZ&)L& z4s+v?U^k8LIzAwWR6l-$ox{l#d^CHxw7LG3PrTB6NjC3s9bo!aoL$_@-GJM!z)>p5 z=z6P-g`2a^Cd`I$8z^us)Ulq7?i^cAs~wjK z#pDI=lnz!Xa@bcoo`v!#iz#bsN;ytno%?1deR?&1KRcXzut$CKYi2G1Hx_HWDlGLn z2=I}Or&Nx2GjkF5EnQZ`AkgQ7G>2G!{2i?Z){_Z~%!gDDm(inu@7EKB`}c44-)s@m zni#9A%RkI(ruzmRP#`W8=3I<457E}IS6eEbD_6bpG*X@pzfk6dWNb&|Z)cwjgt<^3 z>^9*S)Ea5G)2U1GSCi2AQ@9_tD?~sAL7*3%6BzGl!aa4IC(tfZR21YrxAnv#DfYJZ z83dq>2Z}2_-)l>BuYQ)_+U14p?DIGs2fPATXqWNsjJKo%k{Rm79>r_~r*`FZ=UJGT z?;wrC;cL1h*cSlWcnr*kR2oU$%tsjph6&?g73V z8iM}Z1xP({QDtUzBi;S4jFJM1vMgtjk9(S~vVMEtcRqnH&fngYA_1Y+3KvMT@(g^y z<77J`j|HP?yr-(Ba)2a=AjhJWeFUP;paLRk~ZG43-E z$kL;66NoZ{u%X$}hXRYti10qF`ZR*vXVtqFvTN--4@Jd5+`TIUM2RPcYk*@wdz;$G zbZva6Kf0G8oI6)u!|1fu0;Ci|)tHk|EJ>24eh`RAHz|xmnE!dxT)n6BC&R_Wm7-_Q z3+Qqf3T5k?fgB{fnNF${5#G$8E`_}N0Rhwud*SzfKL(o$^uAZEUU9TS@{y3 z=C!T|L0wi)fV{=U62`TL_?Ta4X`%B8O zc5Z~LSH=o^w1GhHDAj4`h55gx_KZuh2vji%95b#bvXPxdK-qz?l(Q2%VC!WjB@_w2 zzU}aFJy+(9P$)b5TH#GF+MU=S> z7M6-Kx7)GHT~-8uJtp>hLOzBj6lo=1wYgI~PqlQhw>Hi_^tfNnd6jy!-#}4YdlQE> zu~1ALai;cYAE4@m6*fK~?Z&jE_a&|)YV%_5Nqt`f)b%^rK#e9g`tRkguGwmU{19;c zWTmr`&LFEhds2Ed)^pC;ZFdou4s%ekC;1KdO6 zhv|4U zQGrVC&1rqUSWP~9(#Bo48*MAEfo1hy2?ihJTUZrVozDovV>d&Xu@!lIOCBb#Z$aoJa^?9W4a6tAw%!zoVB-&V^O z_J-VM0ohIEAg%j)#_@oUDS6X0ZO^-^TQk|ya?TOxEk4{7ETrBqww#`y0;NlJYF)dD zy$VRQbLlR%H$s^m6hviNT*o&+Op9WU4=&E+Lf6LLV$4F}rF<@r?t_D%e2`j`83y)> z3qMp7pi~y893G8x?O`l9*0V7mJ2~|Co!z3Hr+&Vj0QmSAZlE+cRP87&;m+)hk5T;|V!`T>2lcH|m+`!+AmP}IW=$oD^S zjSQtS3ox-FpX~~;WHH3r^q2O9SVT1yJUy2DzU$_ySAu%_+-#88m1eP zs>)RbF?Bl7+B~ZnTj0THn}<`9?(RI_0@Ypa7bTW{y9)lkbrICVKN)H5#DMzDU;60s z?o~*Cn4)%KP~IU>ElHYM0AuQ3KJz!(E3A)q(;0_huXn#xQA#G|O>lLXA;sfO*@Pj z10+v>YK?m!yJm5Z4S*ylb)_rLf!uA_UqMr*!1+s4p3wt3&M%~8Dg$Z&?XaO?K@HmramD|ySDUg{_$djp-i>nYG^UCazws~c%6=^IT4yjtUQYzbgku*1uT{ zcFGjGziNK#x*BE&ZHXG~;4P^Y)4O&>^x0VPG!}8t9*kDL4N6wr|8bM2pG{g*0SH_G zA{%Xja+BB z>F^rBX-UBNU^}((Cp5*iCmZ!;tymHuY^OO z1uNN>YT{Kp%QOmED0@rlAD-3e2C*LiT8n(DoF>5LLIvVLNy(mE%RC47H!6hM$Y3#A ztX6KlHl~x5dZ_wwBtP6{YP|KlT4Qe}X1Yiw<=m;u;4wM%gHP&MO>AkfeO)Os8J4uaZ$*YlS4N zo<0FtJGn%FJeEE7Xg-=QK<5*AZc7-<~$tU9&HE4fGX=Et2c?)2+X~;B93*_}q3+Igs z`GdwSU-WnTl`Nk&qe!)$NCQyXY&_7-)k|U{)p-Pe*$qb7YeBP}&)tfna0d?7AASi8 z@mSx@Y(@2?#eaY2oUh*Y3MOGJs^%ziVx{^)4yKvVbL^+2>_n9(JK+N*at^Ix3 z<4+&VCm)XjX0g&x@2Km;p504(To@3&9VXU{?<#B`6`@M%ncF3fwe;RmWoF96dg+)V zLE2BF;B8{W+}4H(q>)*O@ecqQL_{DT@GsUT+xXgFy{@sBMe=)C_ zs8z0sW22^$2G%Mk(CDH!syvjJ(Rz0^5$GHgaW=BE>`Z__>lLHYDM;pHER%D$ZXeYb zGe1plsG;-p-o%2*E5Ffvlc0g4r^!dP|2k0E=+&Pve3NTin=p@dfiTa%lNU4q3!HvW zFPj=Od9Lb?KIC%9KLN#0PQ*-z)nQp8Yc{cS=x~K!B3v%V2l3hJkFz^U^P0$dp|6%K zPqKysGmq8QaOX>vdOgbbBiy2FZ@FK_J6J6Q-LtHGIIdbA)aGUTfsX=W)V{SbzFhF7 zAWfsnn9$DewlGfMS6Bpw5Ae_Vg@uv3rVs|%VO*5)Rh;wgD?eMYMP<7qipYbMk;@G<-+#0ECzNL)_+wbT3z zf?wT~XybVsUs+#iP)75oS+VZc=6=CC9e5RpRx zN6Yh|IrjKO+qsak`_OXM=Jsrf%IIB7;F^9}{B#YsJ0D!hhID@A{1VH1tne6iKabEz zFW~H(1A966HS@HUT*K{Xx^)rpV0RmZd1KyXS+Kb_q0=bS6$Z4P7#p@AThuFM%j>yQ zIRdCNC&lc8qC?{y(4`iNlMDdUo@{O>-TCOYMTwX+w2!E~5+xf2C|}cFj`WHm?bedSX3zZ;ed@47uJ17lhYbU436f)K+^#X|XErdJulD41Z;yrv#jx*DG2Y zy>U4(-|_}DkKwq`rgG0W z>h+nZoYfxZG@G;;0kwWoLu1QSi;lhTXY711_hJkDk+9|Jv*+P39{FVkD5frbW zg^P1PQ90sggr_~Fp~4;_d($~>mwPl5L!4?Ael=EGFTwGW8;YZcG7(mYJxCbNR(ZMS z$i)_fud8G;Wa`3@{8Y1#PnSbLKmx~UYCcoceZ~LV$&lw9Hl(e@>tx6kEftTh>5cV| ztxRIG{Kqp5a862?f~+L2sRF*Syeifgc0SN(b-q)ndZ0C4zJh<#xvi z`YUdR?h79ngyBK)q^S&OwF`#HfHvW&T$y+l@UWeE>&VS9?#(4SGNggv`sVJU&h27U zlYrLfWO_P=M7UIy@V=}dty_R2N&05WfQ%8HXDs7Dn*7?mxJm?QNdg)OeTsB@6AOM} zgS?)OYizq&3Hmbv{(m$wM`K;Nvf5ruHU>;3&6jQXn{2L6V-bJ+q-o%Mm;(yc!H3s& z@0vJqik>^=bJm=*5bt>=#YReqi55$TbHuU1bIru+lpW);*O&R)Sjj%PzDBG?JD5`7 zzj1sCYBs-K8M4;vUXfiRD^=au?c$!rb6EH&$KjDP0<}uxZv($J-H>KY%&5;GI@0jE zfx}9CXdhP9ZGLax50RMjjvYafa_o$SNr?O@o&#k+p!DE?i-(hblCYK zFkJXX05;pp>bA*Y5M??1XQVG?c{(UwK?UYdW zc@m6v%eG2(x8?S7Jsi*WDD1MkD+`n7`@QwuiPBHEnOa!(jXc+6*QbwqzejS-oSE6K za1__MUgMj=Wi@L62hlah!SLEz-@@YS%kAXJK$3-E7?4hYn}yUjgISe_80tjbwB?hh`B2hllA~T*8t|e54O@I_S_v`PSF~H z8AI}oGtgt_G`=b>nvkh%us-C=zxQy3Kj8s+VV`xTFAe4oqOh%8=AB)^G3g&)EjKnx zj6UeE969cRS;w=q($wOxoa-W)K51F20Ud_`%F&l7%(B*PpbwQG*o(-gQl3VbPTC1W z;v!ctNj6vZzF+Y6a>dbzi1lc+FWluv1${*~Tc+np*yNL9_Q{wVc#oy#3R{?`4TRu0 zU3&?%0lc%?`}LkN&~YD`WYHykSi@JGtA@y<{rJG(qBX3TT#k@_+TAYi;Bf*_WS(*we*fPLMyqp&MWnd<13^9$?0nf?B|j61`#u z^wO3Gc1q;>GY1w!H9wvXENIM9(|@>qS5CD`^>TY#41H_Rgwa>kZi|Pk1qx{0V=$EDKcTI}Q8EDMP(oPD`g|2S44UXXR}qz)qsNw<%bPB3gS z9;||7kh>c$N45r|97)zTu`<3gDw?+$(ArL9_gbW>xM!;&p#^~=c6N|zy*GZ&!oFgT zXSlcAX116t=(`6@Xn~1UOFov(FLzdm?u{!+k<-rDl-w|eX*%+8{EhU|8E_!;eRY6h zJcpbIw46~xC1=0hZt+F|6Q_4c*Ys3R6QewU!|f7fY&i@lL-H23AA`=zJdCLq+JG_a z`q~Hx;-jbnnjO1Q5NE0%mIb7%(1cz_xFuVpS_z0_K_f0#3GrIUS2R z*Z@3aTrL#JpvZx>I$0Tz2r(FA6~o*vH^EB(HNef$P=~zAiKXUS_^ytGt|5nS^SBJC zRRo8{o()9%tf0l?vDs0=Th$#{N^g#jUn?#!Lg~J!zNwfE`=pJ`wl&40iO`TRnmU4a zp4$;tA}X!M=k_6)WPQbNM!*>uQ7+DF)e_m}fV_H#>~wqVfII@`sz1?G-$<*p>i@Ez z;EC*ya}D5bo*t=IFY$JSXch|NYyjYaGtJrE>5>69Zoq9x`161>*m~2K#)mvRJA;}8 z)VzBGz#%PxiJm+UQj3{G9NLsYxq2c!{)w&tDQ!&+Ea$!)=30C8TV8fu%t5x_wzdoA0R>&~re%c%`Y8x1}OO1Gdi_v;br zx?x*7wzy@?)@nOh=jPO!i^c9@7{8unZFxCd=WrO1pKVqS%b48BSi3R1WJ=N#7sUHD zYw)UW#xItu4Z(S&hnSid%&MYFTDzC4we_5Yn_JHn2f>Q_HL>t|)w)6+%_~5R_{?;} zfU5EQTpHcybIpIG*bG!Th!F@$^5Sqxxc~!$T#|4u|J1(Fi)BNRk3*(7lSSR3Wuvxu z>E!MSMp%M4FG1E<)c|LULMg82QhJ)j1pJ>cZ)H@Ru={=8k18eacckC%1PCN5rKo`D z@EerrpU+%5?xtp?0&|9BbT`%tN;-Fw^!8PhiNWVefQ!s|On-5e)p#0VJvzc3Y*|qnhCtx#45pw^TP-Wf}}yRF`tMkg{Q> zn6Y90RoE`OIwrip0s_5P)Y^T@-=!KIyD)B1G=Q-42-wtOH{NP|1a|oVQ-_5nUgMXc zmk9mYXTV&5&3o{yWSk2)ABrsqfIy=&cDYQKL?=J;>`2h!6YP2?BITmOfZ3TC!3ph@ zmJP8^phm@8{cjvvdg3nxP|LdZ-YF@p-ob$w3(6UB*C|n|i`}=TzXN-E4bXAi=+A+{ zY@~puuBO?cl=Tj{tQ8eFG7)g%EtljLUpk>P~{&hDIS z*so>RC*Ygln`8v=of%+2zb^dy{aXS#fYZ&RKp4E5E`4%PomJhzWR3u_dt2%KUD@uW z8KHVDV2W{0<0}+#feCtt*V3u@z)}x1u}sHa30%5Hc!1_-d2UG1T-SlvMmz`x8jx41 z+=}uK6Bt+|2Q(lr&O)t3Y&Y|$gkZ9rK51ZNMaH=GQ+o&r804U;Rj-B&zRo%y0}fG$ zO~@PqX^J*4fXa;NkgO^kkyII-vN_v3;Ju}(gl-3z3@!CiSX4bF{tw2@SOz{4ma6!O} zeJgm|yVTR=3bbYNDVpx-qVd@ZwWcXtnUys(-V&X|+for?{6dz<^_{JIb-hL;UI6~O zh_&qA%Z%k9F92fntnhQI#vg~hzmDAx&+kd#dA+*1zuF!Ka9T&hAWCrc;b-*_K|RLv z)YOR5MXRtc8cpoK_|bMGcSsL7Kwd#Ovo3<6jKI*0B&cj%-UxPODS>5(=fzcpRE-a# ze{m;()_mqYPB=Wll113{Ml0&OUy(EIvHNi9$#?uze_foO9$HwD*BDU#lbNb%P*OgtaXds>Bu&-SB;Fn~P zNsvCDhHM;+&0q>UGm>!LHHYN-H}!}z1BdsTP$y#HmN3GDqR;gJ0^NaB=c=S-A@$ZK z5!izz<45_7iQFGty%7fB@;Jp;X;97;nnVDkfN#Q6?(gGQ1IoRV36Vxh*y-nBxW_#e zWM@c_b|4J~^B#`@_Zn(QtWV-z^<4s&)*HR$FnQx%C5P0b!GJoh%9_^(N#=69GW1{5 z4hY1(Q=ThZoH&6WoNu(Xad9<*()yhOC~!+rSN6nO0~4M*2Q28<_0)Fq;z|P8f7k%^ zQ7SoHve2u6!qP5M*>u303$-yIaWg<^;ph1XPDPDdNqaerD*^J#x$V#LS@cZ+CMscX z#iPPh4}rD`U=wOd7sPw~3Yo1wtpdx_>RI@$$YRb=A0`bvNPMaacB2nS^-Tc&B<{ny z6_4;>Tg~iUc-gw%`uMrJi`1u6hZhWkF3vi}6Rj{fkYkVOrYTPcQ2OmIQOpgxe z#&}Xe*B$|HGQxzij}*npxDEi)fC-dyz=lv=hyGAgxV!|zk(c%S?VSB5ot}6Q1<{XBP>Qi%)U#U+Cfn$2E-N`K%fVfWo}n04J5DZ z;{j0uyPgEdTi*PBzDacF!){~KgBp_UoR+@F;X#+{8-UMFb%xRF* zi|4?4KDSz`{l@d~M-8a)xL^>BS{7Bm zaK1%y1?I6BW*B|n(ijYDoVY>FimkCv-I%M$kK+|~0McVs5DMO`c{w#gU~w-u?sxuP z-bc{N*Bxaq3<0$MW9hyHfHR&Dc4#z_UxY4gw#lUTx{cUS2aPyMA)Ath3+P_#=6!aP zXGEEZ?OO>V1{Ht3)j0@~Ym3((k9(}*^d}NxHty=Ja5J4{8#`u2K)%e2g)jJWHJOAA zoaItUWi*V@>_hpRN{EgZo`9Up3=E(-9l-u3s1}fnsUUzz|4)109o6)*wT|(f;o=7feS>FdozE%o8B7_@dOw z<+1cMh}Yd4#j@-7U2OcW6FClk1tHdF<5r{GCWGq?Ye23AU-AVf70}-7*Y+1DIs>#$ z8z^~zvz>C!dOqou-NPnV&-1q3!bDHvGp&~^Z%u#PM4vaw*)x||*z>upS?(~xk9U(D zx{}7-w!8aYj&?0&>^f~ZLbEoY}}PbErYZ?YPF}*$jlXt77>1lGmg0r zY{$WLW)s9-`5YV++^1c86n|D&?+E05{}5El*9<;e3pB_m3Vnge(R%U{##)W?Ui@ehYx#=7U78C*V(P$d~z*JB5vv#;PXS~a>;|Ze39AYwUe~2f-+Q86K+h{tJ zc^N3-(dP4%b1rh=Zq-?T57D)6ljW$JYo#TA9yUcl)Zj$n&-a(6OL|0OKZ3jkf0yKS z&$ZX(V3OUJ$*R&g`xH;P>>7N70>|g=Y+$&SM$x(-?Nw1boz4z&Ccu34`};GH(-2MM zgobXDV3Q8qhaUtS%#K%LLZs_<>CUY?TTUvA+?#Nswf9etEvBVNMajzkn6|&`#eYJeYt8vFLIFFp~|Y z)4+AyycUKN+*F!ALA>4>dQm`5mX1t@ps}CRcH(U!0^l!17Q>e|7U!f5_hT3sVp^!j z=umIZJ9MpsMG_VTi?v@2sw9!Rutj#X-_<&S&5UQpin#K~xNfTzw;2#7mfITSY`@DU zts2D_g9F~_b+i)*(+jkP&pUUmzb>9^77x;xaAd)c10im)L!2A#25T0R7)KxSuBg(Qux zV$a16&E_J7^{K4Is};~EX;1(pb#bt7lc)C}-e)o}hu|X!TJlQMC1Z1FT5EzZ>*zQ% zpLMk5W)6C(t}dX1pRgi7XSS{TfTLka&TL*{01;5wX(% z7BEQa@d#jv8tVwpM4)id<}4l>YuZbj+ShuwTOG{kDJCU%f+U`5?tmUDUfsIMbyb5e) zY6~C;3cuu++;ojuLP>~ugA@1gashw^P{rz-s_7mGv=kAdqL3Dl$9MtzSR<&El0u@FeRXsrl6(A$4#3#p| zfkP^gQ83a|M}mxVzH5~4AxP#o>}%jTvma9Rg?r7*Yfk69N={7%R_$iA%N_vu@i5P- z!^q z0f3Z-bIV6{I~P}BU~7Rym`V|JR4FsP0v{p6w$#P|036xYv2+3%;h}d=Zb03aIYTNb zG(5o;S7T}8Md?fprRsfJ7v7ymh8N)b0Ifk-{7=-u-b&J&{X1>SW(&a5B(#ej4tXP{ zAR|fL$_j4%rL${iiad)a(;n4tZzg{|cw`8eQ7aza&(%AD6FS@ z6ps_13GKM5KIKBQ*=-Zeu-f&wWWhr^4ChA3zU%R15F*px=INR;gT-d2H3j^~sOUNH z`h*psw6qqL_KII*fe$sLQ%raq^a9U)XqEV+S5Gg-@&QT zx!yH^tALwA5AshCrELSaEI5K0rM$ zo|(HBR0Yf$_r@~~bA!ggK76Zau%=OX8^^640p0HR1(iMimAvfTLHE;8(U7dlUr7*GHxK8F?|h2Ut5Ll*8Q6D;zNBbYy&URjy>K2QeQ<)@HP64K8|a5q&dX z(s${{3=)=Bh*|D>4aH;0F{AaVD8)#H(YN_4O7*j*+Z>~zLbv-s(miQvTpb|+M!{yQ z`5nZt0JDU#JWZrSlo=xePB6y^w&bnS6-UrhLLNPV$`T~i*pmZS!O9F@T?6KU@SxH< z{>6?+i}sos<0d#O*VeTYa1{YrT0x?_nUx|?SY?u(tJ5e@eZnG=)C&upPTUQ|=PbJc z&>RRBK?0C`i%e#$VSfr>u)`BzS9^+O$1VcwCuTco@_deXlt9~(;u2j)?tVX|>S%fT zQA7?SNSVz8gdrFt9%OsKVe=t)Hb^ZZd=%H+-9F%^zcnFMC95*a?2($Lnx^QP$~n6* z5J`PJO2K?4)jmP=jKy1oLKEVyZ5UJuaHiizV56B=mu)M{vu|$r13a9I#sgO2+&j9^ z58D6(-17j@PypO?(@1n94FRhy1+rl*QLrN>jMB6nAwiG`GPv)RL;x1y(H{-qcmXtk z1j{8)J@5$O&sABgz`)AiMgo+|@7JV(WZrGd8>O}!uPXuG@0-3BNYL>uU17hp2R2N3 z7g&h_Vok|31-XXuWtLVJlK3jZysNJ`9eRq0Xj5LzX9>U|F7RUZW`1NQp=ugW0o2}X zGRW8yfEOTw$1qWYvuBj4>b`%MS2rnu2{MAfIR>w-?T&9bby)}u^Z(uFncu|Qn#rv; zw7W$3FuZ_o;|UnFU-}bmP|eB#KD}cXGG2}uXz0SPKwgOf?CX;n51%LVQSjYh6%pEU zpW&T!=dl~q!D6h0Uwww^@(E%*R(=Ha6Z_vH2dFe1c`XUpQ&>a6@py=i4uCbMjI6GV z?KfhFLHm4aFsjiPOvAPKQ*7t8kCtkZBH|kvPJC2zAR{H$UtEFq_c`U}R2MA0tr=`L zf}1*)#rID@!Epe{c@Z%*%8&!09 zNu;O-K7XkiPJD*b)xEC@;-c*Xf7#LPuj?gZGkox=tlMIU#@E4UWCh@C(&SJ2lxN|w zhm|=(jXwn)+p#ow%v1A&!M*X(3VvkEl_;khqYp)}$24>2XyW1gnAa6! zBonVI+*Tg60x^8m>OM#K7#AYm~H7j=S=(tOx zk170A@n)*FA>v->o#JY*f~{{0{lw`}K1c4WcZYC?vem^i>#a@+9iv)22ddQ-aLdoH zTOaukdkNtMefE8cWyeQDHUsEZ$Eu)tGfFLA&@4kPtO#x3E+w&iUxQuj;1Sc3+sUH^ zxRU+S1?G@Aoay+J2P<=^H*v%9&B1hw=c`*pz7}j1A2)S;Y&C$uof3|l;&+O2FcO{P z0|IV0olD|hb-%MyHLv#hEZSgHm`c8^I_@ntf$98?D64&thCb)6t#YVQxfwO$kDI?4PtV$YR9SGcm)f(BHW>;$89W1 z5_|aGZG0@_0NIU%(BN951B_y~PUUTrt5inqvNs{&B@%;kOAp!|Ja4ffXiCMS47?_` zQjEC9MBA#qz!k=xxVXnEZZ=FfHXUaQ;NA9qh?ayHM5ZGP=NkRPfDBlwLy5vdQTuz} ztE&sXE;1vjC9Q3}DbU|iGmw^K)wp}Y?e65+tkH`Vktzn!J3YyPJF(Gw+dyGVkcuI~ zl_gL_Nbj{csDR_?~kO* z4MxU_D{iKXw`T--6!nCot!XPz-@yUNYB@q2 zY{w2c>{rHnvQN_Gg8rxHSR4g0X-euFGme?oF@RNXINSGnMX`Ig6t|IoM zB)!nryObBRY&xF6J0xd6?l#qUTU_w4IzjvBXkq{zxT~pr^i2`sV@fx$7Ly!KJMBGK zuiz^dt|zoGmw;qvkf%Yq@3t#cqZ~YIzKn}|W?N)3Rd@AP?`U&1?AvQQ_5<} z3&Ag~kV}q1@TvGOzHjBKtHs-WD+;A8r;s>cj51L0f=?_Hbj5ta;YU>-ImoA7B&!PEbJKcbfLMG)LKo+o1!<(S4ujJ5NWFS{`-K)=|SX`ExR^K^a}NLzb{;RJFJpq{Rj&T@do7|7dv93{e&#=|SJl z8VbJEKBz?d%T0|_@JMQOi;=GRUPPymN5%t{wyGW&?c~8`J9cSP6yXo|fo2qh*u~if z>gx4y?>rB(Y+(|Y_gU~$=zy=wU{Vs0JE2?jcKYj^zTpV*#uSG3s;h@+6LqlK)e&4L zZd+_~k)w}acpa~|f?L775r(s9M2ncpjoTMhZIHaT`~(9Ga*+G@ST@Iu6AHEU9A|2T z$C0~h-pEVdw_h{#!Xv9}_P(%j zaJ4&`pmkMoPj|n!RL#6zSFAU|>aFKW<~`PH9_}bH%zoND=w;H&H8ysqaBBWN`PMxv z;qJoanp37~^|`5@V1eUdrX!`hK(MZazHt=Ca#@rsu?+^^x=U4h`tmc#1hRtJuE>c1k#`F61e`0avS#Wx1wu>3pYJ{s(HUd%)8A%T%cw)h`!Q{@ETFB?Qu9}CGPR9g1uei>1yh64Qu zHUmAjb|rGtT*63V=lRg5COJ%gcTYai)FT? zFNZrQoGN(pMiEXU$LtMuOkp*3j;`h7V$D4xQ|;xU779s89V6!?jJ}>`U%7#mCos!% zVF}oTiVU`I=4n|d@twS;^VN!G+dv6SyFOGoRpLbXn4X2p2I#OdX*mL@QU^u|a$~fe;(zZD!FY{mT0N>pM%&q7)p1eHv)yK7D^M zn;Y-p6p3FFB^U0;&put2#7&|rd|!sT;ZX(n{qd?(1q`1k`;{eJA7IJ*gLZVX34sN~ zH+4s)EEak38h$J8{MCwEcaAq=g#7(Y9ZDpRHseEgP6K$xm80n(L7gJG%HzHs0pv!pTSV zivvcFNshK}2o1bSSQS*mG`UwHRW~va%XGyYA(m&3w7{3WkhpPbDyp&`2#w>xECwTy zKRXHJx9s7yLHsN{Sdp&6<#0efVSqSEgX1WL+dghpWZ)*F zf6%_65MA&%YKLTyZWQ9AJW?wpORNf3&C;X41Y2j>zs5~{*|&FUtzDhN$7xmaB?S7^ zLBEP)2Bed$=C}&NgMHY}y%6Ga>-wQDFvG#m;jGC38$CuGuc<*_MV zr1#cXMnh`(Fsm$n`&;Ixfk~+iN|9q7HsoAISFS2;%m%s9vd+?8&E32U4V6658}JC< zERBd-v77+~xubf7DM1WXW$;K8j%jIP5kEdwRV}9(cb@22l1+;hn=GEw7a!lzI30QtVFCj zFWPp0GBLu3*#i2RV~qU)ZU%*;tRuf+bok})t;XZAoass!rUFfiacjboa*gGLo+hrRz%Ix9ppga#)r$ zFZ%{#6{XOkNa&-D;$p16t6jmv+w(exYGFYEv+E0Ial{d{)?5>U%R@OVq*_ zxbSBEX2^xMp2z$X2RB}`_vP4<5Epaj)z&AQo2-Fhp5|JpO=M_cTH&6T?=ZeB)Z;2F zCeoG{=kcz{_mqb`Zo8U{!=NLwBJ5_YZP2#Tqk|23R83n4R&Ph-o#Vsv+CS1Ov@5s7 z#WzHDj?32~9UA&B$l04SbJ(jke+sXL_^jnAswSQbk<1L0=noV_3BRv=CLC*19~tq1 zqRFhn-dynkYa=y~K5j7VyOxWVT#+{J9aS+m4T++~ZS&*T-a+!6m&Q2dqb~L4yYv>< z-wRDlE=i4|8xFoG(bJxnK193B(VT~4JFE?gxf%w>c!YJ&>aW#5JxM~h@>Nt*j zesB0qYWPyb)iV%t=@|LWALUhrw$6CbP0tkP=JsB>%*P{VA4=*Wir)7g^(GW^t}53y zHI$6IRUV?rrIl%d(^Y9!g2n^Kj(tq}SA-u)weetTfaf2p7#wd5$YND=f{gs{ z(#q33~1>vgLX~jy65b7B-|&*}=$omow``|J-UwJ56eWGzBep z)q2(jT)H+9!&R-=VgtE!1~Nw$!LoOmL_{z`B(f`klk}S8aw9q)JtmocM|drtB>XeG zsG8E?(UJL^E6O{_8%A!0UiZ77m%`a}^)HcGBMKCt9Aul3j25d3^e2l+O(&X-T+#hG z?vWnL$?S>c=aY?olYKdYMr65ZeHH~{f+jjT1dTsq#ap8IzRn-p;>euk2yLIaOq(};i0_FQHSF1NoD^*l^R<4A&z}AG^U}x&?HoiQi&dUX z+>2llgM{<7pGx;r?G0+;VpHH!NztbvkI77~!`_QU@~*s#rj22xs9Aj!lddh-YR)s~huewprZ#mTGauKgseh1? zOSNv^&I6qw^BlZa*n3} z<$RIlqe$-VRtejSNo1eM-TrZTjgjIvNXKf+u>kUWBE#yN;iAwaGhBC$pgWP|S7ha7 zvmS2~?dh+;Z1a)Od|J8$oA#rU=bfd!KwP}%8*>}j9v|G@j+L6VNK6LPuAQDPa=SS& zEk)7CY$H5G`Bsw)^);9buV32L&zWfbrC~ommjF&bI(k#B;pM;=!KC;wDY@I`_8%gC z5KIZn+wR7nN`?eyWZuJJVXQJT%^jLo5(L3Cto2sy-!ob4RTcE%&4*cG2jf`Y15@4B zLMl>t^)Sn`a~)e-iyJKGkmC3logRNi$G?%x&u?l_?K&(aWj9ZwlgHUw?8sKyU`At{ z|H(rd2`9#*84qmwY=1p-1Q8R8<*GQ$qpDt00!Qpu# zg9#PE;74>p-(C=*?G3)j*)Qfk-!-yb;W;NNcMhNM{z(%4iP{ZVvBSQC4-I(QwT#C_ zuu^3IfvF$TQYrr7RhZZ=*EQJpq6)UL6G!u2yH`g`@n(thD4*-Fpx6hkVj44M7%&6c zhq>afI4G%vAMU66+e9l_lg2ePGoU`t+dui!u?M4Z!SRv7uyz)$pq@ znU!Kerg>qq35#79z12%RuKteK8TOtIg)}PalVVN@N%gT#=)n|7=Gv zRH^(b48Fl~g$znt6sfrfi(~qea^$lvHi&~d>J$oPqmDMT5d!JPsgzq>6*e?-i7>P@ zbXqOtEDkrrKxJeYlbBqEl0uu|(KZRzP1W4lXZGkLhp8VuZ!!$Ll!&06c8+$gzM?iT;CJY>IG&GZk%(eony2!AOZ1{ zJw5?GwqKW2n*~B!D>%-qLGxAbX-G^0n-8bKgMr!VZNJn;6I^DW^lB{C@>g$^%k9F@ zN>>e&D8*;8fy+TEU8B(~^YUSjSZxaN7;P>74?g<^yDcGCTdwz@;$uRtx^v1m^|{(? zS?p!HvgrvPRdO72jOh)3*hn{(LU&be)p&D`oYDzNWM~3j@_fdJl;%De_&#&R@kYfd zMxzSHrJ}bTGIK;uqQ`TNZK@j}8cp0tOztW=xJe>nJ^Z`r;S*3tCt{rD&q5qH#CwOz zMZ6Luk8UY+q%*w<_xkXjQNp@)Tet@$tn2PDWxFrVa>X+5!%^}ZKPcbP5AON-91y#Q zJ*rxl>E36bb3vUE{}!@R zi~7^*^jw_sjPbZxcI`I^RT!-$ ziOAUg>UT{$F!3*Kyf8cVIZlRT&#b-qB9{im_tK4I*v|Q>zX&eezuuo`%~9T+L+|b~ z+bL>UdzI}su!E6jb-m>7vL`#a5wpJTQ~7dfkvT?v$K%msV83QG63@Z@A%t$kJLG%_qPY~YBv~yfsll6+nx#pw4)>{< zM+AF2nIR!2asxNS1hGrkgO9cj>X-b|NjPW}ne&@zBoK^Ye#K8(9Cj>OZge*;xr{}+ zulQkxcBV7n^P8%x{+3TBPpO)4i9Cu+B!Al6S^a5Io7a{p|( zI{|Ej`PJz-+L%R2j|JH9x2Nu;mp9%E!0isE)(b**48%qS9alP2vwZps$A#i65WBUL zKf3KsosyHqD88>Q6NjT*QY?3$2=tcS!&kx`ESwLzYgD-sP+pU3PaO^)0wusdS4By4m6fx(tbhcaDe!GM`&Kl+T z)sEiiCI3|WUQQ9Qe}x`#Zrowna>XcBwEzsZ&c0|5fe!PC4f$;M&9~)Ac6bF*d;7E- z$#c;{fPxJa!SiWV)i5g^?QdhLSMBH)?dYt{T!RjaT)^@(fL21SV+Td~!QBb&yd3vH z=I_6s|4QJ$68NtK{?|wVlqr6E;(o=yDO3EDNAq8+(EcRY{7HNI$l2K5#gV|EYwCH9 zOW}8J+`m+b{j~@&flBr-l%a%Tv7lxwq1Nk9l36aPpC7h|4vvmi_AVUv9Bj=%0bqMT z+exUA%cWojO21mcJUQe6iLSYeDd5dCwucdJ1>~uGKiN(}t=LBnn)X%%rcZjl-}z1d z(xtyvGM06h!i6e;z09uu|b$!}G=cXNr}MJ7tVs+AqRsHdeD zivQvUMDmRM((po6~%<>l8RH!P3(ZO-xwC*<%SN{v^3zKRoO(f`nt3SBa29sq; zs3@rXSgB_gdfweh)~NaR?YoSel(dvF5+q+f{e1U z<3E?o&t?Ezgqhp@WF6=GgCJL%UQbY105EKW0@MQhJO=0Jg+Q5TJ>j3s!$MF&0^2Z` zj5fUh_g@QR|6Evq4&+bm5#ay7P#lAI%GPz<7yWic>e<8Zzhgg%t-Vf7h?di8uSwA zsEVB{VxmU zuRidPg#v|wG5-FY2dp_kLTCRIX*2et(NjIm8)(V7k>M2l7Yi%v$7;?ogNwd)3d4^U z??5XGgo|@5x^hPQDsC%^gpCZObk@GRc`Nq<%UOw6-D=UoMQs5E&&e6@#67&fGyP~_ zQ^twwp3j|+H046ZIj(Sumxue#!qsp@^`>a`=G>(7rMu#m57$B}#~`tt9T#3eiIwiF zd!@~m6rMDZn;@B!m>VR-D_X1)65C5mH|m7~`{v+nFjV_S_IMQ4zSrIl)26zH>o+65YR1}#j)wTH50-?$k*Y-jkl9!#F9 z$mZNHu7YQNp!)t*U#jLly<8TSvPLWEU;y7{o&GNjwn_eBC7R6|4Jm zuPCvlo)#aOR9@Ubyx1BDN#G_iA^&Lpp6Y_iB?glakLQn_Z*zYvz^M*LIh!@|$_tHt zk1AoX5SF66bU__9?8D6Skx}_Q)$GEUU_al^%&OybV<>|h{<)!vb&QHjw4w>_xsg8E zKR$TTY1Io&%-Kj)a=HEdDQ&kz94V84aooQ#)*jC07UuxlywEui1OD-$7ZeZx_QHbn zA8iCa4Z?@s{vU08f*=t6TN^*Y!~U&Jfalli1pdvvJOaPILy*9g@UPF~;U)wee`^Dc zzqUbvnflvxyaXTr_cj8p#oyWl1%AC(5c*4BpuAAv>i+d!D4)lu6<$1h(0>57OtNi$Q1%A;NK3*Y#zv~|#FO=^u&x1J| z1NZ2BPE70^mpWiTrvH62#q~3M1hS`B2R>JZUQbL~mX}|eS5T0Ln_Kvv3^%_luMiKP ztn@wJdr)3MeqI3y`u}_haL5`~USP*2MA#^~zySBn&(+4q2jvz#$H;hJRrcKf0qQFT Axc~qF diff --git a/project/public/Backend/useSSENotifications.js b/project/public/Backend/useSSENotifications.js deleted file mode 100644 index 879e222..0000000 --- a/project/public/Backend/useSSENotifications.js +++ /dev/null @@ -1,38 +0,0 @@ -// hooks/useSSENotifications.js -import { useEffect, useCallback } from 'react'; - -export const useSSENotifications = (token, collaborateurId, onEventReceived) => { - useEffect(() => { - if (!token || !collaborateurId) return; - - const eventSource = new EventSource( - `/api/events?token=${encodeURIComponent(token)}` - ); - - eventSource.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - - console.log('📨 SSE reçu:', data); - - // Log spécifique pour les récupérations - if (data.type === 'demande-validated' && data.typeConge === 'Récupération') { - console.log('🎨 Couleur reçue:', data.couleurHex); - } - - onEventReceived(data); - } catch (error) { - console.error('❌ Erreur parsing SSE:', error); - } - }; - - eventSource.onerror = (error) => { - console.error('❌ Erreur SSE:', error); - eventSource.close(); - }; - - return () => { - eventSource.close(); - }; - }, [token, collaborateurId, onEventReceived]); -}; \ No newline at end of file diff --git a/project/public/Backend/webhook-utils.js b/project/public/Backend/webhook-utils.js index 40b1bcf..22fd15b 100644 --- a/project/public/Backend/webhook-utils.js +++ b/project/public/Backend/webhook-utils.js @@ -1,4 +1,4 @@ -// webhook-utils.js (VERSION ES MODULES - CORRIGÉE) +// webhook-utils.js (VERSION ES MODULES) // Pour projets avec "type": "module" dans package.json import axios from 'axios'; @@ -65,7 +65,6 @@ class WebhookManager { for (let attempt = 1; attempt <= retries; attempt++) { try { console.log(`📤 Envoi webhook: ${eventType} vers ${targetUrl} (tentative ${attempt}/${retries})`); - console.log(` Données:`, JSON.stringify(data, null, 2)); const response = await axios.post( `${targetUrl}/api/webhook/receive`, diff --git a/project/public/php/Dockerfile.backend b/project/public/php/Dockerfile.backend new file mode 100644 index 0000000..bbcc787 --- /dev/null +++ b/project/public/php/Dockerfile.backend @@ -0,0 +1,14 @@ +# Utilise une image PHP avec Apache et la version 8.1 +FROM php:8.1-apache + +# Installe l'extension mysqli pour te connecter à la base de données MySQL +RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli + +# Active le module de réécriture d'URL d'Apache (souvent utile) +RUN a2enmod rewrite + +# Copie tous les fichiers du back-end dans le dossier de travail d'Apache +COPY . /var/www/html/ + +# Expose le port 80 (par défaut pour un serveur web) +EXPOSE 80 \ No newline at end of file diff --git a/project/public/php/check-user-groups.php b/project/public/php/check-user-groups.php new file mode 100644 index 0000000..eaa44e4 --- /dev/null +++ b/project/public/php/check-user-groups.php @@ -0,0 +1,147 @@ +connect_error) { + die(json_encode(["authorized" => false, "message" => "Erreur DB: " . $conn->connect_error])); +} + +// --- ID du groupe cible (Ensup-Groupe) --- +$groupId = "c1ea877c-6bca-4f47-bfad-f223640813a0"; + +// Récupération des données POST +$data = json_decode(file_get_contents("php://input"), true); +$userPrincipalName = $data["userPrincipalName"] ?? ""; + +// Récupération du token dans les headers +$headers = getallheaders(); +$accessToken = isset($headers['Authorization']) + ? str_replace("Bearer ", "", $headers['Authorization']) + : ""; + +if (!$userPrincipalName || !$accessToken) { + echo json_encode(["authorized" => false, "message" => "Email ou token manquant"]); + exit; +} + +/** + * Fonction générique pour appeler Graph API + */ +function callGraph($url, $accessToken, $method = "GET", $body = null) { + $ch = curl_init($url); + $headers = ["Authorization: Bearer $accessToken"]; + if ($method === "POST") { + $headers[] = "Content-Type: application/json"; + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + return null; + } + return json_decode($response, true); +} + +/** + * Vérifier si utilisateur appartient à un groupe + */ +function isUserInGroup($userId, $groupId, $accessToken) { + $url = "https://graph.microsoft.com/v1.0/users/$userId/checkMemberGroups"; + $data = json_encode(["groupIds" => [$groupId]]); + $result = callGraph($url, $accessToken, "POST", $data); + + return $result && isset($result["value"]) && in_array($groupId, $result["value"]); +} + +// 🔹 1. Vérifier si utilisateur existe déjà en DB +$stmt = $conn->prepare("SELECT id, entraUserId, prenom, nom, email, service, role FROM CollaborateurAD WHERE email = ? LIMIT 1"); +$stmt->bind_param("s", $userPrincipalName); +$stmt->execute(); +$result = $stmt->get_result(); +$user = $result->fetch_assoc(); +$stmt->close(); + +if ($user) { + echo json_encode([ + "authorized" => true, + "role" => $user["role"], + "groups" => [$user["role"]], + "localUserId" => (int)$user["id"], // 🔹 ajout important + "user" => $user + ]); + $conn->close(); + exit; +} + + +// 🔹 2. Sinon → chercher l’utilisateur dans Microsoft Graph +$userGraph = callGraph("https://graph.microsoft.com/v1.0/users/$userPrincipalName?\$select=id,displayName,givenName,surname,mail,department,jobTitle", $accessToken); + +if (!$userGraph) { + echo json_encode([ + "authorized" => false, + "message" => "Utilisateur introuvable dans Entra ou token invalide" + ]); + $conn->close(); + exit; +} + +// 🔹 3. Vérifier appartenance au groupe Ensup-Groupe +$isInTargetGroup = isUserInGroup($userGraph["id"], $groupId, $accessToken); + +if (!$isInTargetGroup) { + echo json_encode([ + "authorized" => false, + "message" => "Utilisateur non autorisé : il n'appartient pas au groupe requis" + ]); + $conn->close(); + exit; +} + +// 🔹 4. Insérer dans la base si nouveau +$entraUserId = $userGraph["id"]; +$prenom = $userGraph["givenName"] ?? ""; +$nom = $userGraph["surname"] ?? ""; +$email = $userGraph["mail"] ?? $userPrincipalName; +$service = $userGraph["department"] ?? ""; +$role = "Collaborateur"; // rôle par défaut + +$stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, role) + VALUES (?, ?, ?, ?, ?, ?)"); +$stmt->bind_param("ssssss", $entraUserId, $prenom, $nom, $email, $service, $role); +$stmt->execute(); +$newUserId = $stmt->insert_id; +$stmt->close(); + +// 🔹 5. Réponse finale +echo json_encode([ + "authorized" => true, + "role" => $role, + "groups" => [$role], + "localUserId" => (int)$newUserId, + "user" => [ + "id" => $newUserId, + "entraUserId" => $entraUserId, + "prenom" => $prenom, + "nom" => $nom, + "email" => $email, + "service" => $service, + "role" => $role + ] +]); + + +$conn->close(); +?> diff --git a/project/public/php/db.php b/project/public/php/db.php new file mode 100644 index 0000000..a3ab16f --- /dev/null +++ b/project/public/php/db.php @@ -0,0 +1,20 @@ +connect_error) { + die(json_encode([ + "success" => false, + "message" => "Erreur DB: " . $conn->connect_error + ])); +} + +// Important : définir l’encodage en UTF-8 (pour accents, etc.) +$conn->set_charset("utf8mb4"); diff --git a/project/public/php/getAllTeamRequests.php b/project/public/php/getAllTeamRequests.php new file mode 100644 index 0000000..8a65dad --- /dev/null +++ b/project/public/php/getAllTeamRequests.php @@ -0,0 +1,103 @@ +connect_error) { + error_log("Erreur connexion DB: " . $conn->connect_error); + echo json_encode(["success" => false, "message" => "Erreur de connexion DB"]); + exit(); +} + +// Récupération ID manager +$managerId = $_GET['SuperieurId'] ?? null; +if (!$managerId) { + echo json_encode(["success" => false, "message" => "Paramètre SuperieurId manquant"]); + exit(); +} + +$sql = " + SELECT + dc.Id, + dc.DateDebut, + dc.DateFin, + dc.Statut, + dc.DateDemande, + dc.Commentaire, + dc.DocumentJoint, + dc.CollaborateurADId AS employee_id, + CONCAT(ca.Prenom, ' ', ca.Nom) as employee_name, + ca.Email as employee_email, + tc.Nom as type + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN TypeConge tc ON dc.TypeCongeId = tc.Id + JOIN HierarchieValidationAD hv ON hv.CollaborateurId = ca.id + WHERE hv.SuperieurId = ? + ORDER BY dc.DateDemande DESC +"; + +$stmt = $conn->prepare($sql); +$stmt->bind_param("i", $managerId); +$stmt->execute(); +$result = $stmt->get_result(); + +$requests = []; +while ($row = $result->fetch_assoc()) { + $startDate = new DateTime($row['DateDebut']); + $endDate = new DateTime($row['DateFin']); + $submittedDate = new DateTime($row['DateDemande']); + $days = 0; + + $tmp = clone $startDate; + while ($tmp <= $endDate) { + if ((int)$tmp->format('N') < 6) $days++; + $tmp->modify('+1 day'); + } + + $requests[] = [ + "id" => (int)$row['Id'], + "employee_id" => (int)$row['employee_id'], + "employee_name" => $row['employee_name'], + "employee_email" => $row['employee_email'], + "type" => $row['type'], + "start_date" => $row['DateDebut'], + "end_date" => $row['DateFin'], + "date_display" => $row['DateDebut'] === $row['DateFin'] + ? $startDate->format('d/m/Y') + : $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'), + "days" => $days, + "status" => $row['Statut'], + "reason" => $row['Commentaire'] ?: '', + "file" => $row['DocumentJoint'] ?: null, + "submitted_at" => $row['DateDemande'], + "submitted_display" => $submittedDate->format('d/m/Y') + ]; +} + +echo json_encode([ + "success" => true, + "requests" => $requests +]); + +$stmt->close(); +$conn->close(); +?> diff --git a/project/public/php/getEmploye.php b/project/public/php/getEmploye.php new file mode 100644 index 0000000..844feae --- /dev/null +++ b/project/public/php/getEmploye.php @@ -0,0 +1,52 @@ +connect_error) { + die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error])); +} + +// Récupérer l'ID +$id = isset($_GET['id']) ? (int)$_GET['id'] : 0; +if ($id <= 0) { + echo json_encode(["success" => false, "message" => "ID collaborateur invalide"]); + exit; +} + +try { + $stmt = $conn->prepare(" + SELECT id, Nom, Prenom, Email + FROM CollaborateurAD + WHERE id = ? +"); + + $stmt->bind_param("i", $id); + $stmt->execute(); + $result = $stmt->get_result(); + $employee = $result->fetch_assoc(); + + if ($employee) { + echo json_encode(["success" => true, "employee" => $employee]); + } else { + echo json_encode(["success" => false, "message" => "Collaborateur non trouvé"]); + } +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => "Erreur DB: " . $e->getMessage()]); +} + +$conn->close(); +?> diff --git a/project/public/php/getEmployeRequest.php b/project/public/php/getEmployeRequest.php new file mode 100644 index 0000000..bdb7358 --- /dev/null +++ b/project/public/php/getEmployeRequest.php @@ -0,0 +1,66 @@ +connect_error) { + die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error])); +} + +// Récupérer l'ID +$id = isset($_GET['id']) ? (int)$_GET['id'] : 0; +if ($id <= 0) { + echo json_encode(["success" => false, "message" => "ID employé invalide"]); + exit; +} + +try { + $sql = "SELECT Id, TypeCongeId, NombreJours, DateDebut, DateFin, Statut + FROM DemandeConge + WHERE EmployeeId = ? + ORDER BY DateDemande DESC"; + + $stmt = $conn->prepare($sql); + $stmt->bind_param("i", $id); + $stmt->execute(); + $result = $stmt->get_result(); + + // Mapping des types de congés + $typeNames = [ + 1 => "Congé payé", + 2 => "RTT", + 3 => "Maladie" + ]; + + $requests = []; + while ($row = $result->fetch_assoc()) { + $row['type'] = $typeNames[$row['TypeCongeId']] ?? "Autre"; + $row['days'] = (float)$row['NombreJours']; + // Formater jours : 2j ou 1.5j + $row['days_display'] = ((int)$row['days'] == $row['days'] ? (int)$row['days'] : $row['days']) . "j"; + $row['date_display'] = date("d/m/Y", strtotime($row['DateDebut'])) + . " - " + . date("d/m/Y", strtotime($row['DateFin'])); + $requests[] = $row; + } + + echo json_encode(["success" => true, "requests" => $requests]); +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => "Erreur DB: " . $e->getMessage()]); +} + +$conn->close(); +?> diff --git a/project/public/php/getLeaveCounters.php b/project/public/php/getLeaveCounters.php new file mode 100644 index 0000000..21d331a --- /dev/null +++ b/project/public/php/getLeaveCounters.php @@ -0,0 +1,163 @@ +connect_error) { + error_log("Erreur DB: " . $conn->connect_error); + echo json_encode(['success' => false, 'message' => 'Erreur de connexion à la base de données']); + exit; +} + +$today = new DateTime(); +$yearCurrent = (int)$today->format('Y'); +$yearNMinus1 = $yearCurrent - 1; + +function getTypeId($conn, $nom) { + $stmt = $conn->prepare("SELECT Id FROM TypeConge WHERE Nom=?"); + $stmt->bind_param("s", $nom); + $stmt->execute(); + $result = $stmt->get_result(); + $id = null; + if ($row = $result->fetch_assoc()) { + $id = (int)$row['Id']; + } + $stmt->close(); + error_log("TypeConge '$nom' => Id $id"); + return $id; +} + +$cpTypeId = getTypeId($conn, 'Congé payé'); +$rttTypeId = getTypeId($conn, 'RTT'); + +$soldeReportInitial_CP = 0.0; +$soldeReportInitial_RTT = 0.0; + +$collaborateursResult = $conn->query("SELECT id FROM CollaborateurAD"); +if (!$collaborateursResult) { + error_log("Erreur récupération collaborateurs : ".$conn->error); + echo json_encode(['success' => false, 'message' => 'Erreur récupération collaborateurs']); + exit; +} + +while ($collab = $collaborateursResult->fetch_assoc()) { + $collabId = (int)$collab['id']; + + if ($cpTypeId !== null) { + $existsStmt = $conn->prepare("SELECT Id FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?"); + $existsStmt->bind_param("iii", $collabId, $cpTypeId, $yearNMinus1); + $existsStmt->execute(); + $existsStmt->store_result(); + if ($existsStmt->num_rows === 0) { + $insertStmt = $conn->prepare("INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Solde, Total, SoldeReporte) VALUES (?, ?, ?, ?, ?, ?)"); + $insertStmt->bind_param("iiiddd", $collabId, $cpTypeId, $yearNMinus1, $soldeReportInitial_CP, $soldeReportInitial_CP, $soldeReportInitial_CP); + if (!$insertStmt->execute()) { + error_log("Erreur insertion CP N-1 collaborateur $collabId : ".$insertStmt->error); + } + $insertStmt->close(); + } + $existsStmt->close(); + } + + if ($rttTypeId !== null) { + $existsStmt = $conn->prepare("SELECT Id FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?"); + $existsStmt->bind_param("iii", $collabId, $rttTypeId, $yearNMinus1); + $existsStmt->execute(); + $existsStmt->store_result(); + if ($existsStmt->num_rows === 0) { + $insertStmt = $conn->prepare("INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Solde, Total, SoldeReporte) VALUES (?, ?, ?, ?, ?, ?)"); + $insertStmt->bind_param("iiiddd", $collabId, $rttTypeId, $yearNMinus1, $soldeReportInitial_RTT, $soldeReportInitial_RTT, $soldeReportInitial_RTT); + if (!$insertStmt->execute()) { + error_log("Erreur insertion RTT N-1 collaborateur $collabId : ".$insertStmt->error); + } + $insertStmt->close(); + } + $existsStmt->close(); + } +} + +$cpStart = new DateTime("$yearCurrent-06-01"); +$cpEnd = new DateTime(($yearCurrent + 1) . "-05-31"); +$rttStart = new DateTime("$yearCurrent-01-01"); +$rttEnd = new DateTime("$yearCurrent-12-31"); + +$cpAnnualDays = 25; +$rttAnnualDays = 10; + +$cpPeriodDays = $cpEnd->diff($cpStart)->days + 1; +$rttPeriodDays = $rttEnd->diff($rttStart)->days + 1; + +$cpDailyIncrement = $cpAnnualDays / $cpPeriodDays; +$rttDailyIncrement = $rttAnnualDays / $rttPeriodDays; + +error_log("Incrément CP jour : $cpDailyIncrement"); +error_log("Incrément RTT jour : $rttDailyIncrement"); + +if ($today >= $cpStart && $today <= $cpEnd && $cpTypeId !== null) { + $exerciseYear = (int)$cpStart->format('Y'); + $stmt = $conn->prepare("UPDATE CompteurConges SET Solde = Solde + ? WHERE TypeCongeId = ? AND Annee = ?"); + $stmt->bind_param("dii", $cpDailyIncrement, $cpTypeId, $exerciseYear); + if (!$stmt->execute()) { + error_log("Erreur incrément CP N : ".$stmt->error); + } + $stmt->close(); +} + +if ($today >= $rttStart && $today <= $rttEnd && $rttTypeId !== null) { + $exerciseYear = $yearCurrent; + $stmt = $conn->prepare("UPDATE CompteurConges SET Solde = Solde + ? WHERE TypeCongeId = ? AND Annee = ?"); + $stmt->bind_param("dii", $rttDailyIncrement, $rttTypeId, $exerciseYear); + if (!$stmt->execute()) { + error_log("Erreur incrément RTT N : ".$stmt->error); + } + $stmt->close(); +} + +// Récupérer les compteurs actuels de l'utilisateur demandé en GET +$userId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : 0; +$data = []; + +if ($userId > 0) { + $stmt = $conn->prepare( + "SELECT tc.Nom, cc.Annee, cc.Solde, cc.Total, cc.SoldeReporte + FROM CompteurConges cc + JOIN TypeConge tc ON cc.TypeCongeId = tc.Id + WHERE cc.CollaborateurADId = ?" + ); + $stmt->bind_param("i", $userId); + $stmt->execute(); + $result = $stmt->get_result(); + + while ($row = $result->fetch_assoc()) { + $data[$row['Nom']] = [ + 'Annee' => $row['Annee'], + 'Solde' => (float)$row['Solde'], + 'Total' => (float)$row['Total'], + 'SoldeReporte' => (float)$row['SoldeReporte'], + ]; + } + $stmt->close(); +} + +$conn->close(); + +echo json_encode([ + 'success' => true, + 'message' => 'Compteurs mis à jour', + 'counters' => $data, +]); +exit; diff --git a/project/public/php/getNotifications.php b/project/public/php/getNotifications.php new file mode 100644 index 0000000..6854373 --- /dev/null +++ b/project/public/php/getNotifications.php @@ -0,0 +1,75 @@ +connect_error) { + http_response_code(500); + echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); + exit; +} + +$user_id = isset($_GET['user_id']) ? intval($_GET['user_id']) : 0; + +if ($user_id <= 0) { + http_response_code(400); + echo json_encode(["success" => false, "message" => "Paramètre user_id manquant ou invalide"]); + exit; +} + +// Récupérer les notifications non lues ou récentes (ex: dernières 30 j) +$query = " +SELECT Id, Titre, Message, Type, DemandeCongeId, DateCreation, lu +FROM Notifications +WHERE CollaborateurADId = ? +ORDER BY DateCreation DESC +LIMIT 50 +"; + +$stmt = $conn->prepare($query); +if (!$stmt) { + http_response_code(500); + echo json_encode(["success" => false, "message" => "Erreur préparation requête"]); + exit; +} + +$stmt->bind_param('i', $user_id); // ✅ correction ici +$stmt->execute(); +$result = $stmt->get_result(); + +$notifications = []; +while ($row = $result->fetch_assoc()) { + $notifications[] = [ + "Id" => intval($row['Id']), + "Titre" => $row['Titre'], + "Message" => $row['Message'], + "Type" => $row['Type'], + "DemandeCongeId" => intval($row['DemandeCongeId']), + "DateCreation" => $row['DateCreation'], + "lu" => intval($row['lu']) === 1, + ]; +} + +$stmt->close(); +$conn->close(); + +echo json_encode([ + "success" => true, + "notifications" => $notifications +]); diff --git a/project/public/php/getPendingRequests.php b/project/public/php/getPendingRequests.php new file mode 100644 index 0000000..92a7941 --- /dev/null +++ b/project/public/php/getPendingRequests.php @@ -0,0 +1,159 @@ +connect_error) { + error_log("Erreur connexion DB getPendingRequests: " . $conn->connect_error); + echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); + exit(); +} + +$managerId = $_GET['manager_id'] ?? null; + +if ($managerId === null) { + echo json_encode(["success" => false, "message" => "ID manager manquant"]); + exit(); +} + +error_log("getPendingRequests - Manager ID: $managerId"); + +// Fonction pour calculer les jours ouvrés +function getWorkingDays($startDate, $endDate) { + $workingDays = 0; + $current = new DateTime($startDate); + $end = new DateTime($endDate); + + while ($current <= $end) { + $dayOfWeek = (int)$current->format('N'); + if ($dayOfWeek < 6) { + $workingDays++; + } + $current->modify('+1 day'); + } + return $workingDays; +} + +try { + // Récupérer le service du manager (table CollaborateurAD) + $queryManagerService = "SELECT ServiceId FROM CollaborateurAD WHERE id = ?"; + $stmtManager = $conn->prepare($queryManagerService); + $stmtManager->bind_param("i", $managerId); + $stmtManager->execute(); + $resultManager = $stmtManager->get_result(); + + if ($managerRow = $resultManager->fetch_assoc()) { + $serviceId = $managerRow['ServiceId']; + error_log("getPendingRequests - Service ID du manager: $serviceId"); + + // Récupérer les demandes en attente (multi-types) + $queryRequests = " + SELECT + dc.Id, + dc.DateDebut, + dc.DateFin, + dc.Statut, + dc.DateDemande, + dc.Commentaire, + dc.CollaborateurADId, + CONCAT(ca.prenom, ' ', ca.nom) as employee_name, + ca.email as employee_email, + GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') as types + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) + WHERE ca.ServiceId = ? + AND dc.Statut = 'En attente' + AND ca.id != ? + GROUP BY + dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, + dc.Commentaire, dc.CollaborateurADId, ca.prenom, ca.nom, ca.email + ORDER BY dc.DateDemande ASC + "; + + $stmtRequests = $conn->prepare($queryRequests); + $stmtRequests->bind_param("ii", $serviceId, $managerId); + $stmtRequests->execute(); + $resultRequests = $stmtRequests->get_result(); + + $requests = []; + while ($row = $resultRequests->fetch_assoc()) { + $workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']); + + $startDate = new DateTime($row['DateDebut']); + $endDate = new DateTime($row['DateFin']); + $submittedDate = new DateTime($row['DateDemande']); + + if ($row['DateDebut'] === $row['DateFin']) { + $dateDisplay = $startDate->format('d/m/Y'); + } else { + $dateDisplay = $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'); + } + + $requests[] = [ + 'id' => (int)$row['Id'], + 'employee_id' => (int)$row['CollaborateurADId'], + 'employee_name' => $row['employee_name'], + 'employee_email' => $row['employee_email'], + 'type' => $row['types'], // ex: "Congé payé, RTT" + 'start_date' => $row['DateDebut'], + 'end_date' => $row['DateFin'], + 'date_display' => $dateDisplay, + 'days' => $workingDays, + 'status' => $row['Statut'], + 'reason' => $row['Commentaire'] ?: '', + 'submitted_at' => $row['DateDemande'], + 'submitted_display' => $submittedDate->format('d/m/Y') + ]; + } + + error_log("getPendingRequests - Demandes en attente trouvées: " . count($requests)); + + echo json_encode([ + "success" => true, + "message" => "Demandes en attente récupérées avec succès", + "requests" => $requests, + "service_id" => $serviceId + ]); + + $stmtRequests->close(); + } else { + error_log("getPendingRequests - Manager non trouvé: $managerId"); + echo json_encode([ + "success" => false, + "message" => "Manager non trouvé" + ]); + } + + $stmtManager->close(); + +} catch (Exception $e) { + error_log("Erreur getPendingRequests: " . $e->getMessage()); + echo json_encode([ + "success" => false, + "message" => "Erreur lors de la récupération des demandes: " . $e->getMessage() + ]); +} + +$conn->close(); +?> diff --git a/project/public/php/getRequests.php b/project/public/php/getRequests.php new file mode 100644 index 0000000..b5a02d2 --- /dev/null +++ b/project/public/php/getRequests.php @@ -0,0 +1,133 @@ +connect_error) { + echo json_encode(["success" => false, "message" => "Erreur connexion DB: " . $conn->connect_error]); + exit(); +} + +// Récup paramètre +$userId = $_GET['user_id'] ?? null; +if (!$userId) { + echo json_encode(["success" => false, "message" => "ID utilisateur manquant"]); + exit(); +} + +// Fonction jours ouvrés +function getWorkingDays($startDate, $endDate) { + $workingDays = 0; + $current = new DateTime($startDate); + $end = new DateTime($endDate); + while ($current <= $end) { + $dayOfWeek = (int)$current->format('N'); + if ($dayOfWeek < 6) { + $workingDays++; + } + $current->modify('+1 day'); + } + return $workingDays; +} + +try { + // Requête multi-types + $query = " + SELECT + dc.Id, + dc.DateDebut, + dc.DateFin, + dc.Statut, + dc.DateDemande, + dc.Commentaire, + dc.Validateur, + dc.DocumentJoint, + GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS TypeConges + FROM DemandeConge dc + JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) + WHERE (dc.EmployeeId = ? OR dc.CollaborateurADId = ?) + GROUP BY + dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, + dc.Commentaire, dc.Validateur, dc.DocumentJoint + ORDER BY dc.DateDemande DESC +"; + + $stmt = $conn->prepare($query); + if (!$stmt) { + throw new Exception("Erreur préparation SQL : " . $conn->error); + } + + $stmt->bind_param("ii", $userId, $userId); + $stmt->execute(); + $result = $stmt->get_result(); + + $requests = []; + while ($row = $result->fetch_assoc()) { + $workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']); + + // Format dates + $startDate = new DateTime($row['DateDebut']); + $endDate = new DateTime($row['DateFin']); + $submittedDate = new DateTime($row['DateDemande']); + + $dateDisplay = ($row['DateDebut'] === $row['DateFin']) + ? $startDate->format('d/m/Y') + : $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'); + + // Lien fichier si congé maladie + $fileUrl = null; + if (strpos($row['TypeConges'], 'Congé maladie') !== false && !empty($row['DocumentJoint'])) { + $fileUrl = 'http://localhost/GTA/project/uploads/' . basename($row['DocumentJoint']); + } + + $requests[] = [ + 'id' => (int)$row['Id'], + 'type' => $row['TypeConges'], // ex: "Congé payé, RTT" + 'startDate' => $row['DateDebut'], + 'endDate' => $row['DateFin'], + 'dateDisplay' => $dateDisplay, + 'days' => $workingDays, + 'status' => $row['Statut'], + 'reason' => $row['Commentaire'] ?: 'Aucun commentaire', + 'submittedAt' => $row['DateDemande'], + 'submittedDisplay' => $submittedDate->format('d/m/Y'), + 'validator' => $row['Validateur'] ?: null, + 'fileUrl' => $fileUrl + ]; + } + + echo json_encode([ + "success" => true, + "message" => "Demandes récupérées avec succès", + "requests" => $requests, + "total" => count($requests) + ]); + +} catch (Exception $e) { + echo json_encode([ + "success" => false, + "message" => "Erreur: " . $e->getMessage() + ]); +} + +$conn->close(); diff --git a/project/public/php/getTeamLeaves.php b/project/public/php/getTeamLeaves.php new file mode 100644 index 0000000..f10b181 --- /dev/null +++ b/project/public/php/getTeamLeaves.php @@ -0,0 +1,228 @@ +connect_error) { + echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); + exit(); +} + +// On récupère le rôle directement depuis la requête GET pour la logique PHP +$userId = $_GET['user_id'] ?? null; +$role = strtolower($_GET['role'] ?? 'collaborateur'); + +if ($userId === null) { + echo json_encode(["success" => false, "message" => "ID utilisateur manquant"]); + exit(); +} + +try { + // 🔹 Infos utilisateur + $queryUser = " + SELECT ca.ServiceId, sa.CampusId, sa.SocieteId, + s.Nom as service_nom, c.Nom as campus_nom, so.Nom as societe_nom + FROM CollaborateurAD ca + JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId + JOIN Services s ON ca.ServiceId = s.Id + JOIN Campus c ON sa.CampusId = c.Id + JOIN Societe so ON sa.SocieteId = so.Id + WHERE ca.id = ? + LIMIT 1 + "; + $stmtUser = $conn->prepare($queryUser); + $stmtUser->bind_param("i", $userId); + $stmtUser->execute(); + $resultUser = $stmtUser->get_result(); + + if (!$userRow = $resultUser->fetch_assoc()) { + echo json_encode(["success" => false, "message" => "Collaborateur non trouvé"]); + exit(); + } + + $serviceId = $userRow['ServiceId']; + $campusId = $userRow['CampusId']; + $societeId = $userRow['SocieteId']; + + // ------------------------- + // 🔹 Construire la requête selon le rôle + // ------------------------- + switch ($role) { + case 'president': + case 'rh': + $queryLeaves = " + SELECT + DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date, + DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date, + CONCAT(ca.prenom, ' ', ca.nom) as employee_name, + tc.Nom as type, + tc.CouleurHex as color, + s.Nom as service_nom, + c.Nom as campus_nom, + so.Nom as societe_nom + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN TypeConge tc ON dc.TypeCongeId = tc.Id + JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId + JOIN Services s ON sa.ServiceId = s.Id + JOIN Campus c ON sa.CampusId = c.Id + JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI + WHERE dc.Statut = 'Validée' + ORDER BY c.Nom, so.Nom, s.Nom, dc.DateDebut ASC + "; + $stmtLeaves = $conn->prepare($queryLeaves); + break; + + case 'directeur de campus': + $queryLeaves = " + SELECT + DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date, + DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date, + CONCAT(ca.prenom, ' ', ca.nom) as employee_name, + tc.Nom as type, + tc.CouleurHex as color, + s.Nom as service_nom, + so.Nom as societe_nom, + c.Nom as campus_nom + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN TypeConge tc ON dc.TypeCongeId = tc.Id + JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId + JOIN Services s ON sa.ServiceId = s.Id + JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI + JOIN Campus c ON sa.CampusId = c.Id + WHERE sa.CampusId = ? + AND dc.Statut = 'Validée' + ORDER BY so.Nom, s.Nom, dc.DateDebut ASC + "; + $stmtLeaves = $conn->prepare($queryLeaves); + $stmtLeaves->bind_param("i", $campusId); + break; + + case 'validateur': + case 'collaborateur': + default: + $queryLeaves = " + SELECT + DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date, + DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date, + CONCAT(ca.prenom, ' ', ca.nom) as employee_name, + tc.Nom as type, + tc.CouleurHex as color, + s.Nom as service_nom, + c.Nom as campus_nom, + so.Nom as societe_nom + FROM DemandeConge dc + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id + JOIN TypeConge tc ON dc.TypeCongeId = tc.Id + JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId + JOIN Services s ON sa.ServiceId = s.Id + JOIN Campus c ON sa.CampusId = c.Id + JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI + WHERE ca.ServiceId = ? + AND sa.CampusId = ? + AND dc.Statut = 'Validée' + AND dc.DateFin >= CURDATE() - INTERVAL 30 DAY + ORDER BY dc.DateDebut ASC + "; + $stmtLeaves = $conn->prepare($queryLeaves); + $stmtLeaves->bind_param("ii", $serviceId, $campusId); + } + + $stmtLeaves->execute(); + $resultLeaves = $stmtLeaves->get_result(); + + $leaves = []; + while ($row = $resultLeaves->fetch_assoc()) { + $leaves[] = [ + 'start_date' => $row['start_date'], + 'end_date' => $row['end_date'], + 'employee_name' => $row['employee_name'], + 'type' => $row['type'], + 'color' => $row['color'] ?? '#3B82F6', + 'service_nom' => $row['service_nom'], + 'campus_nom' => $row['campus_nom'] ?? null, + 'societe_nom' => $row['societe_nom'] ?? null + ]; + } + + // ------------------------- + // 🔹 Construire les filtres dynamiques + // ------------------------- + $filters = []; + + if (in_array($role, ['collaborateur', 'validateur'])) { + $queryEmployees = " + SELECT CONCAT(ca.prenom, ' ', ca.nom) as employee_name + FROM CollaborateurAD ca + JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId + WHERE ca.ServiceId = ? + AND sa.CampusId = ? + ORDER BY ca.prenom, ca.nom + "; + $stmtEmployees = $conn->prepare($queryEmployees); + $stmtEmployees->bind_param("ii", $serviceId, $campusId); + $stmtEmployees->execute(); + $resultEmployees = $stmtEmployees->get_result(); + + $employees = []; + while ($row = $resultEmployees->fetch_assoc()) { + $employees[] = $row['employee_name']; + } + $filters['employees'] = $employees; + $stmtEmployees->close(); + + } elseif ($role === 'directeur de campus') { + // Pour le directeur, les filtres se basent sur les congés de son campus + $filters['societes'] = array_values(array_unique(array_column($leaves, 'societe_nom'))); + $filters['services'] = array_values(array_unique(array_column($leaves, 'service_nom'))); + + } elseif (in_array($role, ['president', 'rh'])) { + // 🔹 Récupérer tous les campus, sociétés, services de manière unique + $filters['campus'] = []; + $filters['societes'] = []; + $filters['services'] = []; + + $result = $conn->query("SELECT DISTINCT Nom as campus_nom FROM Campus ORDER BY campus_nom"); + while($row = $result->fetch_assoc()) $filters['campus'][] = $row['campus_nom']; + + $result = $conn->query("SELECT DISTINCT Nom as societe_nom FROM Societe ORDER BY societe_nom"); + while($row = $result->fetch_assoc()) $filters['societes'][] = $row['societe_nom']; + + $result = $conn->query("SELECT DISTINCT Nom as service_nom FROM Services ORDER BY service_nom"); + while($row = $result->fetch_assoc()) $filters['services'][] = $row['service_nom']; +} + + echo json_encode([ + "success" => true, + "role" => $role, + "leaves" => $leaves, + "filters" => $filters + ]); + + $stmtLeaves->close(); + $stmtUser->close(); + +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => "Erreur: " . $e->getMessage()]); +} + +$conn->close(); +?> \ No newline at end of file diff --git a/project/public/php/getTeamMembers.php b/project/public/php/getTeamMembers.php new file mode 100644 index 0000000..e6a910b --- /dev/null +++ b/project/public/php/getTeamMembers.php @@ -0,0 +1,116 @@ +connect_error) { + error_log("Erreur connexion DB getTeamMembersAD: " . $conn->connect_error); + echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); + exit(); +} + +$managerId = $_GET['manager_id'] ?? null; + +if ($managerId === null) { + echo json_encode(["success" => false, "message" => "ID manager manquant"]); + exit(); +} + +error_log("getTeamMembersAD - Manager ID: $managerId"); + +try { + // 🔹 1. Récupérer le ServiceId du manager + $queryManagerService = "SELECT ServiceId FROM CollaborateurAD WHERE id = ?"; + $stmtManager = $conn->prepare($queryManagerService); + $stmtManager->bind_param("i", $managerId); + $stmtManager->execute(); + $resultManager = $stmtManager->get_result(); + + if ($managerRow = $resultManager->fetch_assoc()) { + $serviceId = $managerRow['ServiceId']; + error_log("getTeamMembersAD - ServiceId du manager: $serviceId"); + + // 🔹 2. Récupérer tous les collaborateurs du même service (sauf le manager) + $queryTeam = " + SELECT + c.id, + c.nom, + c.prenom, + c.email, + c.role, + + s.Nom as service_name + FROM CollaborateurAD c + JOIN Services s ON c.ServiceId = s.Id + WHERE c.ServiceId = ? AND c.id != ? + ORDER BY c.prenom, c.nom + "; + + $stmtTeam = $conn->prepare($queryTeam); + $stmtTeam->bind_param("ii", $serviceId, $managerId); + $stmtTeam->execute(); + $resultTeam = $stmtTeam->get_result(); + + $teamMembers = []; + while ($row = $resultTeam->fetch_assoc()) { + $teamMembers[] = [ + 'id' => (int)$row['id'], + 'nom' => $row['nom'], + 'prenom' => $row['prenom'], + 'email' => $row['email'], + 'role' => $row['role'], + + 'service_name' => $row['service_name'] + ]; + } + + error_log("getTeamMembersAD - Membres trouvés: " . count($teamMembers)); + + echo json_encode([ + "success" => true, + "message" => "Équipe récupérée avec succès", + "team_members" => $teamMembers, + "service_id" => $serviceId + ]); + + $stmtTeam->close(); + } else { + error_log("getTeamMembersAD - Manager non trouvé: $managerId"); + echo json_encode([ + "success" => false, + "message" => "Manager non trouvé" + ]); + } + + $stmtManager->close(); + +} catch (Exception $e) { + error_log("Erreur getTeamMembersAD: " . $e->getMessage()); + echo json_encode([ + "success" => false, + "message" => "Erreur lors de la récupération de l'équipe: " . $e->getMessage() + ]); +} + +$conn->close(); +?> diff --git a/project/public/php/initial-sync.php b/project/public/php/initial-sync.php new file mode 100644 index 0000000..ba8857c --- /dev/null +++ b/project/public/php/initial-sync.php @@ -0,0 +1,104 @@ +connect_error) { + die(json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error])); +} + +$tenantId = "9840a2a0-6ae1-4688-b03d-d2ec291be0f9"; +$clientId = "4bb4cc24-bac3-427c-b02c-5d14fc67b561"; +$clientSecret = "ViC8Q~n4F5YweE18wjS0kfhp3kHh6LB2gZ76_b4R"; +$scope = "https://graph.microsoft.com/.default"; + +$url = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"; +$data = [ + "grant_type" => "client_credentials", + "client_id" => $clientId, + "client_secret" => $clientSecret, + "scope" => $scope +]; + +$ch = curl_init(); +curl_setopt($ch, CURLOPT_URL, $url); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$result = curl_exec($ch); +curl_close($ch); + +$tokenData = json_decode($result, true); +$accessToken = $tokenData["access_token"] ?? ""; +if (!$accessToken) { + die(json_encode(["success" => false, "message" => "Impossible d'obtenir un token Microsoft", "details" => $tokenData])); +} + +// --- ID du groupe cible (Ensup-Groupe) --- +$groupId = "c1ea877c-6bca-4f47-bfad-f223640813a0"; + +// --- Récupérer infos du groupe --- +$urlGroup = "https://graph.microsoft.com/v1.0/groups/$groupId?\$select=id,displayName,description,mail,createdDateTime"; +$ch = curl_init($urlGroup); +curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$respGroup = curl_exec($ch); +curl_close($ch); + +$group = json_decode($respGroup, true); +if (!isset($group["id"])) { + die(json_encode(["success" => false, "message" => "Impossible de récupérer le groupe Ensup-Groupe"])); +} + +$displayName = $group["displayName"] ?? ""; + +// --- Récupérer les membres du groupe --- +$urlMembers = "https://graph.microsoft.com/v1.0/groups/$groupId/members?\$select=id,givenName,surname,mail,department,jobTitle"; +$ch = curl_init($urlMembers); +curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$respMembers = curl_exec($ch); +curl_close($ch); + +$members = json_decode($respMembers, true)["value"] ?? []; + +$usersInserted = 0; +foreach ($members as $m) { + $entraUserId = $m["id"]; + $prenom = $m["givenName"] ?? ""; + $nom = $m["surname"] ?? ""; + $email = $m["mail"] ?? ""; + $service = $m["department"] ?? ""; + $description = $m["jobTitle"] ?? null; + if (!$email) continue; + + $stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, description, role) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE prenom=?, nom=?, email=?, service=?, description=?"); + if ($stmt) { + $role = "Collaborateur"; + $stmt->bind_param("ssssssssssss", + $entraUserId, $prenom, $nom, $email, $service, $description, $role, + $prenom, $nom, $email, $service, $description + ); + $stmt->execute(); + $usersInserted++; + } +} + +// --- Réponse finale --- +echo json_encode([ + "success" => true, + "message" => "Synchronisation terminée", + "groupe_sync" => $displayName, + "users_sync" => $usersInserted +]); + +$conn->close(); +?> diff --git a/project/public/php/login.php b/project/public/php/login.php new file mode 100644 index 0000000..cbb2e1f --- /dev/null +++ b/project/public/php/login.php @@ -0,0 +1,152 @@ +connect_error) { + die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error])); +} + +$data = json_decode(file_get_contents('php://input'), true); +$email = $data['email'] ?? ''; +$mot_de_passe = $data['mot_de_passe'] ?? ''; +$entraUserId = $data['entraUserId'] ?? ''; +$userPrincipalName = $data['userPrincipalName'] ?? ''; + +$headers = getallheaders(); +$accessToken = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : ''; + +// ====================================================== +// 1️⃣ Mode Azure AD (avec token + Entra) +// ====================================================== +if ($accessToken && $entraUserId) { + // Vérifier si utilisateur existe déjà dans CollaborateurAD + $stmt = $conn->prepare("SELECT * FROM CollaborateurAD WHERE entraUserId=? OR email=? LIMIT 1"); + $stmt->bind_param("ss", $entraUserId, $email); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows === 0) { + echo json_encode(["success" => false, "message" => "Utilisateur non autorisé (pas dans l'annuaire)"]); + exit(); + } + $user = $result->fetch_assoc(); + + // Récupérer groupes de l’utilisateur via Graph + $ch = curl_init("https://graph.microsoft.com/v1.0/users/$userPrincipalName/memberOf?\$select=id"); + curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + curl_close($ch); + + $dataGraph = json_decode($response, true); + $userGroups = []; + if (isset($dataGraph['value'])) { + foreach ($dataGraph['value'] as $g) { + if (isset($g['id'])) { + $userGroups[] = $g['id']; + } + } + } + + // Vérifier si au moins un groupe est autorisé + $res = $conn->query("SELECT Id FROM EntraGroups WHERE IsActive=1"); + $allowedGroups = []; + while ($row = $res->fetch_assoc()) { + $allowedGroups[] = $row['Id']; + } + + $authorized = count(array_intersect($userGroups, $allowedGroups)) > 0; + + if ($authorized) { + echo json_encode([ + "success" => true, + "message" => "Connexion réussie via Azure AD", + "user" => [ + "id" => $user['id'], + "prenom" => $user['prenom'], + "nom" => $user['nom'], + "email" => $user['email'], + "role" => $user['role'], + "service" => $user['service'] + ] + ]); + } else { + echo json_encode(["success" => false, "message" => "Utilisateur non autorisé - pas dans un groupe actif"]); + } + + $conn->close(); + exit(); +} + +// ====================================================== +// 2️⃣ Mode local (login/password → Users) +// ====================================================== +if ($email && $mot_de_passe) { + $query = " + SELECT + u.ID, + u.Prenom, + u.Nom, + u.Email, + u.Role, + u.ServiceId, + s.Nom AS ServiceNom + FROM Users u + LEFT JOIN Services s ON u.ServiceId = s.Id + WHERE u.Email = ? AND u.MDP = ? + "; + + $stmt = $conn->prepare($query); + + if ($stmt === false) { + die(json_encode(["success" => false, "message" => "Erreur de préparation : " . $conn->error])); + } + + $stmt->bind_param("ss", $email, $mot_de_passe); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows === 1) { + $user = $result->fetch_assoc(); + + echo json_encode([ + "success" => true, + "message" => "Connexion réussie (mode local)", + "user" => [ + "id" => $user['ID'], + "prenom" => $user['Prenom'], + "nom" => $user['Nom'], + "email" => $user['Email'], + "role" => $user['Role'], + "service" => $user['ServiceNom'] ?? 'Non défini' + ] + ]); + } else { + echo json_encode(["success" => false, "message" => "Identifiants incorrects (mode local)"]); + } + + $stmt->close(); + $conn->close(); + exit(); +} + +// ====================================================== +// 3️⃣ Aucun mode ne correspond +// ====================================================== +echo json_encode(["success" => false, "message" => "Aucune méthode de connexion fournie"]); +$conn->close(); +?> diff --git a/project/public/php/manualResetCounters.php b/project/public/php/manualResetCounters.php new file mode 100644 index 0000000..bd737d0 --- /dev/null +++ b/project/public/php/manualResetCounters.php @@ -0,0 +1,116 @@ + + + + + + + Réinitialisation des Compteurs + + + +
+

🔄 Réinitialisation des Compteurs de Congés

+ +
+

⚠️ ATTENTION

+

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

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

Cette action est irréversible !

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

✅ Réinitialisation réussie !

'; + echo '

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

'; + echo '

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

'; + echo '

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

'; + echo '

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

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

❌ Erreur lors de la réinitialisation

'; + echo '

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

'; + echo '
'; + } + } + ?> + +
+

+ +

+ +
+ +
+ +

📋 Informations sur les exercices

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

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

"; + echo "

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

"; + echo "

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

"; + ?> + +

🔗 Actions rapides

+

+ + + +

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

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

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

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

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

👋 Bienvenue sur votre application GTA !

-

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

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

📊 Vos compteurs de congés

-

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

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

🎉 Vous êtes prêt !

-

Vous pouvez maintenant utiliser l'application en toute autonomie.

-
-

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

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

👥 Bienvenue dans la gestion d'équipe !

-

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

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

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

-

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

-
-

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

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

✅ C'est tout pour cette section !

-

Vous pouvez maintenant consulter votre équipe facilement.

-
-

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

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

📅 Bienvenue dans le calendrier !

-

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

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

📅 Sélectionner des dates

-

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

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

🎉 Vous maîtrisez le calendrier !

-

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

-
-

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

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

- Ne plus afficher le tutoriel ? -

-

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

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

Chargement des soldes...

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

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

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

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

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

Répartition des {totalDays} jours ouvrés

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

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