First_Commit

This commit is contained in:
2026-01-12 12:16:53 +01:00
parent 89d74363f8
commit 91cd1dff2f
26 changed files with 6720 additions and 3093 deletions

View File

@@ -1,36 +1,38 @@
services:
backend:
image: ouijdaneim/gta-backend-dev:latest # ✅ Ajoute cette ligne
build:
context: ./project/public/Backend
dockerfile: DockerfileGTA.backend
container_name: gta-backend
container_name: gtaDev-backend
hostname: backend
ports:
- "8012:3000"
- "8014:3004"
volumes:
- ./project/public/Backend/uploads:/app/uploads
networks:
- gta-network
- gtaDev-network
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
frontend:
image: ouijdaneim/gta-frontend-dev:latest # ✅ Ajoute cette ligne
build:
context: ./project
dockerfile: DockerfileGTA.frontend
container_name: gta-frontend
container_name: gtaDev-frontend
hostname: frontend
ports:
- "3013:80"
- "3015:90"
environment:
- VITE_API_URL=http://backend:3000
- VITE_API_URL=http://backend:3004
networks:
- gta-network
- gtaDev-network
depends_on:
- backend
restart: unless-stopped
networks:
gta-network:
driver: bridge
gtaDev-network:
driver: bridge

229
package-lock.json generated
View File

@@ -6,9 +6,11 @@
"": {
"dependencies": {
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"express": "^5.1.0",
"framer-motion": "^12.23.22",
"node-cron": "^4.2.1"
"node-cron": "^4.2.1",
"react-datepicker": "^9.1.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.8.0",
@@ -79,7 +81,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -675,7 +676,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -699,15 +699,14 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@emnapi/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -717,9 +716,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1180,6 +1179,59 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.16",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
"integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.6",
"@floating-ui/utils": "^0.2.10",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2017,27 +2069,6 @@
"@sinonjs/commons": "^3.0.1"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
@@ -2094,9 +2125,9 @@
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -2104,13 +2135,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2946,7 +2970,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001737",
"electron-to-chromium": "^1.5.211",
@@ -3236,6 +3259,15 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3390,6 +3422,16 @@
"node": ">=18"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -3478,13 +3520,6 @@
"node": ">=8"
}
},
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5464,7 +5499,6 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -5579,16 +5613,6 @@
"yallist": "^3.0.2"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": {
"version": "0.30.18",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
@@ -6284,34 +6308,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -6414,38 +6410,27 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/react": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"devOptional": true,
"node_modules/react-datepicker": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz",
"integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
"@floating-ui/react": "^0.27.15",
"clsx": "^2.1.1",
"date-fns": "^4.1.0"
},
"peerDependencies": {
"react": "^19.1.1"
"date-fns-tz": "^3.0.0",
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"date-fns-tz": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -6619,13 +6604,6 @@
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -7130,6 +7108,12 @@
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tabbable": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
"integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
"license": "MIT"
},
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -7246,7 +7230,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -7531,7 +7514,6 @@
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -7648,7 +7630,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

View File

@@ -10,8 +10,10 @@
},
"dependencies": {
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"express": "^5.1.0",
"framer-motion": "^12.23.22",
"node-cron": "^4.2.1"
"node-cron": "^4.2.1",
"react-datepicker": "^9.1.0"
}
}

View File

@@ -1,4 +1,4 @@
FROM node:18-alpine
FROM node:20-alpine
WORKDIR /app
@@ -26,12 +26,12 @@ export default defineConfig({
},
server: {
host: '0.0.0.0',
port: 80,
port: 90,
strictPort: true,
allowedHosts: ['mygta.ensup-adm.net', 'localhost'],
allowedHosts: ['mygta-dev.ensup-adm.net', 'localhost'],
proxy: {
'/api': {
target: 'http://gta-backend:3000',
target: 'http://backend:3004',
changeOrigin: true,
secure: false,
configure: (proxy, options) => {
@@ -39,7 +39,7 @@ export default defineConfig({
console.log('Proxy error:', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('Proxying:', req.method, req.url, '-> http://gta-backend:3000');
console.log('Proxying:', req.method, req.url, '-> http://backend:3004');
});
}
}
@@ -48,6 +48,6 @@ export default defineConfig({
});
VITECONFIG
EXPOSE 80
EXPOSE 90
CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "80"]
CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "90"]

View File

@@ -18,7 +18,7 @@ COPY . .
RUN mkdir -p /app/uploads/medical
# Expose the port
EXPOSE 3000
EXPOSE 3004
# Start the server
CMD ["node", "server.js"]

View File

@@ -1,4 +1,4 @@
{
{
"name": "gta-backend",
"version": "1.0.0",
"description": "GTA Backend API",
@@ -10,7 +10,7 @@
},
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.6.5",
"mssql": "^10.0.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"multer": "^1.4.5-lts.1",
@@ -19,6 +19,7 @@
"body-parser": "^1.20.2",
"axios": "^1.6.0",
"node-cron": "^3.0.3"
},
"engines": {
"node": ">=18.0.0"

View File

@@ -1,6 +1,7 @@
import express from 'express';
import cors from 'cors';
import mysql from 'mysql2/promise';
import sql from 'mssql';
import axios from 'axios';
const app = express();
const PORT = 3000;
@@ -8,117 +9,562 @@ 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,
// Configuration Azure AD
const AZURE_CONFIG = {
tenantId: '9840a2a0-6ae1-4688-b03d-d2ec291be0f9',
clientId: '4bb4cc24-bac3-427c-b02c-5d14fc67b561',
clientSecret: 'gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t',
groupId: 'c1ea877c-6bca-4f47-bfad-f223640813a0'
};
// ✅ CRÉER LE POOL ICI, AU NIVEAU GLOBAL
const pool = mysql.createPool(dbConfig);
// Configuration SQL Server
const dbConfig = {
server: '192.168.0.3',
user: 'gta_app',
password: 'GTA2025!Secure',
database: 'GTA',
port: 1433,
options: {
encrypt: true,
trustServerCertificate: true,
enableArithAbort: true,
connectTimeout: 60000,
requestTimeout: 60000
},
pool: {
max: 10,
min: 0,
idleTimeoutMillis: 30000
}
};
// Route test connexion base + comptage collaborateurs
// Créer le pool de connexions
const pool = new sql.ConnectionPool(dbConfig);
// Connexion au démarrage
pool.connect()
.then(() => {
console.log('✅ Connecté à SQL Server');
console.log(` Base: ${dbConfig.database}@${dbConfig.server}`);
})
.catch(err => {
console.error('❌ Erreur connexion SQL Server:', err.message);
});
// ========================================
// WRAPPER POUR COMPATIBILITÉ (style MySQL)
// ========================================
pool.query = async function (queryText, params = []) {
if (!pool.connected) {
await pool.connect();
}
const request = pool.request();
// Ajouter les paramètres
params.forEach((value, index) => {
request.input(`param${index}`, value);
});
// Remplacer ? par @param0, @param1, etc.
let parameterizedQuery = queryText;
let paramIndex = 0;
parameterizedQuery = parameterizedQuery.replace(/\?/g, () => `@param${paramIndex++}`);
// Conversion LIMIT → TOP
parameterizedQuery = parameterizedQuery.replace(
/LIMIT\s+(\d+)/gi,
(match, limit) => {
return parameterizedQuery.includes('SELECT')
? parameterizedQuery.replace(/SELECT/i, `SELECT TOP ${limit}`)
: '';
}
);
const result = await request.query(parameterizedQuery);
return result.recordset || [];
};
// ========================================
// 🔑 FONCTION TOKEN MICROSOFT GRAPH
// ========================================
async function getGraphToken() {
try {
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: AZURE_CONFIG.clientId,
client_secret: AZURE_CONFIG.clientSecret,
scope: 'https://graph.microsoft.com/.default'
});
const response = await axios.post(
`https://login.microsoftonline.com/${AZURE_CONFIG.tenantId}/oauth2/v2.0/token`,
params.toString(),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
return response.data.access_token;
} catch (error) {
console.error('❌ Erreur obtention token:', error.message);
return null;
}
}
// ========================================
// 🔄 FONCTION SYNCHRONISATION ENTRA ID
// ========================================
async function syncEntraIdUsers() {
const syncResults = {
processed: 0,
inserted: 0,
updated: 0,
deactivated: 0,
errors: []
};
try {
console.log('\n🔄 === DÉBUT SYNCHRONISATION ENTRA ID ===');
// 1⃣ Obtenir le token
const accessToken = await getGraphToken();
if (!accessToken) {
console.error('❌ Impossible d\'obtenir le token');
return syncResults;
}
console.log('✅ Token obtenu');
// 2⃣ Récupérer le groupe
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;
console.log(`📋 Groupe : ${groupName}`);
// 3⃣ Récupérer tous les membres avec pagination
let allAzureMembers = [];
let nextLink = `https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}/members?$select=id,givenName,surname,mail,department,jobTitle,officeLocation,accountEnabled&$top=999`;
console.log('📥 Récupération des membres...');
while (nextLink) {
const membersResponse = await axios.get(nextLink, {
headers: { Authorization: `Bearer ${accessToken}` }
});
allAzureMembers = allAzureMembers.concat(membersResponse.data.value);
nextLink = membersResponse.data['@odata.nextLink'];
if (nextLink) {
console.log(` 📄 ${allAzureMembers.length} membres récupérés...`);
}
}
console.log(`${allAzureMembers.length} membres trouvés`);
// 4⃣ Filtrer les membres valides
const validMembers = allAzureMembers.filter(m => {
if (!m.mail || m.mail.trim() === '') return false;
if (m.accountEnabled === false) return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(m.mail);
});
console.log(`${validMembers.length} membres valides`);
// 5⃣ Traitement avec transaction
const transaction = new sql.Transaction(pool);
await transaction.begin();
try {
const azureEmails = new Set();
validMembers.forEach(m => {
azureEmails.add(m.mail.toLowerCase().trim());
});
console.log('\n📝 Traitement des utilisateurs...');
// 6⃣ Pour chaque membre
for (const m of validMembers) {
try {
const emailClean = m.mail.toLowerCase().trim();
syncResults.processed++;
// Vérifier existence
const request = new sql.Request(transaction);
request.input('email', sql.NVarChar, emailClean);
const result = await request.query(`
SELECT id, email, entraUserId, actif
FROM CollaborateurAD
WHERE LOWER(email) = LOWER(@email)
`);
if (result.recordset.length > 0) {
// MISE À JOUR
const updateRequest = new sql.Request(transaction);
updateRequest.input('entraUserId', sql.NVarChar, m.id);
updateRequest.input('prenom', sql.NVarChar, m.givenName || '');
updateRequest.input('nom', sql.NVarChar, m.surname || '');
updateRequest.input('departement', sql.NVarChar, m.department || '');
updateRequest.input('fonction', sql.NVarChar, m.jobTitle || '');
updateRequest.input('campus', sql.NVarChar, m.officeLocation || '');
updateRequest.input('email', sql.NVarChar, emailClean);
await updateRequest.query(`
UPDATE CollaborateurAD
SET
entraUserId = @entraUserId,
prenom = @prenom,
nom = @nom,
departement = @departement,
fonction = @fonction,
campus = @campus,
actif = 1
WHERE LOWER(email) = LOWER(@email)
`);
syncResults.updated++;
console.log(` ✓ Mis à jour : ${emailClean}`);
} else {
// INSERTION
const insertRequest = new sql.Request(transaction);
insertRequest.input('entraUserId', sql.NVarChar, m.id);
insertRequest.input('prenom', sql.NVarChar, m.givenName || '');
insertRequest.input('nom', sql.NVarChar, m.surname || '');
insertRequest.input('email', sql.NVarChar, emailClean);
insertRequest.input('departement', sql.NVarChar, m.department || '');
insertRequest.input('fonction', sql.NVarChar, m.jobTitle || '');
insertRequest.input('campus', sql.NVarChar, m.officeLocation || '');
await insertRequest.query(`
INSERT INTO CollaborateurAD
(entraUserId, prenom, nom, email, departement, fonction, campus, role, SocieteId, actif, dateCreation, TypeContrat)
VALUES (@entraUserId, @prenom, @nom, @email, @departement, @fonction, @campus, 'Collaborateur', 1, 1, GETDATE(), '37h')
`);
syncResults.inserted++;
console.log(` ✓ Créé : ${emailClean}`);
}
} catch (userError) {
syncResults.errors.push({
email: m.mail,
error: userError.message
});
console.error(` ❌ Erreur ${m.mail}:`, userError.message);
}
}
// 7⃣ DÉSACTIVATION des comptes absents
console.log('\n🔍 Désactivation des comptes obsolètes...');
if (azureEmails.size > 0) {
const activeEmailsList = Array.from(azureEmails).map(e => `'${e}'`).join(',');
const deactivateRequest = new sql.Request(transaction);
const deactivateResult = await deactivateRequest.query(`
UPDATE CollaborateurAD
SET actif = 0
WHERE
email IS NOT NULL
AND email != ''
AND LOWER(email) NOT IN (${activeEmailsList})
AND actif = 1
`);
syncResults.deactivated = deactivateResult.rowsAffected[0];
console.log(`${syncResults.deactivated} compte(s) désactivé(s)`);
}
await transaction.commit();
console.log('\n📊 === RÉSUMÉ ===');
console.log(` Groupe: ${groupName}`);
console.log(` Total Entra: ${allAzureMembers.length}`);
console.log(` Valides: ${validMembers.length}`);
console.log(` Traités: ${syncResults.processed}`);
console.log(` Créés: ${syncResults.inserted}`);
console.log(` Mis à jour: ${syncResults.updated}`);
console.log(` Désactivés: ${syncResults.deactivated}`);
console.log(` Erreurs: ${syncResults.errors.length}`);
} catch (error) {
await transaction.rollback();
throw error;
}
} catch (error) {
console.error('\n❌ ERREUR SYNCHRONISATION:', error.message);
}
return syncResults;
}
// ========================================
// 📡 ROUTES API
// ========================================
// Route test connexion
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;
const result = await pool.query('SELECT COUNT(*) AS count FROM CollaborateurAD', []);
const collaboratorCount = result[0]?.count || 0;
res.json({
success: true,
message: 'Connexion à la base OK',
message: 'Connexion SQL Server OK',
collaboratorCount,
});
} catch (error) {
console.error('Erreur connexion base:', error);
console.error('Erreur connexion:', error);
res.status(500).json({
success: false,
message: 'Erreur de connexion à la base',
message: 'Erreur connexion base',
error: error.message,
});
}
});
// Route sync unitaire
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);
const email = (req.body.mail || req.body.userPrincipalName)?.toLowerCase().trim();
const entraUserId = req.body.id;
// 1. Chercher user
const [users] = await conn.query('SELECT * FROM CollaborateurAD WHERE email = ?', [email]);
let userId;
let userRole;
if (!email) {
return res.json({ success: false, message: 'Email manquant' });
}
console.log(`\n🔄 Sync utilisateur : ${email}`);
const transaction = new sql.Transaction(pool);
await transaction.begin();
try {
// Vérifier existence
const checkRequest = new sql.Request(transaction);
checkRequest.input('email', sql.NVarChar, email);
const existing = await checkRequest.query(`
SELECT id, email, actif
FROM CollaborateurAD
WHERE LOWER(email) = LOWER(@email)
`);
if (existing.recordset.length > 0) {
// UPDATE
const updateRequest = new sql.Request(transaction);
updateRequest.input('collaborateurADId', sql.NVarChar, entraUserId);
updateRequest.input('prenom', sql.NVarChar, req.body.givenName || '');
updateRequest.input('nom', sql.NVarChar, req.body.surname || '');
updateRequest.input('departement', sql.NVarChar, req.body.department || '');
updateRequest.input('fonction', sql.NVarChar, req.body.jobTitle || '');
updateRequest.input('campus', sql.NVarChar, req.body.officeLocation || '');
updateRequest.input('email', sql.NVarChar, email);
updateRequest.input('dateMaj', sql.DateTime, new Date());
await updateRequest.query(`
UPDATE CollaborateurAD
SET
CollaborateurADId = @collaborateurADId,
prenom = @prenom,
nom = @nom,
departement = @departement,
fonction = @fonction,
campus = @campus,
actif = 1,
dateMiseAJour = @dateMaj
WHERE LOWER(email) = LOWER(@email)
`);
console.log(` ✅ Mis à jour : ${email}`);
} else {
// INSERT
const insertRequest = new sql.Request(transaction);
insertRequest.input('collaborateurADId', sql.NVarChar, entraUserId);
insertRequest.input('prenom', sql.NVarChar, req.body.givenName || '');
insertRequest.input('nom', sql.NVarChar, req.body.surname || '');
insertRequest.input('email', sql.NVarChar, email);
insertRequest.input('departement', sql.NVarChar, req.body.department || '');
insertRequest.input('fonction', sql.NVarChar, req.body.jobTitle || '');
insertRequest.input('campus', sql.NVarChar, req.body.officeLocation || '');
insertRequest.input('dateCreation', sql.DateTime, new Date());
insertRequest.input('dateMaj', sql.DateTime, new Date());
await insertRequest.query(`
INSERT INTO CollaborateurAD
(CollaborateurADId, prenom, nom, email, departement, fonction, campus, service, societe, actif, dateCreation, dateMiseAJour)
VALUES (@collaborateurADId, @prenom, @nom, @email, @departement, @fonction, @campus, NULL, NULL, 1, @dateCreation, @dateMaj)
`);
console.log(` ✅ Créé : ${email}`);
}
// Récupérer données
const getUserRequest = new sql.Request(transaction);
getUserRequest.input('email', sql.NVarChar, email);
const userData = await getUserRequest.query(`
SELECT id as localUserId, email, prenom, nom, fonction, departement
FROM CollaborateurAD
WHERE LOWER(email) = LOWER(@email)
`);
await transaction.commit();
if (userData.recordset.length === 0) {
throw new Error('Utilisateur introuvable après sync');
}
res.json({
success: true,
message: 'Sync réussie',
localUserId: userData.recordset[0].localUserId,
user: userData.recordset[0]
});
} catch (error) {
await transaction.rollback();
throw error;
}
} catch (error) {
console.error('❌ Erreur sync:', error);
res.json({
success: false,
message: error.message
});
}
});
// Route check groups
app.post('/api/check-user-groups', async (req, res) => {
try {
const { userPrincipalName } = req.body;
if (!userPrincipalName) {
return res.json({ authorized: false, message: 'Email manquant' });
}
const users = await pool.query(
'SELECT id, email, prenom, nom, actif FROM CollaborateurAD WHERE email = ?',
[userPrincipalName]
);
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 || ''
]);
const user = users[0];
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';
if (user.actif === 0) {
return res.json({ authorized: false, message: 'Compte désactivé' });
}
console.log('✅ User créé/récupéré:', userId);
return res.json({
authorized: true,
localUserId: user.id,
user: user
});
}
res.json({
authorized: true,
message: 'Sera créé au login'
});
} catch (error) {
console.error('❌ Erreur check:', error);
res.json({ authorized: false, error: error.message });
}
});
// Route sync complète manuelle
app.post('/api/sync-all', async (req, res) => {
try {
console.log('🚀 Sync complète manuelle...');
const results = await
IdUsers();
res.json({
success: true,
message: 'Sync terminée',
stats: results
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
});
// Route diagnostic
app.get('/api/diagnostic-sync', async (req, res) => {
try {
const totalDB = await pool.query(
'SELECT COUNT(*) as total, SUM(CASE WHEN actif = 1 THEN 1 ELSE 0 END) as actifs FROM CollaborateurAD',
[]
);
const sansEmail = await pool.query(
'SELECT COUNT(*) as total FROM CollaborateurAD WHERE email IS NULL OR email = \'\'',
[]
);
const derniers = await pool.query(
'SELECT TOP 10 id, prenom, nom, email, CollaborateurADId, actif FROM CollaborateurAD ORDER BY id DESC',
[]
);
// Test Entra
let entraStatus = { connected: false };
try {
const token = await getGraphToken();
if (token) {
const groupResponse = await axios.get(
`https://graph.microsoft.com/v1.0/groups/${AZURE_CONFIG.groupId}?$select=id,displayName`,
{ headers: { Authorization: `Bearer ${token}` } }
);
entraStatus = {
connected: true,
groupName: groupResponse.data.displayName
};
}
} catch (err) {
entraStatus.error = err.message;
}
res.json({
success: true,
localUserId: userId,
role: userRole
database: {
total: totalDB[0]?.total || 0,
actifs: totalDB[0]?.actifs || 0,
sansEmail: sansEmail[0]?.total || 0
},
entraId: entraStatus,
derniers_utilisateurs: derniers
});
} 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,
success: false,
error: error.message
});
}
});
app.listen(PORT, () => {
console.log(`✅ ✅ ✅ SERVEUR TEST DÉMARRÉ SUR LE PORT ${PORT} ✅ ✅ ✅`);
});
// ========================================
// 🚀 DÉMARRAGE
// ========================================
app.listen(PORT, "0.0.0.0", async () => {
console.log("✅ ==========================================");
console.log(" SERVEUR TEST DÉMARRÉ");
console.log(" Port:", PORT);
console.log(` Base SQL Server: ${dbConfig.database}@${dbConfig.server}`);
console.log("==========================================");
// Sync auto après 5 secondes
setTimeout(async () => {
console.log("\n🚀 Sync Entra ID automatique...");
await syncEntraIdUsers();
}, 5000);
});

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@ function AppContent() {
<Route
path="/demandes"
element={
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'RH', 'Admin']}>
<ProtectedRoute allowedRoles={['Validateur', 'Validatrice', 'Collaborateur', 'Collaboratrice', 'Apprenti', 'RH', 'Admin', 'Directeur de campus', 'Directrice de campus']}>
<Requests />
</ProtectedRoute>
}
@@ -85,7 +85,7 @@ function AppContent() {
<Route
path="/compte-rendu-activites"
element={
<ProtectedRoute allowedRoles={['Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
<CompteRenduActivites />
</ProtectedRoute>
}

View File

@@ -1,34 +1,46 @@
// authConfig.js
const hostname = window.location.hostname;
const protocol = window.location.protocol;
// Détection environnements (utile pour le debug)
// Détection environnements
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 : API URL
export const API_BASE_URL = "/api";
// --- MSAL Config ---
// ✅ EXPORT : MSAL Config - OPTIMISÉ POUR MOBILE iOS
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"
redirectUri: isProduction
? "https://mygta.ensup-adm.net"
: `${protocol}//${hostname}`,
navigateToLoginRequestUrl: false, // ✅ false pour éviter double redirection
postLogoutRedirectUri: isProduction
? "https://mygta.ensup-adm.net"
: `${protocol}//${hostname}`,
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
cacheLocation: "localStorage",
storeAuthStateInCookie: true,
},
system: {
allowRedirectInIframe: false,
allowNativeBroker: false,
loggerOptions: {
logLevel: "Verbose",
piiLoggingEnabled: false,
},
windowHashTimeout: 25000,
iframeHashTimeout: 25000,
loadFrameTimeout: 25000,
tokenRenewalOffsetSeconds: 300,
asyncPopups: false,
}
};
// --- Permissions Graph ---
// ✅ EXPORT : Permissions Graph
export const loginRequest = {
scopes: [
"User.Read",
@@ -37,11 +49,14 @@ export const loginRequest = {
"GroupMember.Read.All",
"Mail.Send",
],
prompt: "select_account",
responseMode: "fragment",
};
// ✅ Log de configuration au démarrage
console.log("🔧 Config Auth:", {
hostname,
protocol,
API_BASE_URL,
redirectUri: msalConfig.auth.redirectUri,
});
redirectUri: msalConfig.auth.redirectUri
});

File diff suppressed because it is too large Load Diff

View File

@@ -149,7 +149,7 @@ const GlobalTutorial = ({ userId, userRole }) => {
},
{
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 !',
content: '📅 Vos congés payés de l\'année précédente. ⚠️ Attention : ils doivent être soldés avant le 31 mai de l\'année suivante !',
placement: 'top',
},
{
@@ -164,7 +164,7 @@ const GlobalTutorial = ({ userId, userRole }) => {
},
{
target: '[data-tour="recup"]',
content: '🔄 Vos jours de récupération accumulés suite à des heures supplémentaires.',
content: '🔄 Vos jours de récupération accumulés suite au JPO/SF.',
placement: 'top',
},
{

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { X, Upload, AlertCircle } from 'lucide-react';
import { X, AlertCircle } from 'lucide-react';
const NewLeaveRequestModal = ({
onClose,
@@ -17,22 +17,82 @@ const NewLeaveRequestModal = ({
types: preselectedType ? [preselectedType] : [],
startDate: preselectedStartDate || '',
endDate: preselectedEndDate || '',
reason: '',
medicalDocuments: []
reason: ''
});
const [repartition, setRepartition] = useState({});
const [periodeSelection, setPeriodeSelection] = useState({});
const [totalDays, setTotalDays] = useState(0);
const [saturdayCount, setSaturdayCount] = useState(0);
const [holidayCount, setHolidayCount] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
// ⭐ État pour les données des compteurs
const [countersData, setCountersData] = useState(null);
const [isLoadingCounters, setIsLoadingCounters] = useState(true);
// ⭐ Charger les compteurs au montage
// ⭐ État pour stocker les jours fériés
const [publicHolidays, setPublicHolidays] = useState({});
// ⭐ Fonction pour vérifier si une date est un weekend
const isWeekend = (dateString) => {
if (!dateString) return false;
const date = new Date(dateString);
const day = date.getDay();
return day === 0 || day === 6; // 0 = Dimanche, 6 = Samedi
};
// ⭐ Fonction pour obtenir le prochain jour ouvrable
const getNextWorkingDay = (dateString) => {
const date = new Date(dateString);
const day = date.getDay();
// Si c'est vendredi (5), ajouter 3 jours pour arriver à lundi
if (day === 5) {
date.setDate(date.getDate() + 3);
}
// Si c'est samedi (6), ajouter 2 jours
else if (day === 6) {
date.setDate(date.getDate() + 2);
}
// Si c'est dimanche (0), ajouter 1 jour
else if (day === 0) {
date.setDate(date.getDate() + 1);
}
return date.toISOString().split('T')[0];
};
// ⭐ Charger les jours fériés depuis l'API gouvernementale
useEffect(() => {
const fetchPublicHolidays = async () => {
try {
const currentYear = new Date().getFullYear();
const nextYear = currentYear + 1;
const [currentYearData, nextYearData] = await Promise.all([
fetch(`https://calendrier.api.gouv.fr/jours-feries/metropole/${currentYear}.json`).then(r => r.json()),
fetch(`https://calendrier.api.gouv.fr/jours-feries/metropole/${nextYear}.json`).then(r => r.json())
]);
const allHolidays = { ...currentYearData, ...nextYearData };
setPublicHolidays(allHolidays);
console.log('📅 Jours fériés chargés:', allHolidays);
} catch (error) {
console.error('❌ Erreur chargement jours fériés:', error);
}
};
fetchPublicHolidays();
}, []);
// ⭐ Fonction pour vérifier si une date est un jour férié
const isPublicHoliday = (date) => {
const dateStr = date.toISOString().split('T')[0];
return dateStr in publicHolidays;
};
useEffect(() => {
const fetchCounters = async () => {
if (!userId) return;
@@ -60,7 +120,6 @@ const NewLeaveRequestModal = ({
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),
@@ -85,35 +144,45 @@ const NewLeaveRequestModal = ({
}
}, [preselectedStartDate, preselectedEndDate, preselectedType]);
// ⭐ Calcul des jours ouvrés en excluant les jours fériés
useEffect(() => {
if (formData.startDate && formData.endDate) {
const start = new Date(formData.startDate);
const end = new Date(formData.endDate);
let workingDays = 0;
let saturdays = 0;
let holidays = 0;
const current = new Date(start);
while (current <= end) {
const dayOfWeek = current.getDay();
if (dayOfWeek === 6) {
const isHoliday = isPublicHoliday(current);
if (isHoliday) {
holidays++;
console.log(`🎉 Jour férié détecté: ${current.toISOString().split('T')[0]} - ${publicHolidays[current.toISOString().split('T')[0]]}`);
} else if (dayOfWeek === 6) {
saturdays++;
} else if (dayOfWeek !== 0) {
workingDays++;
}
current.setDate(current.getDate() + 1);
}
setTotalDays(workingDays);
setSaturdayCount(saturdays);
setHolidayCount(holidays);
console.log('📊 Calcul période:', {
debut: formData.startDate,
fin: formData.endDate,
joursOuvres: workingDays,
samedis: saturdays
samedis: saturdays,
joursFeries: holidays
});
}
}, [formData.startDate, formData.endDate]);
}, [formData.startDate, formData.endDate, publicHolidays]);
const getMinDate = () => {
const tomorrow = new Date();
@@ -121,6 +190,13 @@ const NewLeaveRequestModal = ({
return tomorrow.toISOString().split('T')[0];
};
// ⭐ NOUVELLE FONCTION : Date minimum pour Formation (1 semaine = 7 jours)
const getMinDateFormation = () => {
const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);
return nextWeek.toISOString().split('T')[0];
};
useEffect(() => {
if (formData.types.length > 0) {
const newRepartition = {};
@@ -160,50 +236,8 @@ const NewLeaveRequestModal = ({
}
};
const handleFileUpload = (e) => {
const files = Array.from(e.target.files);
const validFiles = [];
const maxSize = 5 * 1024 * 1024;
for (const file of files) {
const validTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
if (!validTypes.includes(file.type)) {
setError(`Le fichier "${file.name}" n'est pas un format valide.`);
continue;
}
if (file.size > maxSize) {
setError(`Le fichier "${file.name}" est trop volumineux.`);
continue;
}
validFiles.push(file);
}
setFormData(prev => ({
...prev,
medicalDocuments: [...prev.medicalDocuments, ...validFiles]
}));
e.target.value = '';
};
const removeDocument = (index) => {
setFormData(prev => ({
...prev,
medicalDocuments: prev.medicalDocuments.filter((_, i) => i !== index)
}));
};
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
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);
@@ -211,7 +245,6 @@ const NewLeaveRequestModal = ({
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;
@@ -227,6 +260,17 @@ const NewLeaveRequestModal = ({
const start = new Date(formData.startDate);
const end = new Date(formData.endDate);
// ⭐ VALIDATION SPÉCIALE POUR FORMATION (7 JOURS D'AVANCE)
if (formData.types.includes('Formation')) {
const minFormationDate = new Date(today);
minFormationDate.setDate(minFormationDate.getDate() + 7);
if (start < minFormationDate) {
setError('La formation doit être posée au moins 7 jours à l\'avance (1 semaine minimum).');
return false;
}
}
if (start.getTime() === today.getTime() || end.getTime() === today.getTime()) {
setError("Vous ne pouvez pas poser un congé pour aujourd'hui.");
return false;
@@ -242,27 +286,12 @@ const NewLeaveRequestModal = ({
return false;
}
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');
return false;
}
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;
}
// Calculer les jours demandés par type
const joursDemandesParType = {};
if (formData.types.length === 1) {
@@ -283,7 +312,6 @@ const NewLeaveRequestModal = ({
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;
@@ -296,7 +324,6 @@ const NewLeaveRequestModal = ({
}
}
// Vérifier RTT
if (joursDemandesParType['RTT'] > 0) {
const rttDemande = joursDemandesParType['RTT'];
const rttDisponible = safeCounters.availableRTT;
@@ -309,7 +336,6 @@ const NewLeaveRequestModal = ({
}
}
// Vérifier Récup
if (joursDemandesParType['Récup'] > 0) {
const recupDemande = joursDemandesParType['Récup'];
const recupDisponible = safeCounters.availableRecup;
@@ -326,8 +352,6 @@ const NewLeaveRequestModal = ({
return true;
};
const handleSubmit = async () => {
setError('');
@@ -395,10 +419,6 @@ const NewLeaveRequestModal = ({
formDataToSend.append('Repartition', JSON.stringify(repartitionArray));
formData.medicalDocuments.forEach((file) => {
formDataToSend.append('medicalDocuments', file);
});
const response = await fetch('/api/submitLeaveRequest', {
method: 'POST',
body: formDataToSend
@@ -431,43 +451,15 @@ 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
// ⭐ Ajout de Formation en bleu pour les apprentis
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: 'ABS', label: 'Arrêt maladie' },
{ key: 'Formation', label: 'Formation' },
{
key: 'Récup',
@@ -479,14 +471,12 @@ const NewLeaveRequestModal = ({
{
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
},
@@ -494,11 +484,9 @@ const NewLeaveRequestModal = ({
key: 'Récup',
label: 'Récupération',
available: countersData?.data?.recupN?.solde || 0
},
{ key: 'ABS', label: 'Arrêt maladie' }
}
];
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
@@ -510,10 +498,6 @@ const NewLeaveRequestModal = ({
</div>
<div className="p-6 space-y-5">
{/* ⭐ BLOC SOLDES DÉTAILLÉS */}
{/* Loading */}
{isLoadingCounters && (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 text-center">
<div className="animate-spin h-6 w-6 border-2 border-blue-600 border-t-transparent rounded-full mx-auto mb-2"></div>
@@ -527,26 +511,21 @@ const NewLeaveRequestModal = ({
</label>
<div className="space-y-2">
{availableTypes.map(type => {
const disabled = isTypeDisabled(type.key);
const tooltip = getDisabledTooltip(type.key);
return (
<label
key={type.key}
className={`flex items-center gap-3 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
title={tooltip}
className="flex items-center gap-3 cursor-pointer"
>
<input
type="checkbox"
checked={formData.types.includes(type.key)}
onChange={() => handleTypeToggle(type.key)}
disabled={disabled}
className={`w-4 h-4 rounded border-gray-300 ${disabled ? 'cursor-not-allowed' : ''}`}
className="w-4 h-4 rounded border-gray-300"
/>
<span className={`px-2.5 py-1 rounded text-sm font-medium ${type.key === 'CP' ? 'bg-blue-100 text-blue-800' :
type.key === 'RTT' ? 'bg-green-100 text-green-800' :
type.key === 'ABS' ? 'bg-red-100 text-red-800' :
type.key === 'Récup' ? 'bg-orange-100 text-orange-800' :
type.key === 'Récup' ? 'bg-orange-100 text-orange-800' :
type.key === 'Formation' ? 'bg-blue-100 text-blue-800' :
'bg-purple-100 text-purple-800'
}`}>
{type.label}
@@ -570,17 +549,18 @@ const NewLeaveRequestModal = ({
);
})}
</div>
{formData.types.includes('ABS') && (
<div className="mt-3 flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<AlertCircle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
<p className="text-amber-700 text-xs">
Un arrêt maladie ne peut pas être combiné avec d'autres types de congés.
</p>
</div>
)}
</div>
{/* ⭐ AVERTISSEMENT FORMATION */}
{formData.types.includes('Formation') && (
<div className="flex items-start gap-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<AlertCircle className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<p className="text-blue-700 text-xs">
⚠️ La formation doit être posée au moins <strong>7 jours à l'avance</strong> (1 semaine minimum).
</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-900 mb-2">
@@ -589,10 +569,32 @@ const NewLeaveRequestModal = ({
<input
type="date"
value={formData.startDate}
min={getMinDate()}
onChange={(e) => setFormData(prev => ({ ...prev, startDate: e.target.value }))}
min={formData.types.includes('Formation') ? getMinDateFormation() : getMinDate()}
onChange={(e) => {
const newStartDate = e.target.value;
setFormData(prev => {
// Si c'est un weekend, ajuster automatiquement
const adjustedDate = isWeekend(newStartDate)
? getNextWorkingDay(newStartDate)
: newStartDate;
return {
...prev,
startDate: adjustedDate,
// Mettre à jour la date de fin si elle est vide ou antérieure
endDate: !prev.endDate || new Date(prev.endDate) < new Date(adjustedDate)
? adjustedDate
: prev.endDate
};
});
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{formData.startDate && isWeekend(formData.startDate) && (
<p className="text-xs text-orange-600 mt-1">
Les weekends ne sont pas comptabilisés
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-900 mb-2">
@@ -601,13 +603,52 @@ const NewLeaveRequestModal = ({
<input
type="date"
value={formData.endDate}
min={getMinDate()}
onChange={(e) => setFormData(prev => ({ ...prev, endDate: e.target.value }))}
min={formData.startDate || (formData.types.includes('Formation') ? getMinDateFormation() : getMinDate())}
onChange={(e) => {
const newEndDate = e.target.value;
// Si c'est un weekend, ajuster automatiquement
const adjustedDate = isWeekend(newEndDate)
? getNextWorkingDay(newEndDate)
: newEndDate;
setFormData(prev => ({
...prev,
endDate: adjustedDate
}));
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{formData.endDate && isWeekend(formData.endDate) && (
<p className="text-xs text-orange-600 mt-1">
Les weekends ne sont pas comptabilisés
</p>
)}
</div>
</div>
{/* ⭐ Affichage du récapitulatif avec jours fériés */}
{totalDays > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-blue-900">
Jours ouvrés : <span className="text-lg font-bold">{totalDays}</span>
</p>
{holidayCount > 0 && (
<p className="text-xs text-blue-700 mt-1">
🎉 {holidayCount} jour{holidayCount > 1 ? 's' : ''} férié{holidayCount > 1 ? 's' : ''} exclu{holidayCount > 1 ? 's' : ''}
</p>
)}
{saturdayCount > 0 && (
<p className="text-xs text-blue-700">
📅 {saturdayCount} samedi{saturdayCount > 1 ? 's' : ''} exclu{saturdayCount > 1 ? 's' : ''}
</p>
)}
</div>
</div>
</div>
)}
{formData.types.length === 1 && ['CP', 'RTT', 'Récup'].includes(formData.types[0]) && (
<div className="border-t border-gray-200 pt-4">
<h3 className="text-sm font-semibold text-gray-900 mb-2">
@@ -659,7 +700,7 @@ const NewLeaveRequestModal = ({
Répartition des {totalDays} jours ouvrés
</h3>
<p className="text-xs text-gray-500 mb-4">
Indiquez la répartition souhaitée (le système vérifiera automatiquement)
Indiquez la répartition souhaitée
</p>
<div className="space-y-3">
{formData.types.map((type) => {
@@ -751,76 +792,6 @@ const NewLeaveRequestModal = ({
/>
</div>
{formData.types.includes('ABS') && (
<div>
<label className="block text-sm font-medium text-gray-900 mb-3">
Justificatif médical <span className="text-red-600">*</span>
</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors">
<div className="w-12 h-12 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<Upload className="w-6 h-6 text-gray-400" />
</div>
<p className="text-gray-700 text-sm mb-2">
Glissez vos documents ici ou cliquez pour sélectionner
</p>
<p className="text-gray-500 text-xs mb-4">
Formats acceptés : PDF, JPG, PNG (max 5MB par fichier)
</p>
<input
type="file"
multiple
accept=".pdf,.jpg,.jpeg,.png"
onChange={handleFileUpload}
className="hidden"
id="medical-documents"
/>
<label
htmlFor="medical-documents"
className="inline-block px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors text-sm font-medium text-gray-700"
>
Sélectionner des fichiers
</label>
</div>
{formData.medicalDocuments.length > 0 && (
<div className="mt-4 space-y-2">
<p className="text-sm font-medium text-gray-900 mb-2">
Fichiers sélectionnés ({formData.medicalDocuments.length})
</p>
{formData.medicalDocuments.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-blue-100 rounded flex items-center justify-center flex-shrink-0">
{file.type === 'application/pdf' ? (
<svg className="w-4 h-4 text-red-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clipRule="evenodd" />
</svg>
) : (
<svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900 truncate">{file.name}</p>
<p className="text-xs text-gray-500">{formatFileSize(file.size)}</p>
</div>
</div>
<button
type="button"
onClick={() => removeDocument(index)}
className="text-gray-400 hover:text-red-600 ml-2 flex-shrink-0"
>
<X className="w-5 h-5" />
</button>
</div>
))}
</div>
)}
</div>
)}
{error && (
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
@@ -851,4 +822,4 @@ const NewLeaveRequestModal = ({
);
};
export default NewLeaveRequestModal;
export default NewLeaveRequestModal;

View File

@@ -1,22 +1,35 @@
import React from 'react';
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
const ProtectedRoute = ({ children }) => {
const { user, isLoading } = useAuth();
const ProtectedRoute = ({ children, allowedRoles = [] }) => {
const { isAuthorized, user, isLoading } = useAuth();
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Chargement...</p>
</div>
</div>
);
}
// ✅ FIX MOBILE : Attendre la fin du chargement avant de rediriger
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-cyan-600 mx-auto mb-4"></div>
<p className="text-gray-600 font-medium">Chargement en cours...</p>
</div>
</div>
);
}
return user ? children : <Navigate to="/login" replace />;
// ✅ Vérifier si l'utilisateur est autorisé
if (!isAuthorized || !user) {
console.log('❌ ProtectedRoute: Utilisateur non autorisé, redirection vers /login');
return <Navigate to="/login" replace />;
}
// ✅ Vérifier les rôles autorisés si spécifiés
if (allowedRoles.length > 0 && !allowedRoles.includes(user.role)) {
console.log(`❌ ProtectedRoute: Rôle ${user.role} non autorisé pour cette route`);
return <Navigate to="/dashboard" replace />;
}
return children;
};
export default ProtectedRoute;

View File

@@ -134,8 +134,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
data-tour="dashboard"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/dashboard")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Home className="w-5 h-5" />
@@ -147,8 +147,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
data-tour="demandes"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/demandes")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<FileText className="w-5 h-5" />
@@ -160,8 +160,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
data-tour="calendrier"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/calendrier")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Calendar className="w-5 h-5" />
@@ -175,12 +175,12 @@ const Sidebar = ({ isOpen, onToggle }) => {
data-tour="compte-rendu"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/compte-rendu-activites")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Clock className="w-5 h-5" />
<span className="font-medium">Compte-Rendu</span>
<span className="font-medium">CRA</span>
</Link>
)}
@@ -190,8 +190,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
data-tour="mon-equipe"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(teamPath)
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Users className="w-5 h-5" />

View File

@@ -1,7 +1,6 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import * as msal from '@azure/msal-browser';
// ✅ Correction: Import de API_BASE_URL
import { msalConfig, loginRequest, API_BASE_URL } from '../authConfig';
import { useMsal } from '@azure/msal-react';
import { loginRequest, API_BASE_URL } from '../authConfig';
const AuthContext = createContext();
@@ -11,19 +10,28 @@ export const useAuth = () => {
return context;
};
const msalInstance = new msal.PublicClientApplication(msalConfig);
// ✅ Détection mobile améliorée
const isMobileDevice = () => {
const ua = navigator.userAgent;
return /iPhone|iPad|iPod|Android|webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua);
};
const shouldUseRedirect = () => {
if (isMobileDevice()) {
return true;
}
return window.innerWidth < 768;
};
export const AuthProvider = ({ children }) => {
const { instance, accounts, inProgress } = useMsal();
const [user, setUser] = useState(null);
const [userGroups, setUserGroups] = useState([]);
const [isAuthorized, setIsAuthorized] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isMsalInitialized, setIsMsalInitialized] = useState(false);
// ✅ Fonction corrigée pour construire l'URL
const getApiUrl = (endpoint) => {
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
// API_BASE_URL est "/api", donc cela retourne "/api/endpoint"
return `${API_BASE_URL}/${cleanEndpoint}`;
};
@@ -66,11 +74,11 @@ export const AuthProvider = ({ children }) => {
if (response.ok) {
const data = await response.json();
console.log('Utilisateur synchronisé:', entraUser.userPrincipalName);
console.log('Utilisateur synchronisé:', entraUser.userPrincipalName);
return data;
}
} catch (error) {
console.error('Erreur synchronisation utilisateur:', error);
console.error('Erreur synchronisation utilisateur:', error);
}
return null;
};
@@ -85,63 +93,26 @@ export const AuthProvider = ({ children }) => {
if (response.ok) {
const data = await response.json();
console.log('Full sync terminée:', data);
console.log('Full sync terminée:', data);
return data;
}
} catch (error) {
console.error('Erreur full sync:', error);
console.error('Erreur full sync:', error);
}
return null;
};
// --- S'assurer que MSAL est initialisé avant tout appel
const ensureMsalInitialized = async () => {
if (!isMsalInitialized) {
try {
await msalInstance.initialize();
setIsMsalInitialized(true);
console.log('MSAL initialisé');
} catch (error) {
console.error('Erreur initialisation MSAL:', error);
throw error;
}
}
};
// --- Initialisation au chargement
useEffect(() => {
const initializeMsal = async () => {
try {
await ensureMsalInitialized();
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
try {
const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0]
});
await handleSuccessfulAuth(response);
} catch (error) {
console.log('Token silent acquisition failed:', error);
}
}
} catch (error) {
console.error("Erreur d'initialisation MSAL:", error);
} finally {
setIsLoading(false);
}
};
initializeMsal();
}, []);
// --- Gestion login réussi
const handleSuccessfulAuth = async (authResponse) => {
try {
console.log('🔐 Traitement authentification réussie...');
const account = authResponse.account;
const accessToken = authResponse.accessToken;
if (!account || !accessToken) {
throw new Error('Données d\'authentification incomplètes');
}
let entraUser = {
id: account.homeAccountId,
displayName: account.name,
@@ -149,29 +120,39 @@ export const AuthProvider = ({ children }) => {
mail: account.username
};
const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// Appel Graph API pour enrichir les données
console.log('📞 Appel Graph API...');
try {
const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (graphResponse.ok) {
const graphData = await graphResponse.json();
entraUser = { ...entraUser, ...graphData };
if (graphResponse.ok) {
const graphData = await graphResponse.json();
entraUser = { ...entraUser, ...graphData };
console.log('✅ Données Graph récupérées');
}
} catch (graphError) {
console.warn('⚠️ Erreur Graph API:', graphError);
}
// 1 Synchroniser lutilisateur connecté
// Synchronisation utilisateur
console.log('🔄 Synchronisation utilisateur...');
const syncResult = await syncUserToDatabase(entraUser, accessToken);
// 2⃣ Full sync si admin
if (syncResult?.role === 'Admin') {
console.log('Admin détecté → lancement full sync...');
console.log('👑 Admin détecté → lancement full sync...');
await fullSyncDatabase(accessToken);
}
// 3 Vérifier groupes
// Vérification des groupes
console.log('🔍 Vérification groupes...');
const authResult = await checkUserAuthorization(entraUser.userPrincipalName, accessToken);
if (authResult.authorized) {
setUser({
console.log('✅ Utilisateur autorisé');
const userData = {
id: syncResult?.localUserId || entraUser.id,
CollaborateurADId: syncResult?.localUserId,
entraUserId: entraUser.id,
@@ -188,17 +169,95 @@ export const AuthProvider = ({ children }) => {
typeContrat: syncResult?.typeContrat || '37h',
dateEntree: syncResult?.dateEntree || null,
groups: authResult.groups
});
};
setUser(userData);
setIsAuthorized(true);
console.log('✅ Connexion réussie:', userData.email);
} else {
console.error('❌ Utilisateur non autorisé');
throw new Error('Utilisateur non autorisé - pas membre des groupes requis');
}
} catch (error) {
console.error('Erreur lors de la gestion de l\'authentification:', error);
console.error('Erreur handleSuccessfulAuth:', error);
throw error;
} finally {
setIsLoading(false);
}
};
// ✅ SIMPLIFIÉ : L'initialisation MSAL est déjà faite dans main.jsx
useEffect(() => {
const processAuthentication = async () => {
// Attendre que MSAL finisse ses opérations en cours
if (inProgress !== 'none') {
console.log('⏳ MSAL inProgress:', inProgress);
return;
}
console.log('🌐 AuthContext - Vérification session');
console.log('📊 Comptes MSAL:', accounts.length);
// Si un compte existe, récupérer le token et traiter l'auth
if (accounts.length > 0) {
const account = accounts[0];
console.log('✅ Compte trouvé:', account.username);
try {
// Définir le compte actif
instance.setActiveAccount(account);
// Acquérir un token silencieusement
const tokenResponse = await instance.acquireTokenSilent({
...loginRequest,
account: account
});
console.log('✅ Token acquis silencieusement');
await handleSuccessfulAuth(tokenResponse);
} catch (error) {
console.error('❌ Erreur acquireTokenSilent:', error);
// Si interaction requise, relancer l'auth
if (error.name === 'InteractionRequiredAuthError' ||
error.errorCode === 'consent_required' ||
error.errorCode === 'interaction_required' ||
error.errorCode === 'login_required') {
console.log('🔄 Interaction requise, relancement...');
try {
if (shouldUseRedirect()) {
await instance.acquireTokenRedirect({
...loginRequest,
account: account
});
} else {
const response = await instance.acquireTokenPopup({
...loginRequest,
account: account
});
await handleSuccessfulAuth(response);
}
} catch (interactionError) {
console.error('❌ Erreur interaction:', interactionError);
setIsLoading(false);
}
} else {
setIsLoading(false);
}
}
} else {
// Pas de compte = utilisateur non connecté
console.log(' Aucun compte MSAL - utilisateur non connecté');
setIsLoading(false);
}
};
processAuthentication();
}, [instance, accounts, inProgress]);
// --- Connexion classique
const login = async (email, password) => {
try {
@@ -234,15 +293,28 @@ export const AuthProvider = ({ children }) => {
// --- Connexion Office 365
const loginWithO365 = async () => {
try {
await ensureMsalInitialized();
const authResponse = await msalInstance.loginPopup(loginRequest);
await handleSuccessfulAuth(authResponse);
return true;
} catch (error) {
console.error('Erreur login Office 365:', error);
if (error.message?.includes('non autorisé')) {
throw new Error('Accès refusé: Vous n\'êtes pas membre d\'un groupe autorisé.');
const useRedirect = shouldUseRedirect();
console.log(`🔐 Connexion O365: ${useRedirect ? 'REDIRECT' : 'POPUP'}`);
if (useRedirect) {
await instance.loginRedirect(loginRequest);
} else {
try {
const authResponse = await instance.loginPopup(loginRequest);
await handleSuccessfulAuth(authResponse);
return true;
} catch (popupError) {
if (popupError.errorCode === 'popup_window_error' ||
popupError.errorCode === 'empty_window_error') {
console.warn('⚠️ Popup bloqué, fallback redirect');
await instance.loginRedirect(loginRequest);
} else {
throw popupError;
}
}
}
} catch (error) {
console.error('❌ Erreur login O365:', error);
throw error;
}
};
@@ -250,12 +322,23 @@ export const AuthProvider = ({ children }) => {
// --- Déconnexion
const logout = async () => {
try {
const accounts = msalInstance.getAllAccounts();
const useRedirect = shouldUseRedirect();
if (accounts.length > 0) {
await msalInstance.logoutPopup({ account: accounts[0] });
if (useRedirect) {
await instance.logoutRedirect({
account: accounts[0],
postLogoutRedirectUri: window.location.origin
});
} else {
await instance.logoutPopup({
account: accounts[0],
postLogoutRedirectUri: window.location.origin
});
}
}
} catch (error) {
console.error('Erreur lors de la déconnexion:', error);
console.error('Erreur déconnexion:', error);
} finally {
setUser(null);
setUserGroups([]);
@@ -266,11 +349,11 @@ export const AuthProvider = ({ children }) => {
// --- Obtenir token API
const getAccessToken = async () => {
try {
await ensureMsalInitialized();
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) throw new Error('Aucun compte connecté');
if (accounts.length === 0) {
throw new Error('Aucun compte connecté');
}
const response = await msalInstance.acquireTokenSilent({
const response = await instance.acquireTokenSilent({
...loginRequest,
account: accounts[0]
});
@@ -278,6 +361,19 @@ export const AuthProvider = ({ children }) => {
return response.accessToken;
} catch (error) {
console.error('Erreur obtention token:', error);
// Tenter une interaction si nécessaire
if (error.name === 'InteractionRequiredAuthError') {
try {
const response = await instance.acquireTokenPopup({
...loginRequest,
account: accounts[0]
});
return response.accessToken;
} catch (popupError) {
console.error('Erreur popup token:', popupError);
}
}
return null;
}
};
@@ -296,4 +392,4 @@ export const AuthProvider = ({ children }) => {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export default AuthContext;
export default AuthContext;

View File

@@ -3,15 +3,121 @@ import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';
import { MsalProvider } from "@azure/msal-react";
import { PublicClientApplication } from "@azure/msal-browser";
import { PublicClientApplication, EventType } from "@azure/msal-browser";
import { msalConfig } from "./authConfig";
// ✅ CRITIQUE : Créer l'instance MSAL
const msalInstance = new PublicClientApplication(msalConfig);
createRoot(document.getElementById('root')).render(
<StrictMode>
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
</StrictMode>
);
// ✅ CRITIQUE : Fonction d'initialisation asynchrone
async function initializeApp() {
console.log('🚀 Initialisation de l\'application...');
console.log('🔗 Hash actuel:', window.location.hash);
console.log('📍 URL complète:', window.location.href);
// ✅ Sauvegarder le hash OAuth s'il existe (avant que quoi que ce soit ne le supprime)
const currentHash = window.location.hash;
if (currentHash && currentHash.includes('code=')) {
console.log('🚨 Hash OAuth détecté - Sauvegarde...');
sessionStorage.setItem('oauth_hash_backup', currentHash);
sessionStorage.setItem('oauth_url_backup', window.location.href);
sessionStorage.setItem('oauth_capture_time', Date.now().toString());
}
try {
// ✅ CRITIQUE : Initialiser MSAL (requis depuis MSAL 3.x)
console.log('⏳ Initialisation MSAL...');
await msalInstance.initialize();
console.log('✅ MSAL initialisé');
// ✅ CRITIQUE : Traiter la redirection OAuth AVANT le rendu React
console.log('⏳ Traitement handleRedirectPromise...');
const response = await msalInstance.handleRedirectPromise();
if (response) {
console.log('✅ Réponse OAuth reçue:', {
account: response.account?.username,
hasAccessToken: !!response.accessToken,
scopes: response.scopes
});
// Nettoyer le hash de l'URL après traitement réussi
window.history.replaceState({}, document.title, window.location.pathname);
// Nettoyer le backup
sessionStorage.removeItem('oauth_hash_backup');
sessionStorage.removeItem('oauth_url_backup');
sessionStorage.removeItem('oauth_capture_time');
} else {
console.log(' Pas de réponse OAuth (normal si pas de redirection en cours)');
// Vérifier s'il y avait un code mais pas de réponse (échec silencieux)
const backupHash = sessionStorage.getItem('oauth_hash_backup');
if (backupHash && backupHash.includes('code=')) {
const captureTime = sessionStorage.getItem('oauth_capture_time');
const elapsed = Date.now() - parseInt(captureTime || '0');
// Si le backup a moins de 30 secondes, c'est un échec récent
if (elapsed < 30000) {
console.warn('⚠️ Code OAuth détecté mais non traité par MSAL');
console.log('🔧 Le hash était:', backupHash.substring(0, 100) + '...');
}
// Nettoyer le backup après vérification
sessionStorage.removeItem('oauth_hash_backup');
sessionStorage.removeItem('oauth_url_backup');
sessionStorage.removeItem('oauth_capture_time');
}
}
// ✅ Configurer les événements MSAL pour le debug
msalInstance.addEventCallback((event) => {
if (event.eventType === EventType.LOGIN_SUCCESS) {
console.log('🎉 LOGIN_SUCCESS event:', event.payload?.account?.username);
}
if (event.eventType === EventType.LOGIN_FAILURE) {
console.error('❌ LOGIN_FAILURE event:', event.error);
}
if (event.eventType === EventType.ACQUIRE_TOKEN_SUCCESS) {
console.log('🔑 Token acquis pour:', event.payload?.account?.username);
}
if (event.eventType === EventType.HANDLE_REDIRECT_END) {
console.log('🏁 HANDLE_REDIRECT_END');
}
});
// ✅ Définir le compte actif si disponible
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
console.log('📊 Comptes MSAL trouvés:', accounts.length);
msalInstance.setActiveAccount(accounts[0]);
console.log('✅ Compte actif défini:', accounts[0].username);
}
} catch (error) {
console.error('❌ Erreur lors de l\'initialisation MSAL:', error);
// En cas d'erreur, nettoyer et continuer
sessionStorage.removeItem('oauth_hash_backup');
sessionStorage.removeItem('oauth_url_backup');
sessionStorage.removeItem('oauth_capture_time');
// Nettoyer l'URL si elle contient encore le code
if (window.location.hash.includes('code=')) {
window.history.replaceState({}, document.title, window.location.pathname);
}
}
// ✅ Rendre l'application React APRÈS l'initialisation MSAL
console.log('🎨 Rendu de l\'application React...');
createRoot(document.getElementById('root')).render(
<StrictMode>
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
</StrictMode>
);
}
// ✅ Lancer l'initialisation
initializeApp();

View File

@@ -51,6 +51,9 @@ const Calendar = () => {
const [initialFiltersSet, setInitialFiltersSet] = useState(false);
// ⭐ Liste des employés à exclure de l'affichage
const EXCLUDED_EMPLOYEES = ['Kevin Lambert'];
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024);
@@ -66,7 +69,7 @@ const Calendar = () => {
];
const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
const dayNamesMobile = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
const dayNamesMobile = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
useEffect(() => {
if (accounts.length > 0) {
@@ -152,9 +155,9 @@ const Calendar = () => {
if (role === 'president' || role === 'rh' || role === 'admin' ||
role === 'directeur de campus' || role === 'directrice de campus' ||
role === 'collaborateur' || role === 'collaboratrice' || role === 'apprenti') {
role === 'collaborateur' || role === 'collaboratrice' || role === 'apprenti' ||
role === 'validateur' || role === 'validatrice') {
// ⭐ TOUJOURS envoyer les paramètres
url += `&selectedSociete=${encodeURIComponent(selectedSociete || 'all')}`;
url += `&selectedCampus=${encodeURIComponent(selectedCampus || 'all')}`;
url += `&selectedService=${encodeURIComponent(selectedService || 'all')}`;
@@ -171,8 +174,21 @@ const Calendar = () => {
});
if (data.success) {
setTeamLeaves(data.leaves || []);
setFilters(data.filters || {});
// ⭐ Filtrer les congés des employés exclus
const filteredLeaves = (data.leaves || []).filter(leave =>
!EXCLUDED_EMPLOYEES.includes(leave.employeename)
);
setTeamLeaves(filteredLeaves);
// ⭐ Filtrer les employés exclus des filtres
const filteredFilters = { ...data.filters };
if (filteredFilters.employees) {
filteredFilters.employees = filteredFilters.employees.filter(emp => {
const empName = typeof emp === 'string' ? emp : emp.name;
return !EXCLUDED_EMPLOYEES.includes(empName);
});
}
setFilters(filteredFilters);
}
} catch (error) {
console.error('Erreur récupération congés équipe:', error);
@@ -279,7 +295,6 @@ const Calendar = () => {
}
}, [filters.defaultCampus, isFirstLoad, role]);
// ⭐ Initialisation des filtres par défaut pour collaborateur/apprenti (UNE SEULE FOIS)
useEffect(() => {
if (!initialFiltersSet &&
filters.defaultCampus &&
@@ -300,11 +315,32 @@ const Calendar = () => {
}
}, [filters.defaultCampus, filters.defaultService, filters.defaultSociete, initialFiltersSet, role]);
// ⭐ Rechargement quand les filtres changent (TOUJOURS)
useEffect(() => {
if (role === 'president' || role === 'rh' || role === 'admin' ||
if (!initialFiltersSet &&
filters.defaultCampus &&
(role === 'validateur' || role === 'validatrice')) {
console.log('🎯 Initialisation des filtres par défaut pour validateur');
console.log('📍 Valeurs reçues du backend:', {
defaultSociete: filters.defaultSociete,
defaultCampus: filters.defaultCampus,
defaultService: filters.defaultService
});
setSelectedSociete(filters.defaultSociete || 'all');
setSelectedCampus(filters.defaultCampus || 'all');
setSelectedService(filters.defaultService || 'all');
setInitialFiltersSet(true);
}
}, [filters.defaultCampus, filters.defaultService, filters.defaultSociete, initialFiltersSet, role]);
useEffect(() => {
// ⭐ Exclure "president" de la liste des rôles qui déclenchent le rechargement
if (role === 'rh' || role === 'admin' ||
role === 'directeur de campus' || role === 'directrice de campus' ||
role === 'collaborateur' || role === 'collaboratrice' || role === 'apprenti') {
role === 'collaborateur' || role === 'collaboratrice' || role === 'apprenti' ||
role === 'validateur' || role === 'validatrice') {
console.log("🔄 Rechargement données:", {
societe: selectedSociete,
@@ -357,22 +393,16 @@ const Calendar = () => {
for (let day = 1; day <= daysInMonth; day++) {
const currentDay = new Date(year, month, day);
const dayOfWeek = currentDay.getDay();
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
days.push(currentDay);
}
days.push(currentDay);
}
return days;
};
// ⭐ SIMPLIFIÉ : Le backend filtre déjà
const getBaseFilteredLeaves = () => {
return teamLeaves;
};
// ⭐ SIMPLIFIÉ : Pas de cas spécial pour collaborateur
const getAllEmployees = () => {
if (!filters.employees || filters.employees.length === 0) {
return [];
@@ -385,12 +415,12 @@ const Calendar = () => {
service: emp.service || ''
}));
// Dédoublonner
const uniqueEmployees = [];
const seenNames = new Set();
for (const emp of employeeList) {
if (!seenNames.has(emp.name)) {
// ⭐ Exclure les employés de la liste noire
if (!seenNames.has(emp.name) && !EXCLUDED_EMPLOYEES.includes(emp.name)) {
seenNames.add(emp.name);
uniqueEmployees.push(emp);
}
@@ -408,15 +438,16 @@ const Calendar = () => {
filteredEmployees = allEmployees.filter(emp => selectedEmployees.includes(emp.name));
}
// ⭐ Exclure "president" de la condition de tri automatique
const shouldAutoSort = (
['president', 'rh', 'admin', 'directeur de campus', 'directrice de campus'].includes(role) &&
['rh', 'admin', 'directeur de campus', 'directrice de campus'].includes(role) &&
selectedCampus === 'all' &&
selectedSociete === 'all' &&
selectedService === 'all'
);
const shouldSortByService = (
['collaborateur', 'collaboratrice', 'apprenti'].includes(role) &&
['collaborateur', 'collaboratrice', 'apprenti', 'validateur', 'validatrice'].includes(role) &&
selectedService === 'all'
);
@@ -478,7 +509,7 @@ const Calendar = () => {
}
if (shouldSortByService) {
console.log("🔄 Tri par service pour collaborateur");
console.log("🔄 Tri par service pour collaborateur/validateur");
return filteredEmployees.sort((a, b) => {
const serviceCompare = (a.service || '').localeCompare(b.service || '');
if (serviceCompare !== 0) return serviceCompare;
@@ -497,7 +528,6 @@ const Calendar = () => {
return baseFiltered.filter(leave => displayedNames.includes(leave.employeename));
};
// ⭐ useMemo simplifié
const allEmployeesData = useMemo(() => {
return getAllEmployees();
}, [filters.employees]);
@@ -571,37 +601,74 @@ const Calendar = () => {
});
};
const getLeaveColor = (leave) => {
// 🆕 FONCTION CORRIGÉE : RÉCUP SUR WEEKEND = MAGENTA
const getLeaveColor = (leave, date = null) => {
if (!leave) return { tailwindClass: '', hexColor: '' };
const status = leave.statut?.toLowerCase();
const type = leave.type?.toLowerCase();
// Détection si saisie par RH
const saisieParRH = leave.createdbyrole?.toLowerCase() === 'rh' || leave.saisieParRH === true;
// Détection des types
const isRecup = type?.includes('récup') ||
type?.includes('recup') ||
type?.includes('récupération') ||
type?.includes('recuperation');
const isJPOSF = type?.includes('jpo') ||
type?.includes('sf') ||
type?.includes('jpocpf') ||
type?.includes('journée portes ouvertes') ||
type?.includes('journee portes ouvertes') ||
type?.includes('salon formation');
// Détection weekend
const isWeekend = date && (date.getDay() === 6 || date.getDay() === 0);
// 🔴 PRIORITÉ 1 : RÉCUP SUR SAMEDI/DIMANCHE = MAGENTA (#d946ef)
if (isRecup && isWeekend) {
return { tailwindClass: '', hexColor: '#d946ef' };
}
// 🔴 PRIORITÉ 2 : JPO/SF SAISIE PAR RH = MAGENTA (#d946ef) - TOUJOURS
if (saisieParRH && isJPOSF) {
return { tailwindClass: '', hexColor: '#d946ef' };
}
// 🟠 PRIORITÉ 3 : Statut "en attente" = ORANGE (pour TOUS les types)
if (status === 'en attente' || status === 'pending' || status === 'en attente de validation') {
return { tailwindClass: 'bg-orange-400', hexColor: '#fb923c' };
}
if (type) {
if (type.toLowerCase().includes('récupération') ||
type.toLowerCase().includes('recuperation') ||
type.toLowerCase().includes('recup') ||
type.toLowerCase().includes('récup')) {
return { tailwindClass: '', hexColor: '#d946ef' };
}
// 🟢 PRIORITÉ 4 : Si validé, on applique les couleurs selon le type
if (type.includes('formation')) {
return { tailwindClass: 'bg-blue-400', hexColor: '#60a5fa' };
}
if (type.includes('cp') || type.includes('congé') || type.includes('conge') || type.includes('payé') || type.includes('paye')) {
return { tailwindClass: 'bg-green-400', hexColor: '#4ade80' };
}
if (type.includes('rtt')) {
return { tailwindClass: 'bg-blue-300', hexColor: '#93c5fd' };
}
// JPO/SF NORMAL (non RH) = MAGENTA aussi
if (isJPOSF) {
return { tailwindClass: '', hexColor: '#d946ef' };
}
// Récup (peu importe qui l'a saisie, si validée) = VERT
if (isRecup) {
return { tailwindClass: 'bg-green-400', hexColor: '#4ade80' };
}
// Formation = BLEU FONCÉ (#60a5fa)
if (type?.includes('formation')) {
return { tailwindClass: 'bg-blue-400', hexColor: '#60a5fa' };
}
// CP/Congé validé = VERT (#4ade80)
if (type?.includes('cp') ||
type?.includes('congé') ||
type?.includes('conge') ||
type?.includes('payé') ||
type?.includes('paye')) {
return { tailwindClass: 'bg-green-400', hexColor: '#4ade80' };
}
// Par défaut : VERT (congé validé sans type spécifique)
return { tailwindClass: 'bg-green-400', hexColor: '#4ade80' };
};
@@ -772,8 +839,9 @@ const Calendar = () => {
? (detailedCounters.cpN?.solde || 0) + (detailedCounters.cpN1?.solde || 0) + (detailedCounters.rttN?.solde || 0)
: 0;
const canViewAllFilters = ['president', 'rh', 'admin', 'directeur de campus', 'directrice de campus', 'collaborateur', 'collaboratrice', 'apprenti'].includes(role);
const canViewCampusFilters = ['president', 'rh', 'admin', 'directeur de campus', 'directrice de campus'].includes(role);
// ⭐ Exclure "president" de la liste des rôles autorisés pour les filtres
const canViewAllFilters = ['rh', 'admin', 'directeur de campus', 'directrice de campus', 'validateur', 'validatrice', 'collaborateur', 'collaboratrice', 'apprenti'].includes(role);
const canViewCampusFilters = ['rh', 'admin', 'directeur de campus', 'directrice de campus', 'validateur', 'validatrice', 'collaborateur', 'collaboratrice', 'apprenti'].includes(role);
const activeFiltersCount = [
employeeFilter !== "all" ? employeeFilter : null,
@@ -784,7 +852,7 @@ const Calendar = () => {
].filter(Boolean).length;
const resetToDefaultFilters = () => {
if (['collaborateur', 'collaboratrice', 'apprenti'].includes(role)) {
if (['collaborateur', 'collaboratrice', 'apprenti', 'validateur', 'validatrice'].includes(role)) {
const userEmployee = filters.employees?.find(emp =>
emp.name === `${user.prenom} ${user.nom}`
);
@@ -802,10 +870,10 @@ const Calendar = () => {
setSelectedEmployees([]);
};
const renderLeaveCell = (leave) => {
const renderLeaveCell = (leave, date = null) => {
if (!leave) return null;
const colorObj = getLeaveColor(leave);
const colorObj = getLeaveColor(leave, date);
const bgColor = colorObj.hexColor || '#4ade80';
let details;
@@ -854,6 +922,26 @@ const Calendar = () => {
);
};
const renderMobileLeaveCell = (leave, date) => {
if (!leave) {
if (isHoliday(date)) {
return <div className="w-2 h-2 bg-gray-700 rounded-full mx-auto"></div>;
}
if (isSaturday(date) || isSunday(date)) {
return <div className="w-2 h-2 bg-gray-300 rounded-full mx-auto"></div>;
}
return null;
}
const colorObj = getLeaveColor(leave, date);
return (
<div
className="w-2 h-2 rounded-full mx-auto"
style={{ backgroundColor: colorObj.hexColor || '#4ade80' }}
></div>
);
};
return (
<div className="flex h-screen bg-gray-50" onMouseMove={handleMouseMove}>
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
@@ -954,7 +1042,7 @@ const Calendar = () => {
onClick={resetToDefaultFilters}
className="text-sm text-blue-600 hover:text-blue-700"
>
{['collaborateur', 'collaboratrice', 'apprenti'].includes(role)
{['collaborateur', 'collaboratrice', 'apprenti', 'validateur', 'validatrice'].includes(role)
? 'Réinitialiser aux valeurs par défaut'
: 'Réinitialiser les filtres'
}
@@ -999,7 +1087,7 @@ const Calendar = () => {
)}
{/* Campus filter */}
{filters.campus && filters.campus.length > 0 && (
{canViewCampusFilters && filters.campus && filters.campus.length > 0 && (
<div data-tour="filtre-campus">
<label className="block text-sm font-medium text-gray-700 mb-2">
Campus
@@ -1127,7 +1215,7 @@ const Calendar = () => {
{canViewCampusFilters && employee.societe && (
<> {employee.societe}</>
)}
{(role === 'president' || role === 'rh' || role === 'admin') && employee.campus && (
{(role === 'rh' || role === 'admin') && employee.campus && (
<> {employee.campus}</>
)}
{(role === 'directeur de campus' || role === 'directrice de campus') && employee.societe === 'Ensup Solution & Support' && employee.campus !== 'N/A' && (
@@ -1147,7 +1235,7 @@ const Calendar = () => {
)}
</div>
{/* Calendar */}
{/* CALENDRIER DESKTOP */}
{!isMobile && (
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
<div className="overflow-x-auto">
@@ -1243,7 +1331,7 @@ const Calendar = () => {
{canViewCampusFilters && employee.societe && (
<> {employee.societe}</>
)}
{(role === 'president' || role === 'rh' || role === 'admin') && employee.campus && (
{(role === 'rh' || role === 'admin') && employee.campus && (
<> {employee.campus}</>
)}
{(role === 'directeur de campus' || role === 'directrice de campus') && employee.societe === 'Ensup Solution & Support' && employee.campus !== 'N/A' && (
@@ -1259,18 +1347,26 @@ const Calendar = () => {
const saturday = isSaturday(date);
const sunday = isSunday(date);
const isToday = date.toDateString() === new Date().toDateString();
const weekendAlreadyHasLeave = (saturday || sunday) && leave;
const isWeekend = saturday || sunday;
// 🆕 Détection récup sur weekend
const isRecupWeekend = leave && isWeekend && (
leave.type?.toLowerCase().includes('récup') ||
leave.type?.toLowerCase().includes('recup') ||
leave.type?.toLowerCase().includes('récupération') ||
leave.type?.toLowerCase().includes('recuperation')
);
let details, hasMatin = false, hasApresMidi = false;
if (weekendAlreadyHasLeave) {
if (leave) {
try {
if (typeof leave.detailsconges === 'string') {
details = JSON.parse(leave.detailsconges);
} else if (Array.isArray(leave.detailsconges)) {
details = leave.detailsconges;
}
hasMatin = details.some(d => d.periode === 'Matin');
hasApresMidi = details.some(d => d.periode === 'Après-midi');
hasMatin = details?.some(d => d.periode === 'Matin');
hasApresMidi = details?.some(d => d.periode === 'Après-midi');
} catch (e) {
console.error('Erreur parsing:', e);
}
@@ -1283,60 +1379,69 @@ const Calendar = () => {
inRange ? 'bg-blue-100' :
past ? 'bg-gray-50 opacity-60' :
holiday ? 'bg-gray-600' :
(sunday || saturday) && !weekendAlreadyHasLeave ? 'bg-gray-200' :
weekendAlreadyHasLeave ? 'cursor-not-allowed' :
''
isWeekend && !isRecupWeekend ? 'bg-gray-200' : ''
} ${leave || (employee.name === `${user.prenom} ${user.nom}` && !past && !holiday && !isWeekend)
? ''
: 'cursor-not-allowed'
}`}
onClick={() => {
if (!leave && employee.name === `${user.prenom} ${user.nom}` && !past && !holiday && !sunday && !saturday) {
if (!leave && employee.name === `${user.prenom} ${user.nom}` && !past && !holiday && !isWeekend) {
handleDateClick(date);
}
}}
onContextMenu={(e) => handleContextMenu(e, date)}
onContextMenu={(e) => {
if (employee.name === `${user.prenom} ${user.nom}` && !past && !holiday && !isWeekend) {
handleContextMenu(e, date);
}
}}
onMouseEnter={() => !isMobile && leave && setHoveredLeave({ employee, leave, date })}
onMouseLeave={() => setHoveredLeave(null)}
title={
weekendAlreadyHasLeave
? `${saturday ? 'Samedi' : 'Dimanche'} saisi : ${leave?.type}${hasMatin ? ' - Matin' : ''}${hasApresMidi ? ' - Après-midi' : ''} - Non modifiable`
: isHoliday(date)
? getHolidayName(date)
: sunday
? 'Dimanche - Non sélectionnable'
: saturday
? 'Samedi - Non sélectionnable'
: ''
isRecupWeekend ? `${saturday ? 'Samedi' : 'Dimanche'} - Récupération` :
isHoliday(date) ? getHolidayName(date) :
sunday ? 'Dimanche - Non sélectionnable' :
saturday ? 'Samedi - Non sélectionnable' :
''
}
>
{/* JOUR FÉRIÉ */}
{holiday ? (
<div className="h-6 w-6 mx-auto bg-gray-700 rounded" title={getHolidayName(date)}></div>
) : sunday ? (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-not-allowed">
{weekendAlreadyHasLeave ? (
<>
{hasMatin && <div className="absolute top-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>}
{hasApresMidi && <div className="absolute bottom-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>}
{!hasMatin && !hasApresMidi && <div className="h-full w-full" style={{ backgroundColor: '#d946ef' }}></div>}
</>
) : (
<div className="h-full w-full" style={{ backgroundColor: '#d1d5db' }}></div>
/* 🆕 RÉCUP SUR WEEKEND → MAGENTA */
) : isRecupWeekend ? (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden">
{hasMatin && (
<div className="absolute top-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>
)}
{hasApresMidi && (
<div className="absolute bottom-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>
)}
{!hasMatin && !hasApresMidi && (
<div className="h-full w-full" style={{ backgroundColor: '#d946ef' }}></div>
)}
</div>
) : saturday ? (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-not-allowed">
{weekendAlreadyHasLeave ? (
<>
{hasMatin && <div className="absolute top-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>}
{hasApresMidi && <div className="absolute bottom-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>}
{!hasMatin && !hasApresMidi && <div className="h-full w-full" style={{ backgroundColor: '#d946ef' }}></div>}
</>
) : (
<div className="h-full w-full" style={{ backgroundColor: '#d1d5db' }}></div>
)}
/* WEEKEND NORMAL SANS CONGÉ → GRIS */
) : isWeekend && !leave ? (
<div className="h-6 w-6 mx-auto bg-gray-300 rounded"></div>
/* WEEKEND AVEC AUTRE CONGÉ → GRIS AUSSI */
) : isWeekend && leave ? (
<div className="h-6 w-6 mx-auto bg-gray-300 rounded"
title={`${saturday ? 'Samedi' : 'Dimanche'} - Congé non modifiable`}>
</div>
/* CONGÉ NORMAL EN SEMAINE */
) : leave ? (
renderLeaveCell(leave)
renderLeaveCell(leave, date)
/* CELLULE VIDE CLIQUABLE */
) : employee.name === `${user.prenom} ${user.nom}` && !past && !holiday ? (
<div className={`h-6 w-6 mx-auto rounded cursor-pointer transition-all ${inRange ? 'bg-blue-500 scale-110' : 'bg-gray-100 hover:bg-blue-200 hover:scale-105'}`}></div>
<div className={`h-6 w-6 mx-auto rounded cursor-pointer transition-all ${inRange ? 'bg-blue-500 scale-110' : 'bg-gray-100 hover:bg-blue-200 hover:scale-105'
}`}></div>
/* CELLULE VIDE NON CLIQUABLE */
) : (
<div className="h-6 w-6 mx-auto bg-gray-100 rounded"></div>
)}
@@ -1371,7 +1476,7 @@ const Calendar = () => {
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gray-400 rounded"></div>
<span className="text-gray-600">Samedi/Dimanche </span>
<span className="text-gray-600">Samedi/Dimanche</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gray-700 rounded"></div>
@@ -1385,7 +1490,129 @@ const Calendar = () => {
</div>
)}
{/* Mobile calendar - TO BE IMPLEMENTED IF NEEDED */}
{/* CALENDRIER MOBILE */}
{isMobile && (
<div className="bg-white rounded-lg border shadow-sm">
{/* Navigation mois mobile */}
<div className="flex items-center justify-between py-3 px-4 border-b bg-gray-50">
<button onClick={() => navigateMonth('prev')} className="p-2 hover:bg-gray-200 rounded">
<ChevronLeft className="w-5 h-5 text-blue-600" />
</button>
<span className="text-base font-semibold">
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
</span>
<button onClick={() => navigateMonth('next')} className="p-2 hover:bg-gray-200 rounded">
<ChevronRight className="w-5 h-5 text-blue-600" />
</button>
</div>
{/* Grille calendrier mobile */}
<div className="p-3">
{/* En-têtes jours */}
<div className="grid grid-cols-7 gap-1 mb-2">
{dayNamesMobile.map((day, idx) => (
<div key={idx} className="text-center text-xs font-medium text-gray-500 py-1">
{day}
</div>
))}
</div>
{/* Jours du mois */}
<div className="grid grid-cols-7 gap-1">
{days.map((date, idx) => {
if (!date) return <div key={idx} className="h-10"></div>;
const isToday = date.toDateString() === new Date().toDateString();
const past = isPastDate(date);
const holiday = isHoliday(date);
const saturday = isSaturday(date);
const sunday = isSunday(date);
const hasLeaveToday = hasLeave(date);
return (
<div
key={idx}
onClick={() => {
if (!past && !holiday && !sunday && !saturday) {
handleDateClick(date);
}
}}
className={`h-10 flex flex-col items-center justify-center rounded-lg text-sm cursor-pointer transition-colors ${isToday ? 'bg-cyan-100 font-bold' :
holiday ? 'bg-gray-600 text-white' :
(sunday || saturday) ? 'bg-gray-200 text-gray-400' :
past ? 'text-gray-300' :
isSelected(date) ? 'bg-blue-500 text-white' :
'hover:bg-gray-100'
}`}
>
<span>{date.getDate()}</span>
{hasLeaveToday && !holiday && (
<div className="w-1.5 h-1.5 bg-green-500 rounded-full mt-0.5"></div>
)}
</div>
);
})}
</div>
</div>
{/* Liste des congés mobile */}
<div className="border-t p-3">
<h3 className="text-sm font-semibold text-gray-700 mb-3">
Congés du mois ({teamLeaves.length})
</h3>
<div className="space-y-2 max-h-60 overflow-y-auto">
{teamLeaves.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">
Aucun congé ce mois
</p>
) : (
teamLeaves.slice(0, 10).map((leave, idx) => {
const colorObj = getLeaveColor(leave, parseLocalDate(leave.startdate));
return (
<div key={idx} className="flex items-center gap-3 p-2 bg-gray-50 rounded-lg">
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: colorObj.hexColor }}
></div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">
{leave.employeename}
</p>
<p className="text-xs text-gray-500">
{leave.startdate} - {leave.enddate}
</p>
</div>
</div>
);
})
)}
</div>
</div>
{/* Légende mobile */}
<div className="border-t p-3">
<div className="flex flex-wrap gap-3 text-xs">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
<span>Validé</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-orange-400 rounded-full"></div>
<span>En attente</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
<span>Formation</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: '#d946ef' }}></div>
<span>JPO/SF</span>
</div>
</div>
</div>
</div>
)}
</div>
</div>
@@ -1439,7 +1666,7 @@ const Calendar = () => {
</div>
)}
{/* Hover tooltip */}
{/* Tooltip (désactivé, ne montre plus le type) */}
{hoveredLeave && !isMobile && (
<div
className="fixed bg-white rounded-lg shadow-xl border p-4 z-50 min-w-[250px]"
@@ -1448,9 +1675,7 @@ const Calendar = () => {
top: mousePosition.y + 10,
}}
>
{/* Tooltip content - TO BE COMPLETED */}
<div className="text-sm font-semibold">{hoveredLeave.employee.name}</div>
<div className="text-xs text-gray-500 mt-1">{hoveredLeave.leave.type}</div>
</div>
)}
@@ -1523,4 +1748,4 @@ const Calendar = () => {
);
};
export default Calendar;
export default Calendar;

View File

@@ -6,7 +6,7 @@ import { Users, CheckCircle, XCircle, Clock, Calendar, FileText, Menu, Eye, Mess
const Collaborateur = () => {
const { user } = useAuth();
const [sidebarOpen, setSidebarOpen] = useState(false);
const isEmployee = user?.role === 'Collaborateur'||'Apprenti';
const isEmployee = user?.role === 'Collaborateur' || 'Apprenti';
const [teamMembers, setTeamMembers] = useState([]);
const [pendingRequests, setPendingRequests] = useState([]);
const [allRequests, setAllRequests] = useState([]);
@@ -191,7 +191,7 @@ const Collaborateur = () => {
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
{isEmployee ? 'Mon équipe 👥' : 'Gestion d\'équipe 👥'}
</h1>
</div>
{/* Stats Cards */}
@@ -222,9 +222,9 @@ const Collaborateur = () => {
</div>
</div>
</div>
{/* Main Content */}

View File

@@ -110,7 +110,15 @@ const CompteRenduActivites = () => {
const congesData = await congesResponse.json();
if (congesData.success) {
setCongesData(congesData.leaves || []);
// ⭐ FILTRE : Ne garder que les congés de l'utilisateur actuel
const mesConges = (congesData.leaves || []).filter(leave => {
const employeeName = leave.employeename?.toLowerCase() || '';
const userName = `${user.prenom} ${user.nom}`.toLowerCase();
return employeeName === userName;
});
console.log('🏖️ Congés détectés pour', `${user.prenom} ${user.nom}:`, mesConges.length);
setCongesData(mesConges);
}
} catch (error) {
@@ -118,7 +126,7 @@ const CompteRenduActivites = () => {
} finally {
setIsLoading(false);
}
}, [userId, annee, mois, user?.role]);
}, [userId, annee, mois, user?.role, user?.prenom, user?.nom]);
// Charger les stats annuelles
const loadStatsAnnuelles = useCallback(async () => {
@@ -163,7 +171,7 @@ const CompteRenduActivites = () => {
return selectedYear === previousYear && selectedMonth === previousMonth;
};
// Générer les jours du mois (lundi-samedi) avec décalage correct
// Générer les jours du mois (7 colonnes : Lun-Dim)
const getDaysInMonth = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
@@ -174,7 +182,7 @@ const CompteRenduActivites = () => {
// Jour de la semaine du 1er (0=dimanche, 1=lundi, ..., 6=samedi)
let firstDayOfWeek = firstDay.getDay();
// Convertir pour que lundi = 0, mardi = 1, ..., samedi = 5, dimanche = 6
// Convertir pour que lundi = 0
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
const days = [];
@@ -184,15 +192,9 @@ const CompteRenduActivites = () => {
days.push(null);
}
// Ajouter tous les jours du mois (lundi-samedi uniquement)
// Ajouter tous les jours du mois
for (let day = 1; day <= daysInMonth; day++) {
const currentDay = new Date(year, month, day);
const dayOfWeek = currentDay.getDay();
// Exclure les dimanches (0)
if (dayOfWeek !== 0) {
days.push(currentDay);
}
days.push(new Date(year, month, day));
}
return days;
@@ -201,31 +203,47 @@ const CompteRenduActivites = () => {
const getJourData = (date) => {
const dateStr = formatDateToString(date);
const found = joursActifs.find(j => {
// Normaliser la date de la BDD (peut être un objet Date ou une string)
// Normaliser la date de la BDD
let jourDateStr = j.JourDate;
if (j.JourDate instanceof Date) {
jourDateStr = formatDateToString(j.JourDate);
} else if (typeof j.JourDate === 'string') {
// Si c'est déjà une string, extraire juste la partie date (YYYY-MM-DD)
jourDateStr = j.JourDate.split('T')[0];
}
const match = jourDateStr === dateStr;
console.log('Comparaison:', jourDateStr, 'vs', dateStr, 'match:', match);
return match;
});
if (found) {
console.log('✅ Jour trouvé:', dateStr, found);
}
return found;
};
// ⭐ Détection des congés avec logs
const isJourEnConge = (date) => {
return congesData.some(conge => {
if (!date) return false;
const checkDate = new Date(date);
checkDate.setHours(0, 0, 0, 0);
const enConge = congesData.some(conge => {
const start = new Date(conge.startdate);
const end = new Date(conge.enddate);
return date >= start && date <= end && conge.statut === 'Valide';
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
const isInRange = checkDate >= start && checkDate <= end;
const isValide = conge.statut === 'Validée' || conge.statut === 'Validé' || conge.statut === 'Valide';
return isInRange && isValide;
});
return enConge;
};
// ⭐ Détection week-end
const isWeekend = (date) => {
if (!date) return false;
const dayOfWeek = date.getDay();
return dayOfWeek === 0 || dayOfWeek === 6; // Dimanche ou Samedi
};
// Vérifier si le jour est STRICTEMENT dans le passé (pas aujourd'hui)
@@ -263,11 +281,18 @@ const CompteRenduActivites = () => {
return;
}
// ⭐ Bloquer les week-ends
if (isWeekend(date)) {
showInfo('Les samedis et dimanches ne peuvent pas être saisis', 'info');
return;
}
if (isHoliday(date)) {
showInfo(`Jour férié : ${getHolidayName(date)} - Saisie impossible`, 'info');
return;
}
// ⭐ Bloquer les congés
if (isJourEnConge(date)) {
showInfo('Vous êtes en congé ce jour - Saisie impossible', 'info');
return;
@@ -292,10 +317,25 @@ const CompteRenduActivites = () => {
// Sauvegarder un jour
const handleSaveJour = async () => {
if ((!selectedJour.reposQuotidien || !selectedJour.reposHebdo)) {
if (!selectedJour.commentaire || selectedJour.commentaire.trim() === '') {
showInfo('Commentaire obligatoire en cas de non-respect des repos', 'warning');
return;
// ⭐ Vérifier uniquement le repos quotidien pour lundi-jeudi
// Pour vendredi, vérifier les deux
const isVendredi = selectedJour.date.getDay() === 5;
if (isVendredi) {
// Vendredi : vérifier repos quotidien ET hebdomadaire
if (!selectedJour.reposQuotidien || !selectedJour.reposHebdo) {
if (!selectedJour.commentaire || selectedJour.commentaire.trim() === '') {
showInfo('Commentaire obligatoire en cas de non-respect des repos', 'warning');
return;
}
}
} else {
// Lundi-Jeudi : vérifier uniquement repos quotidien
if (!selectedJour.reposQuotidien) {
if (!selectedJour.commentaire || selectedJour.commentaire.trim() === '') {
showInfo('Commentaire obligatoire en cas de non-respect du repos quotidien', 'warning');
return;
}
}
}
@@ -310,7 +350,7 @@ const CompteRenduActivites = () => {
date: selectedJour.dateStr,
jour_travaille: selectedJour.jourTravaille,
repos_quotidien: selectedJour.reposQuotidien,
repos_hebdo: selectedJour.reposHebdo,
repos_hebdo: isVendredi ? selectedJour.reposHebdo : true, // true par défaut pour lundi-jeudi
commentaire: selectedJour.commentaire,
rh_override: isRH
})
@@ -323,7 +363,6 @@ const CompteRenduActivites = () => {
await loadStatsAnnuelles();
setShowSaisieModal(false);
showInfo('✅ Jour enregistré', 'success');
console.log('Données rechargées après sauvegarde');
} else {
showInfo(data.message || 'Erreur lors de la sauvegarde', 'error');
}
@@ -375,8 +414,6 @@ const CompteRenduActivites = () => {
}
};
const formatDateToString = (date) => {
if (!date) return null;
const year = date.getFullYear();
@@ -505,7 +542,6 @@ const CompteRenduActivites = () => {
<p className="text-sm opacity-90">Jours travaillés</p>
<p className="text-3xl font-bold">{statsAnnuelles.totalJoursTravailles || 0}</p>
</div>
</div>
</div>
)}
@@ -562,23 +598,21 @@ const CompteRenduActivites = () => {
<FileText className="w-4 h-4" />
<span className="hidden sm:inline">Saisie en masse</span>
</button>
</div>
</div>
</div>
{/* Calendrier */}
{/* Calendrier 7 colonnes (Lun-Dim) */}
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
<div className="grid grid-cols-6 gap-2 p-4 bg-gray-50">
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'].map(day => (
<div className="grid grid-cols-7 gap-2 p-4 bg-gray-50">
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'].map(day => (
<div key={day} className="text-center font-semibold text-gray-700 text-sm">
{day}
</div>
))}
</div>
<div className="grid grid-cols-6 gap-2 p-4">
<div className="grid grid-cols-7 gap-2 p-4">
{days.map((date, index) => {
// Case vide pour le décalage
if (date === null) {
@@ -589,10 +623,10 @@ const CompteRenduActivites = () => {
const jourData = getJourData(date);
const enConge = isJourEnConge(date);
const weekend = isWeekend(date);
const ferie = isHoliday(date);
const isPast = isPastOnly(date);
const isToday = date.toDateString() === new Date().toDateString();
const jourVerrouille = isJourVerrouille(date);
// Déterminer la classe de fond
let bgClass = 'bg-white hover:bg-gray-50';
@@ -601,7 +635,12 @@ const CompteRenduActivites = () => {
if (ferie) {
bgClass = 'bg-gray-700 text-white';
cursorClass = 'cursor-not-allowed';
} else if (weekend) {
// ⭐ Week-ends en gris clair et non cliquables
bgClass = 'bg-gray-100';
cursorClass = 'cursor-not-allowed';
} else if (enConge) {
// ⭐ Congés en violet et non cliquables
bgClass = 'bg-purple-100';
cursorClass = 'cursor-not-allowed';
} else if (jourData) {
@@ -623,17 +662,20 @@ const CompteRenduActivites = () => {
`}
title={
ferie ? getHolidayName(date) :
enConge ? 'En con' :
jourData ? 'Jour saisi - Cliquer pour modifier' :
''
weekend ? 'Week-end - Non saisissable' :
enConge ? 'En congé' :
jourData ? 'Jour saisi - Cliquer pour modifier' :
''
}
>
<div className={`text-right text-sm font-semibold mb-2 flex items-center justify-end gap-1 ${ferie ? 'text-white' :
jourData ? 'text-gray-700' :
'text-gray-700'
weekend ? 'text-gray-500' :
enConge ? 'text-purple-700' :
jourData ? 'text-gray-700' :
'text-gray-700'
}`}>
{date.getDate()}
{jourData && !ferie && !enConge && (
{jourData && !ferie && !enConge && !weekend && (
<Lock className="w-3 h-3 text-gray-600" />
)}
</div>
@@ -642,6 +684,10 @@ const CompteRenduActivites = () => {
<div className="text-center">
<div className="text-xs text-white font-bold truncate">{getHolidayName(date)}</div>
</div>
) : weekend ? (
<div className="text-center">
<div className="text-xs text-gray-500 font-semibold">Week-end</div>
</div>
) : enConge ? (
<div className="text-center">
<div className="text-xs text-purple-700 font-semibold">En congé</div>
@@ -699,6 +745,10 @@ const CompteRenduActivites = () => {
<Lock className="w-3 h-3 text-gray-600" />
<span>Jour saisi (grisé)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-100 border-2 border-gray-200 rounded"></div>
<span>Week-end</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-purple-100 border-2 border-gray-200 rounded"></div>
<span>En congé</span>
@@ -725,8 +775,6 @@ const CompteRenduActivites = () => {
</h3>
<div className="space-y-4">
{selectedJour.jourTravaille && (
<>
<div className="border-t pt-4">
@@ -748,26 +796,29 @@ const CompteRenduActivites = () => {
</label>
</div>
<div>
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={selectedJour.reposHebdo}
onChange={(e) => setSelectedJour({ ...selectedJour, reposHebdo: e.target.checked })}
className="w-5 h-5 text-blue-600 rounded mt-0.5"
/>
<div>
<span className="text-gray-700 font-medium block">
Respect du repos hebdomadaire
</span>
<span className="text-xs text-gray-500">
35 heures consécutives minimum (24h + 11h)
</span>
</div>
</label>
</div>
{/* ⭐ Afficher repos hebdomadaire uniquement le vendredi */}
{selectedJour.date.getDay() === 5 && (
<div>
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={selectedJour.reposHebdo}
onChange={(e) => setSelectedJour({ ...selectedJour, reposHebdo: e.target.checked })}
className="w-5 h-5 text-blue-600 rounded mt-0.5"
/>
<div>
<span className="text-gray-700 font-medium block">
Respect du repos hebdomadaire
</span>
<span className="text-xs text-gray-500">
35 heures consécutives minimum (24h + 11h)
</span>
</div>
</label>
</div>
)}
{(!selectedJour.reposQuotidien || !selectedJour.reposHebdo) && (
{(!selectedJour.reposQuotidien || (selectedJour.date.getDay() === 5 && !selectedJour.reposHebdo)) && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Commentaire obligatoire
@@ -823,7 +874,7 @@ const CompteRenduActivites = () => {
<SaisieMasseModal
mois={mois}
annee={annee}
days={days.filter(d => d !== null)} // Filtrer les cases vides
days={days.filter(d => d !== null)}
congesData={congesData}
holidays={holidays}
onClose={() => setShowSaisieMasse(false)}
@@ -835,7 +886,7 @@ const CompteRenduActivites = () => {
);
};
// Modal de saisie en masse
// Modal de saisie en masse (7 colonnes)
const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, onSave, isSaving }) => {
const [selectedDays, setSelectedDays] = useState([]);
@@ -843,10 +894,19 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
const isJourEnConge = (date) => {
const checkDate = new Date(date);
checkDate.setHours(0, 0, 0, 0);
return congesData.some(conge => {
const start = new Date(conge.startdate);
const end = new Date(conge.enddate);
return date >= start && date <= end && conge.statut === 'Valide';
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
const isInRange = checkDate >= start && checkDate <= end;
const isValide = conge.statut === 'Validée' || conge.statut === 'Validé' || conge.statut === 'Valide';
return isInRange && isValide;
});
};
@@ -858,6 +918,11 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
);
};
const isWeekend = (date) => {
const dayOfWeek = date.getDay();
return dayOfWeek === 0 || dayOfWeek === 6;
};
const isPastOnly = (date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
@@ -867,6 +932,11 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
};
const toggleDay = (date) => {
// ⭐ Bloquer la sélection des week-ends et congés
if (isWeekend(date) || isJourEnConge(date) || isHoliday(date)) {
return;
}
const dateStr = formatDateToString(date);
if (selectedDays.includes(dateStr)) {
setSelectedDays(selectedDays.filter(d => d !== dateStr));
@@ -876,8 +946,9 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
};
const selectAllWorkingDays = () => {
// ⭐ Exclure week-ends, congés et jours fériés
const workingDays = days
.filter(date => isPastOnly(date) && !isJourEnConge(date) && !isHoliday(date))
.filter(date => isPastOnly(date) && !isWeekend(date) && !isJourEnConge(date) && !isHoliday(date))
.map(date => formatDateToString(date));
setSelectedDays(workingDays);
@@ -902,7 +973,7 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
onSave(joursTravailles);
};
// Générer les jours avec décalage pour la saisie en masse aussi
// Générer les jours avec décalage pour lundi-dimanche
const getDaysWithOffset = () => {
const year = annee;
const month = mois - 1;
@@ -918,7 +989,6 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
daysWithOffset.push(null);
}
// Ajouter les jours réels
daysWithOffset.push(...days);
return daysWithOffset;
@@ -935,8 +1005,15 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<p className="text-sm text-blue-800">
Sélectionnez tous les jours travaillés du mois. Le jour actuel, les jours fériés et les congés sont automatiquement exclus.
Les repos quotidien et hebdomadaire seront considérés comme respectés.
Sélectionnez tous les jours travaillés du mois. Le jour actuel, les week-ends, les jours fériés et les congés sont automatiquement exclus.
</p>
</div>
{/* ⭐ Message d'information sur les repos */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4 flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-amber-800">
<strong>Par cette action, les repos quotidiens et hebdomadaires seront automatiquement considérés comme respectés.</strong>
</p>
</div>
@@ -947,7 +1024,8 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
Sélectionner tous les jours ouvrés disponibles
</button>
<div className="grid grid-cols-6 gap-2 p-4">
{/* Grid 7 colonnes */}
<div className="grid grid-cols-7 gap-2 p-4">
{daysWithOffset.map((date, index) => {
// Case vide
if (date === null) {
@@ -959,26 +1037,33 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
const dateStr = formatDateToString(date);
const enConge = isJourEnConge(date);
const ferie = isHoliday(date);
const weekend = isWeekend(date);
const isPast = isPastOnly(date);
const isSelected = selectedDays.includes(dateStr);
const isToday = date.toDateString() === new Date().toDateString();
// ⭐ Les week-ends ne sont pas sélectionnables
const isDisabled = !isPast || weekend || enConge || ferie;
return (
<div
key={index}
onClick={() => !enConge && !ferie && isPast && toggleDay(date)}
onClick={() => toggleDay(date)}
className={`
p-3 rounded-lg border-2 text-center cursor-pointer transition-all
p-3 rounded-lg border-2 text-center transition-all
${isToday ? 'border-cyan-500 bg-cyan-100' : ''}
${ferie ? 'bg-gray-700 text-white cursor-not-allowed' : ''}
${weekend ? 'bg-gray-100 cursor-not-allowed' : ''}
${enConge ? 'bg-purple-100 cursor-not-allowed' : ''}
${!isPast ? 'opacity-30 cursor-not-allowed' : ''}
${isSelected ? 'bg-green-500 border-green-600 text-white' : 'bg-white border-gray-200 hover:bg-gray-50'}
${isSelected && !isDisabled ? 'bg-green-500 border-green-600 text-white' : ''}
${!isDisabled && !isSelected ? 'bg-white border-gray-200 hover:bg-gray-50 cursor-pointer' : ''}
`}
>
<div className="font-semibold">{date.getDate()}</div>
{isToday && <div className="text-xs mt-1">Aujourd'hui</div>}
{ferie && <div className="text-xs mt-1">Férié</div>}
{weekend && <div className="text-xs mt-1 text-gray-500">WE</div>}
{enConge && <div className="text-xs mt-1 text-purple-700">Congé</div>}
</div>
);

View File

@@ -7,6 +7,29 @@ import { useMsal } from "@azure/msal-react";
import { loginRequest } from "../authConfig";
import { useNavigate } from 'react-router-dom';
// 🎄 COMPOSANT FLOCONS DE NEIGE
const Snowflakes = () => {
return (
<div className="snowflakes-container">
{[...Array(50)].map((_, i) => (
<div
key={i}
className="snowflake"
style={{
left: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 10}s`,
animationDuration: `${10 + Math.random() * 10}s`,
opacity: Math.random() * 0.6 + 0.4,
fontSize: `${Math.random() * 10 + 10}px`
}}
>
</div>
))}
</div>
);
};
const Dashboard = () => {
const { user } = useAuth();
const [graphToken, setGraphToken] = useState(null);
@@ -27,7 +50,6 @@ const Dashboard = () => {
const [recentRequests, setRecentRequests] = useState([]);
const [teamLeaves, setTeamLeaves] = useState([]);
const [isUpdatingCounters, setIsUpdatingCounters] = useState(false);
const navigate = useNavigate();
const userId = user?.id || user?.CollaborateurADId || user?.ID;
@@ -188,7 +210,7 @@ const Dashboard = () => {
const data = await response.json();
if (data.success) {
const filteredLeaves = (data.leaves || []).filter(leave => {
let filteredLeaves = (data.leaves || []).filter(leave => {
const startDate = new Date(leave.startdate);
const endDate = new Date(leave.enddate);
const startMonth = startDate.getMonth();
@@ -201,6 +223,14 @@ const Dashboard = () => {
(startDate <= new Date(currentYear, currentMonth, 1) &&
endDate >= new Date(currentYear, currentMonth + 1, 0));
});
// 🆕 FILTRE SPÉCIFIQUE POUR LES RH : seulement leur service
if (user.role === 'RH' && user.service) {
console.log('🔍 Filtrage RH - Service:', user.service);
filteredLeaves = filteredLeaves.filter(leave => leave.servicenom === user.service);
console.log('✅ Congés filtrés pour RH:', filteredLeaves.length);
}
setTeamLeaves(filteredLeaves);
}
} catch (error) {
@@ -466,7 +496,26 @@ const Dashboard = () => {
}
return (
<div className="min-h-screen bg-gray-50 flex">
<div className="min-h-screen bg-gray-50 flex christmas-theme">
{/* 🎄 DÉCORATIONS DE NOËL */}
<Snowflakes />
{/* Guirlande lumineuse en haut */}
<div className="christmas-lights">
<div className="light red"></div>
<div className="light yellow"></div>
<div className="light green"></div>
<div className="light blue"></div>
<div className="light red"></div>
<div className="light yellow"></div>
<div className="light green"></div>
<div className="light blue"></div>
<div className="light red"></div>
<div className="light yellow"></div>
<div className="light green"></div>
<div className="light blue"></div>
</div>
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
<div className="flex-1 lg:ml-60">
<div className="p-4 lg:p-8 w-full">
@@ -532,8 +581,8 @@ const Dashboard = () => {
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
Bonjour, {user?.name || user?.prenom || 'Utilisateur'} 👋
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2 christmas-title">
Bonjour, {user?.name || user?.prenom || 'Utilisateur'} 👋🎄
</h1>
<p className="text-sm lg:text-base text-gray-600">
Vos soldes de congés au {detailedCounters ? formatDate(detailedCounters.dateReference) : '...'}
@@ -804,7 +853,6 @@ const Dashboard = () => {
</div>
</div>
{/* RACCOURCI COMPTE-RENDU ACTIVITÉS (si forfait jour) */}
{(user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfait_jour') && (
<div
@@ -869,7 +917,6 @@ const Dashboard = () => {
</div>
)}
{detailedCounters.cpN && (
<div data-tour="cp-n" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
{isUpdatingCounters && (
@@ -966,7 +1013,7 @@ const Dashboard = () => {
<div className="bg-gradient-to-r from-purple-500 to-purple-600 p-4 text-white">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold">Récupérations {detailedCounters.recupN.annee}</h3>
<h3 className="text-lg font-bold">Récupérations</h3>
</div>
<Calendar className="w-8 h-8 opacity-80" />
</div>
@@ -977,7 +1024,7 @@ const Dashboard = () => {
<span className="text-2xl font-bold text-purple-600">{detailedCounters.recupN.solde.toFixed(2)}j</span>
</div>
<div className="flex justify-between items-center py-2 border-b border-gray-100">
<span className="text-sm font-medium text-gray-600">Jours accumulés</span>
<span className="text-sm font-medium text-gray-600">Jours acquis</span>
<span className="text-lg font-bold text-green-600">+{detailedCounters.recupN.acquis.toFixed(2)}j</span>
</div>
<div className="flex justify-between items-center py-2 border-b border-gray-100">
@@ -1037,7 +1084,136 @@ const Dashboard = () => {
</div>
</div>
{/* 🎄 STYLES NOËL */}
<style>{`
/* ===== THÈME DE NOËL ===== */
.christmas-theme {
position: relative;
overflow-x: hidden;
}
/* Flocons de neige */
.snowflakes-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
overflow: hidden;
}
.snowflake {
position: absolute;
top: -20px;
color: #fff;
text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
animation: fall linear infinite;
user-select: none;
}
@keyframes fall {
0% {
top: -10%;
transform: translateX(0) rotate(0deg);
}
100% {
top: 110%;
transform: translateX(100px) rotate(360deg);
}
}
/* Guirlande lumineuse en haut */
.christmas-lights {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 20px;
display: flex;
justify-content: space-around;
z-index: 49;
pointer-events: none;
background: linear-gradient(to bottom, rgba(0,0,0,0.1), transparent);
}
.light {
width: 15px;
height: 15px;
border-radius: 50%;
margin: 5px;
animation: blink 1.5s infinite;
box-shadow: 0 0 10px currentColor;
}
.light.red {
background: #ff0000;
animation-delay: 0s;
}
.light.yellow {
background: #ffff00;
animation-delay: 0.3s;
}
.light.green {
background: #00ff00;
animation-delay: 0.6s;
}
.light.blue {
background: #0000ff;
animation-delay: 0.9s;
}
@keyframes blink {
0%, 49%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(0.9);
}
}
/* Effet scintillement sur le titre */
.christmas-title:hover {
animation: sparkle 0.5s ease-in-out;
}
@keyframes sparkle {
0%, 100% {
text-shadow: none;
}
50% {
text-shadow: 0 0 10px rgba(6, 182, 212, 0.6),
0 0 20px rgba(6, 182, 212, 0.4);
}
}
/* Petit cadeau qui apparaît au survol des cartes */
[data-tour]:hover::before {
content: '🎁';
position: absolute;
top: -10px;
right: -10px;
font-size: 24px;
animation: bounce 0.5s ease-in-out;
z-index: 10;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
/* Animation slideInRight existante */
@keyframes slideInRight {
from {
transform: translateX(400px);
@@ -1053,4 +1229,4 @@ const Dashboard = () => {
);
};
export default Dashboard;
export default Dashboard;

View File

@@ -1,37 +1,52 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router-dom';
import Sidebar from '../components/Sidebar';
import { Calendar, Clock, CheckCircle, XCircle } from 'lucide-react';
import {
Calendar,
Clock,
CheckCircle,
XCircle,
ArrowLeft,
Mail,
Briefcase,
Building,
TrendingDown,
TrendingUp
} from 'lucide-react';
const EmployeeDetails = () => {
const { id } = useParams();
const navigate = useNavigate();
const [employee, setEmployee] = useState(null);
const [requests, setRequests] = useState([]);
const [detailedCounters, setDetailedCounters] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchEmployeeData();
}, [id]);
// Dans EmployeeDetails.jsx, modifier fetchEmployeeData:
const fetchEmployeeData = async () => {
try {
setIsLoading(true);
// 1⃣ Données employé (avec compteurs inclus)
const resEmployee = await fetch(`/api/getEmploye?id=${id}`);
const dataEmployee = await resEmployee.json();
console.log("Réponse API employé:", dataEmployee);
if (!dataEmployee.success) {
setEmployee(null);
return;
}
// ✅ Les compteurs sont déjà dans la réponse
setEmployee(dataEmployee.employee);
// 2⃣ Historique des demandes
const resCounters = await fetch(`/api/getDetailedLeaveCounters?user_id=${id}`);
const dataCounters = await resCounters.json();
if (dataCounters.success) {
setDetailedCounters(dataCounters.data);
}
const resRequests = await fetch(`/api/getEmployeRequest?id=${id}`);
const dataRequests = await resRequests.json();
@@ -46,64 +61,257 @@ const EmployeeDetails = () => {
}
};
const getStatusIcon = (status) => {
const getStatusConfig = (status) => {
switch (status) {
case 'Validée':
return <CheckCircle className="inline text-green-500 mr-1" />;
return {
icon: <CheckCircle className="w-4 h-4" />,
bg: 'bg-emerald-50',
text: 'text-emerald-700',
dot: 'bg-emerald-500'
};
case 'Refusée':
case 'Annulée':
return <XCircle className="inline text-red-500 mr-1" />;
return {
icon: <XCircle className="w-4 h-4" />,
bg: 'bg-red-50',
text: 'text-red-700',
dot: 'bg-red-500'
};
default:
return <Clock className="inline text-yellow-500 mr-1" />;
return {
icon: <Clock className="w-4 h-4" />,
bg: 'bg-amber-50',
text: 'text-amber-700',
dot: 'bg-amber-500'
};
}
};
if (isLoading) return <p className="text-center p-6">Chargement...</p>;
if (!employee) return <p className="text-center p-6">Collaborateur introuvable</p>;
const getTypeContratLabel = (type) => {
switch (type) {
case '37h': return '37h/sem';
case 'forfait_jour': return 'Forfait jour';
case 'temps_partiel': return 'Temps partiel';
default: return type || '37h/sem';
}
};
const CounterCard = ({ label, solde, acquis, pris, color, icon: Icon }) => {
const colorClasses = {
blue: { bg: 'bg-blue-500', light: 'bg-blue-50', text: 'text-blue-600', border: 'border-blue-200' },
cyan: { bg: 'bg-cyan-500', light: 'bg-cyan-50', text: 'text-cyan-600', border: 'border-cyan-200' },
green: { bg: 'bg-emerald-500', light: 'bg-emerald-50', text: 'text-emerald-600', border: 'border-emerald-200' },
purple: { bg: 'bg-violet-500', light: 'bg-violet-50', text: 'text-violet-600', border: 'border-violet-200' },
};
const c = colorClasses[color] || colorClasses.blue;
return (
<div className={`relative bg-white rounded-2xl border ${c.border} p-5 hover:shadow-md transition-shadow`}>
<div className="flex items-start justify-between mb-4">
<div>
<p className="text-sm font-medium text-gray-500 mb-1">{label}</p>
<p className={`text-3xl font-bold ${c.text}`}>{solde.toFixed(1)}<span className="text-lg ml-1">j</span></p>
</div>
<div className={`${c.bg} p-2.5 rounded-xl`}>
<Icon className="w-5 h-5 text-white" />
</div>
</div>
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-1.5">
<TrendingUp className="w-3.5 h-3.5 text-emerald-500" />
<span className="text-gray-600">Acquis:</span>
<span className="font-semibold text-gray-900">{acquis.toFixed(1)}j</span>
</div>
<div className="flex items-center gap-1.5">
<TrendingDown className="w-3.5 h-3.5 text-red-400" />
<span className="text-gray-600">Pris:</span>
<span className="font-semibold text-gray-900">{pris.toFixed(1)}j</span>
</div>
</div>
</div>
);
};
if (isLoading) return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-cyan-600 mx-auto mb-3"></div>
<p className="text-gray-600">Chargement...</p>
</div>
</div>
);
if (!employee) return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<p className="text-gray-600 mb-4">Collaborateur introuvable</p>
<button
onClick={() => navigate(-1)}
className="text-cyan-600 hover:underline"
>
Retour
</button>
</div>
</div>
);
return (
<div className="min-h-screen bg-gray-50 flex">
<Sidebar />
<div className="flex-1 lg:ml-60 p-6">
<h1 className="text-2xl font-bold mb-2">{employee.Prenom} {employee.Nom}</h1>
<p className="text-gray-600 mb-6">{employee.Email}</p>
<div className="flex-1 lg:ml-60 p-6 lg:p-8">
{/* Bouton retour */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
<span className="text-sm font-medium">Retour</span>
</button>
{/* Compteurs congés/RTT */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="bg-white p-4 rounded-xl shadow">
<p className="text-sm text-gray-600">Congés restants</p>
<p className="text-xl font-bold">{employee.conges_restants || 0} jours</p>
</div>
<div className="bg-white p-4 rounded-xl shadow">
<p className="text-sm text-gray-600">RTT restants</p>
<p className="text-xl font-bold">{employee.rtt_restants || 0} jours</p>
{/* Profil employé */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 mb-6">
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
{/* Avatar */}
<div className="w-16 h-16 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-2xl flex items-center justify-center flex-shrink-0">
<span className="text-2xl font-bold text-white">
{employee.Prenom?.charAt(0)}{employee.Nom?.charAt(0)}
</span>
</div>
{/* Infos */}
<div className="flex-1">
<h1 className="text-xl font-bold text-gray-900 mb-1">
{employee.Prenom} {employee.Nom}
</h1>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600">
<div className="flex items-center gap-1.5">
<Mail className="w-4 h-4 text-gray-400" />
<span>{employee.Email}</span>
</div>
{detailedCounters?.user?.role && (
<div className="flex items-center gap-1.5">
<Briefcase className="w-4 h-4 text-gray-400" />
<span>{detailedCounters.user.role}</span>
</div>
)}
{detailedCounters?.user?.service && (
<div className="flex items-center gap-1.5">
<Building className="w-4 h-4 text-gray-400" />
<span>{detailedCounters.user.service}</span>
</div>
)}
</div>
</div>
{/* Badge contrat */}
{detailedCounters?.user?.typeContrat && (
<div className="px-3 py-1.5 bg-gray-100 rounded-lg text-sm font-medium text-gray-700">
{getTypeContratLabel(detailedCounters.user.typeContrat)}
</div>
)}
</div>
</div>
{/* Historique des congés */}
<h2 className="text-lg font-semibold mb-4">Historique des congés</h2>
<div className="space-y-3">
{requests.length === 0 ? (
<p className="text-gray-500">Aucune demande</p>
) : (
requests.map((r) => (
<div key={r.Id} className="bg-white p-4 rounded-xl shadow border flex justify-between items-center">
<div>
<p className="font-medium">{r.type} - {r.days}j</p>
<p className="text-sm text-gray-600">{r.date_display}</p>
</div>
<div className="flex items-center">
{getStatusIcon(r.status)}
<span className="text-sm text-gray-700">{r.status}</span>
</div>
{/* Compteurs */}
{detailedCounters && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{detailedCounters.cpN1 && (
<CounterCard
label="CP N-1"
solde={detailedCounters.cpN1.solde}
acquis={detailedCounters.cpN1.reporte}
pris={detailedCounters.cpN1.pris}
color="blue"
icon={Calendar}
/>
)}
{detailedCounters.cpN && (
<CounterCard
label="CP N"
solde={detailedCounters.cpN.solde}
acquis={detailedCounters.cpN.acquis}
pris={detailedCounters.cpN.pris}
color="cyan"
icon={Calendar}
/>
)}
{detailedCounters.rttN && detailedCounters.user?.role !== 'Apprenti' && (
<CounterCard
label={`RTT ${detailedCounters.rttN.annee}`}
solde={detailedCounters.rttN.solde}
acquis={detailedCounters.rttN.acquis}
pris={detailedCounters.rttN.pris}
color="green"
icon={Clock}
/>
)}
{detailedCounters.recupN && (
<CounterCard
label="Récupérations"
solde={detailedCounters.recupN.solde}
acquis={detailedCounters.recupN.acquis}
pris={detailedCounters.recupN.pris}
color="purple"
icon={Clock}
/>
)}
</div>
)}
{/* Historique */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100">
<h2 className="text-lg font-semibold text-gray-900">Historique des demandes</h2>
<p className="text-sm text-gray-500">{requests.length} demande{requests.length > 1 ? 's' : ''}</p>
</div>
<div className="divide-y divide-gray-100">
{requests.length === 0 ? (
<div className="px-6 py-12 text-center">
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">Aucune demande de congés</p>
</div>
))
)}
) : (
requests.map((r) => {
const statusConfig = getStatusConfig(r.status);
return (
<div key={r.Id} className="px-6 py-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`w-2 h-2 rounded-full ${statusConfig.dot}`}></div>
<div>
<p className="font-medium text-gray-900">{r.type}</p>
<p className="text-sm text-gray-500">{r.date_display}</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-gray-700">{r.days}j</span>
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${statusConfig.bg} ${statusConfig.text}`}>
{statusConfig.icon}
{r.status}
</span>
</div>
</div>
</div>
);
})
)}
</div>
</div>
</div>
</div>
);
};
export default EmployeeDetails;
export default EmployeeDetails;

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';
import { AlertTriangle } from 'lucide-react';
@@ -8,23 +8,39 @@ const Login = () => {
const [error, setError] = useState('');
const navigate = useNavigate();
const { loginWithO365 } = useAuth();
const { loginWithO365, isAuthorized, isLoading: authLoading } = useAuth();
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
// ✅ AJOUT : Redirection automatique si déjà connecté (cas retour OAuth mobile)
useEffect(() => {
if (isAuthorized && !authLoading) {
console.log('✅ Utilisateur autorisé détecté, redirection vers dashboard...');
navigate('/dashboard', { replace: true });
}
}, [isAuthorized, authLoading, navigate]);
const handleO365Login = async () => {
setIsLoading(true);
setError('');
try {
const success = await loginWithO365();
if (isMobile) {
console.log('🔐 Redirection mobile vers Office 365...');
await loginWithO365();
// Ce code ne sera jamais atteint sur mobile car il y a une redirection
} else {
const success = await loginWithO365();
if (!success) {
setError("Erreur lors de la connexion Office 365");
setIsLoading(false);
return;
if (!success) {
setError("Erreur lors de la connexion Office 365");
setIsLoading(false);
return;
}
navigate('/dashboard');
}
navigate('/dashboard');
} catch (error) {
console.error('Erreur O365:', error);
@@ -32,14 +48,28 @@ const Login = () => {
setError('Accès refusé : Vous devez être membre d\'un groupe autorisé dans votre organisation.');
} else if (error.message?.includes('AADSTS')) {
setError('Erreur d\'authentification Azure AD. Contactez votre administrateur.');
} else if (error.errorCode === 'user_cancelled') {
setError('Connexion annulée');
} else {
setError(error.message || "Erreur lors de la connexion Office 365");
}
}
setIsLoading(false);
setIsLoading(false);
}
};
// ✅ AJOUT : Afficher un loader pendant la vérification de l'auth
if (authLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-cyan-600 mx-auto mb-4"></div>
<p className="text-gray-600">Vérification de la connexion...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex flex-col lg:flex-row">
{/* Image côté gauche */}
@@ -120,4 +150,4 @@ const Login = () => {
);
};
export default Login;
export default Login;

View File

@@ -12,6 +12,7 @@ import {
Check,
X,
MessageSquare,
Loader2,
} from "lucide-react";
import { useNavigate } from "react-router-dom";
import { motion, AnimatePresence } from "framer-motion";
@@ -31,7 +32,6 @@ const Manager = () => {
const [comment, setComment] = useState("");
const [isValidating, setIsValidating] = useState(false);
useEffect(() => {
if (user?.id) fetchTeamData();
}, [user]);
@@ -51,35 +51,77 @@ const Manager = () => {
}
};
// ✅ SIMPLIFIÉ - Le backend gère tout le filtrage
// ✅ SIMPLIFIÉ - Le backend gère tout le filtrage
const fetchTeamMembers = async () => {
try {
const res = await fetch(`/api/getTeamMembers?manager_id=${user.id}`);
const data = await res.json();
if (data.success) setTeamMembers(data.team_members || []);
else setTeamMembers([]);
} catch {
console.log('📊 getTeamMembers:', {
success: data.success,
count: data.team_members?.length || 0,
role: user.role,
service: user.service,
campus: user.campus
});
if (data.success) {
setTeamMembers(data.team_members || []);
} else {
console.error('❌ Erreur getTeamMembers:', data.message);
setTeamMembers([]);
}
} catch (error) {
console.error('❌ Erreur fetch getTeamMembers:', error);
setTeamMembers([]);
}
};
// ✅ SIMPLIFIÉ - Le backend gère tout le filtrage
const fetchPendingRequests = async () => {
try {
const res = await fetch(`/api/getPendingRequests?manager_id=${user.id}`);
const data = await res.json();
if (data.success) setPendingRequests(data.requests || []);
else setPendingRequests([]);
} catch {
console.log('📊 getPendingRequests:', {
success: data.success,
count: data.requests?.length || 0,
role: user.role
});
if (data.success) {
setPendingRequests(data.requests || []);
} else {
console.error('❌ Erreur getPendingRequests:', data.message);
setPendingRequests([]);
}
} catch (error) {
console.error('❌ Erreur fetch getPendingRequests:', error);
setPendingRequests([]);
}
};
// ✅ SIMPLIFIÉ - Le backend gère tout le filtrage
const fetchAllTeamRequests = async () => {
try {
const res = await fetch(`/api/getAllTeamRequests?SuperieurId=${user.id}`);
const data = await res.json();
if (data.success) setAllRequests(data.requests || []);
else setAllRequests([]);
} catch {
console.log('📊 getAllTeamRequests:', {
success: data.success,
count: data.requests?.length || 0,
role: user.role
});
if (data.success) {
setAllRequests(data.requests || []);
} else {
console.error('❌ Erreur getAllTeamRequests:', data.message);
setAllRequests([]);
}
} catch (error) {
console.error('❌ Erreur fetch getAllTeamRequests:', error);
setAllRequests([]);
}
};
@@ -87,9 +129,11 @@ const Manager = () => {
const openValidationModal = (request, action) => {
setValidationModal({ request, action });
setComment("");
setIsValidating(false);
};
const closeValidationModal = () => {
if (isValidating) return;
setValidationModal(null);
setComment("");
};
@@ -102,49 +146,48 @@ const Manager = () => {
return;
}
await handleValidateRequest(request.id, action, comment);
closeValidationModal();
if (isValidating) return;
setIsValidating(true);
try {
await handleValidateRequest(request.id, action, comment);
showToast("success", action === "approve" ? "Demande approuvée avec succès" : "Demande refusée");
closeValidationModal();
} catch (error) {
showToast("error", "Une erreur est survenue");
setIsValidating(false);
}
};
const handleValidateRequest = async (requestId, action, comment = '') => {
if (!user || !user.id) {
alert('Utilisateur non identifié');
return;
throw new Error('Utilisateur non identifié');
}
try {
setIsValidating(true); // ✅ Maintenant défini
const response = await fetch('/api/validateRequest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_id: requestId,
action: action,
validator_id: user.id,
comment: comment
}),
});
const response = await fetch('/api/validateRequest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_id: requestId,
action: action,
validator_id: user.id,
comment: comment
}),
});
const data = await response.json();
const data = await response.json();
if (data.success) {
// Rafraîchir les données
await Promise.all([
fetchPendingRequests(),
fetchAllTeamRequests()
]);
} else {
alert(`❌ Erreur : ${data.message}`);
}
} catch (error) {
console.error('❌ Erreur lors de la validation:', error);
alert('❌ Erreur lors de la validation de la demande');
} finally {
setIsValidating(false);
if (!data.success) {
throw new Error(data.message || 'Erreur lors de la validation');
}
await Promise.all([
fetchPendingRequests(),
fetchAllTeamRequests()
]);
};
const showToast = (type, message) => {
setToast({ type, message });
setTimeout(() => setToast(null), 4000);
@@ -199,6 +242,7 @@ const Manager = () => {
);
}
return (
<div className="relative min-h-screen bg-gray-50 flex overflow-hidden">
{/* Toast Notification */}
@@ -305,7 +349,9 @@ const Manager = () => {
onChange={(e) => setComment(e.target.value)}
placeholder={validationModal.action === "approve" ? "Ajouter un commentaire..." : "Expliquer le motif du refus..."}
rows={4}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:outline-none resize-none ${validationModal.action === "reject" && !comment.trim()
disabled={isValidating}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:outline-none resize-none transition ${isValidating ? 'bg-gray-100 cursor-not-allowed' : ''
} ${validationModal.action === "reject" && !comment.trim()
? "border-red-300 focus:ring-red-500 focus:border-red-500"
: "border-gray-300 focus:ring-blue-500 focus:border-blue-500"
}`}
@@ -319,17 +365,25 @@ const Manager = () => {
<div className="p-6 border-t border-gray-100 flex gap-3">
<button
onClick={closeValidationModal}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition font-medium"
disabled={isValidating}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Annuler
</button>
<button
onClick={confirmValidation}
disabled={validationModal.action === "reject" && !comment.trim()}
className={`flex-1 px-4 py-2 text-white rounded-lg transition font-medium disabled:opacity-50 disabled:cursor-not-allowed ${validationModal.action === "approve" ? "bg-green-600 hover:bg-green-700" : "bg-red-600 hover:bg-red-700"
disabled={isValidating || (validationModal.action === "reject" && !comment.trim())}
className={`flex-1 px-4 py-2 text-white rounded-lg transition font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 ${validationModal.action === "approve" ? "bg-green-600 hover:bg-green-700" : "bg-red-600 hover:bg-red-700"
}`}
>
{validationModal.action === "approve" ? "Approuver" : "Refuser"}
{isValidating ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>Traitement...</span>
</>
) : (
validationModal.action === "approve" ? "Approuver" : "Refuser"
)}
</button>
</div>
</motion.div>
@@ -383,15 +437,17 @@ const Manager = () => {
<div className="flex gap-2">
<button
onClick={() => openValidationModal(r, "approve")}
className="flex-1 bg-green-600 text-white px-3 py-2 rounded-lg hover:bg-green-700 text-sm"
data-tour="approuver-btn" >
disabled={isValidating}
className="flex-1 bg-green-600 text-white px-3 py-2 rounded-lg hover:bg-green-700 text-sm disabled:opacity-50 disabled:cursor-not-allowed transition"
data-tour="approuver-btn">
<CheckCircle className="w-4 h-4 inline mr-1" />
Approuver
</button>
<button
onClick={() => openValidationModal(r, "reject")}
className="flex-1 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 text-sm"
data-tour="refuser-btn" >
disabled={isValidating}
className="flex-1 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 text-sm disabled:opacity-50 disabled:cursor-not-allowed transition"
data-tour="refuser-btn">
<XCircle className="w-4 h-4 inline mr-1" />
Refuser
</button>
@@ -417,7 +473,7 @@ const Manager = () => {
key={m.id}
onClick={() => navigate(`/employee/${m.id}`)}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer transition"
data-tour="membre-equipe" >
data-tour="membre-equipe">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 font-medium text-sm">
@@ -488,16 +544,15 @@ const Manager = () => {
</a>
</div>
)}
</div>
))
</div>
))
)}
</div>
</div>
</div>
)}
</div>
<GlobalTutorial userId={user?.id} userRole={user?.role} />
</div >
</div>
);
};

View File

@@ -43,18 +43,15 @@ const Requests = () => {
const [sseConnected, setSseConnected] = useState(false);
const [toasts, setToasts] = useState([]);
// ⭐ NOUVEAU : State pour la modal de confirmation de suppression
// ⭐ State pour la modal de confirmation de suppression
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [requestToDelete, setRequestToDelete] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// 🎯 STATES POUR LE TUTORIEL
const [runTour, setRunTour] = useState(false);
// 🎯 DÉCLENCHER LE TUTORIEL À CHAQUE FOIS
useEffect(() => {
if (userId && !isLoading) {
@@ -95,7 +92,6 @@ const Requests = () => {
}
};
useEffect(() => {
if (accounts.length > 0) {
const request = {
@@ -140,6 +136,10 @@ const Requests = () => {
throw new Error('Le serveur PHP ne répond pas correctement');
}
if (data.success) {
console.log('🔍 DEBUG - Requests reçues:', data.requests);
if (data.requests && data.requests.length > 0) {
console.log('🔍 DEBUG - Premier request:', data.requests[0]);
}
setAllRequests(data.requests || []);
} else {
throw new Error(data.message || 'Erreur lors de la récupération des demandes');
@@ -184,18 +184,22 @@ const Requests = () => {
}, 5000);
}, []);
// ⭐ NOUVELLE FONCTION : Modifier une demande
// ⭐ FONCTION CORRIGÉE : Modifier une demande
const handleEditRequest = (request) => {
if (request.status !== 'En attente') {
showToast('⚠️ Vous ne pouvez modifier que les demandes en attente', 'warning');
return;
}
console.log('🔍 DEBUG - Request à éditer:', request);
console.log('🔍 DEBUG - Request ID:', request.id);
console.log('🔍 DEBUG - Request startDate:', request.startDate);
console.log('🔍 DEBUG - Request endDate:', request.endDate);
console.log('🔍 DEBUG - Request type:', request.type);
console.log('🔍 DEBUG - Request reason:', request.reason);
setRequestToEdit(request);
setShowEditRequestModal(true);
console.log('✅ Modal d\'édition devrait s\'ouvrir');
};
// ⭐ NOUVELLE FONCTION : Supprimer une demande
// ⭐ NOUVELLE FONCTION : Annuler une demande (En attente OU Validée, si date future)
// ⭐ FONCTION : Supprimer/Annuler une demande
const handleDeleteRequest = async (requestId) => {
try {
setIsLoading(true);
@@ -209,7 +213,7 @@ const Requests = () => {
console.log('⚠️ Demande non trouvée dans l\'état local, récupération via API...');
try {
const response = await fetch(`/getRequests?user_id=${user.id}`);
const response = await fetch(`/api/getRequests?user_id=${userId}`);
const result = await response.json();
if (result.success && result.requests) {
@@ -246,7 +250,7 @@ const Requests = () => {
return;
}
// ⭐ CONFIRMATION AVEC MODAL AU LIEU DE alert()
// ⭐ CONFIRMATION AVEC MODAL
setRequestToDelete(request);
setShowDeleteConfirm(true);
setIsLoading(false);
@@ -257,6 +261,7 @@ const Requests = () => {
setIsLoading(false);
}
};
// Fonction helper pour formater les dates
const formatDate = (dateStr) => {
if (!dateStr) return '';
@@ -264,8 +269,7 @@ const Requests = () => {
return date.toLocaleDateString('fr-FR');
};
// ⭐ NOUVELLE FONCTION : Confirmer la suppression (sans setDeletedRequests)
// ⭐ FONCTION : Confirmer la suppression
const confirmDeleteRequest = async () => {
if (!requestToDelete) return;
@@ -339,6 +343,7 @@ const Requests = () => {
}
}
};
// Connexion SSE
useEffect(() => {
if (!userId) return;
@@ -405,7 +410,7 @@ const Requests = () => {
if (userId) {
refreshAllData();
}
}, [userId]);
}, [userId, refreshAllData]);
useEffect(() => {
let filtered = allRequests;
@@ -427,7 +432,7 @@ const Requests = () => {
setFilteredRequests(filtered);
setCurrentPage(1);
}, [allRequests, searchTerm, statusFilter, typeFilter]);
}, [allRequests, searchTerm, statusFilter, typeFilter]);
const indexOfLastRequest = currentPage * requestsPerPage;
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
@@ -441,7 +446,7 @@ const Requests = () => {
case 'En attente': return 'bg-yellow-100 text-yellow-800';
case 'Validée': return 'bg-green-100 text-green-800';
case 'Refusée': return 'bg-red-100 text-red-800';
case 'Annulée': return 'bg-gray-100 text-gray-800'; // ⭐ AJOUT
case 'Annulée': return 'bg-gray-100 text-gray-800';
default: return 'bg-gray-100 text-gray-800';
}
};
@@ -588,7 +593,6 @@ const Requests = () => {
</button>
<div>
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900">Mes demandes</h1>
</div>
</div>
<div className="flex items-center gap-2">
@@ -601,7 +605,7 @@ const Requests = () => {
<RefreshCw className={`w-5 h-5 ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
<button
data-tour="nouvelle-demande"
data-tour="nouvelle-demande"
onClick={() => setShowNewRequestModal(true)}
className="bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-700 text-sm lg:text-base"
>
@@ -610,8 +614,6 @@ const Requests = () => {
</div>
</div>
{/* Compteurs */}
{/* Main content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: list */}
@@ -632,7 +634,7 @@ const Requests = () => {
</div>
</div>
<button
data-tour="filtres"
data-tour="filtres"
onClick={() => setShowFilters(!showFilters)}
className="flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50"
>
@@ -676,13 +678,13 @@ const Requests = () => {
<p className="text-gray-500">Chargement...</p>
</div>
) : currentRequests.length === 0 ? (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 text-center" >
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 text-center">
<Info className="w-12 h-12 mx-auto text-gray-300 mb-3" />
<p className="text-gray-500">Aucune demande trouvée</p>
</div>
) : (
<>
<div className="space-y-3" data-tour="liste-demandes">
<div className="space-y-3" data-tour="liste-demandes">
{currentRequests.map((request) => (
<div key={request.id} className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-2">
@@ -702,20 +704,18 @@ const Requests = () => {
<div className="mt-3 flex justify-between items-center text-sm">
<span className="text-gray-500">{request.submittedDisplay}</span>
<div className="flex items-center gap-2">
{/* Bouton Modifier (seulement si En attente) */}
{request.status === 'En attente' && (
<button
onClick={() => handleEditRequest(request)}
className="text-blue-600 hover:text-blue-700 flex items-center gap-1 px-2 py-1 hover:bg-blue-50 rounded"
title="Modifier"
>
<Edit2 className="w-4 h-4" />
<span className="hidden sm:inline">Modifier</span>
</button>
)}
{/* Bouton Modifier - Plus de restriction sur le statut */}
<button
onClick={() => handleEditRequest(request)}
className="text-blue-600 hover:text-blue-700 flex items-center gap-1 px-2 py-1 hover:bg-blue-50 rounded"
title="Modifier"
>
<Edit2 className="w-4 h-4" />
<span className="hidden sm:inline">Modifier</span>
</button>
{/* Bouton Supprimer */}
{request.status === 'En attente' && (
{/* Bouton Annuler */}
{request.status !== 'Annulée' && request.status !== 'Refusée' && (
<button
onClick={() => handleDeleteRequest(request.id)}
className="text-orange-600 hover:text-orange-700 flex items-center gap-1 px-2 py-1 hover:bg-orange-50 rounded"
@@ -803,29 +803,31 @@ const Requests = () => {
<p className="italic text-sm bg-gray-50 p-3 rounded-lg border-l-4" style={{ borderLeftColor: selectedRequest.status === 'Validée' ? '#10b981' : '#ef4444' }}>{selectedRequest.validationComment}</p>
</div>
)}
<MedicalDocuments demandeId={selectedRequest.id} />
<MedicalDocuments demandeId={selectedRequest.id} />
</div>
{/* Actions dans la sidebar */}
<div className="mt-6 pt-4 border-t space-y-2">
{selectedRequest.status === 'En attente' && (
{/* ⭐ Bouton Modifier - Toujours visible */}
<button
onClick={() => handleEditRequest(selectedRequest)}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<Edit2 className="w-4 h-4" />
Modifier cette demande
</button>
{/* Bouton Annuler */}
{selectedRequest.status !== 'Annulée' && selectedRequest.status !== 'Refusée' && (
<button
onClick={() => handleEditRequest(selectedRequest)}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
onClick={() => handleDeleteRequest(selectedRequest.id)}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"
>
<Edit2 className="w-4 h-4" />
Modifier cette demande
<X className="w-4 h-4" />
Annuler cette demande
</button>
)}
<button
onClick={() => handleDeleteRequest(selectedRequest.id)}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"
>
<X className="w-4 h-4" />
Annuler cette demande
</button>
</div>
</div>
) : (
@@ -865,10 +867,12 @@ const Requests = () => {
/>
)}
{/* Modal d'édition */}
{/* ⭐ MODAL D'ÉDITION CORRIGÉE */}
{showEditRequestModal && requestToEdit && detailedCounters && (
<EditLeaveRequestModal
isOpen={showEditRequestModal}
onClose={() => {
console.log('❌ Fermeture du modal d\'édition');
setShowEditRequestModal(false);
setRequestToEdit(null);
}}
@@ -889,10 +893,12 @@ const Requests = () => {
accessToken={graphToken}
userId={userId}
userEmail={user.email}
userRole={user.role}
userName={`${user.prenom} ${user.nom}`} // CORRIGÉ : ajout des accolades {}
userName={`${user.prenom} ${user.nom}`}
onRequestUpdated={() => {
console.log('✅ Demande mise à jour, rafraîchissement...');
refreshAllData();
setShowEditRequestModal(false);
setRequestToEdit(null);
}}
/>
)}

View File

@@ -9,12 +9,12 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://192.168.0.4:3000',
target: 'http://192.168.0.3:3004',
changeOrigin: true,
secure: false
},
'/uploads': {
target: 'http://192.168.0.4:3000',
target: 'http://192.168.0.3:3004',
changeOrigin: true,
secure: false
}