Compare commits
4 Commits
89d74363f8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d24aff2e9 | |||
| 91cd1dff2f | |||
| 47c1fb99b8 | |||
| 048c2929b9 |
@@ -1,36 +1,38 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
|
image: ouijdaneim/gta-backend-dev:latest # ✅ Ajoute cette ligne
|
||||||
build:
|
build:
|
||||||
context: ./project/public/Backend
|
context: ./project/public/Backend
|
||||||
dockerfile: DockerfileGTA.backend
|
dockerfile: DockerfileGTA.backend
|
||||||
container_name: gta-backend
|
container_name: gtaDev-backend
|
||||||
hostname: backend
|
hostname: backend
|
||||||
ports:
|
ports:
|
||||||
- "8012:3000"
|
- "8014:3004"
|
||||||
volumes:
|
volumes:
|
||||||
- ./project/public/Backend/uploads:/app/uploads
|
- ./project/public/Backend/uploads:/app/uploads
|
||||||
networks:
|
networks:
|
||||||
- gta-network
|
- gtaDev-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
image: ouijdaneim/gta-frontend-dev:latest # ✅ Ajoute cette ligne
|
||||||
build:
|
build:
|
||||||
context: ./project
|
context: ./project
|
||||||
dockerfile: DockerfileGTA.frontend
|
dockerfile: DockerfileGTA.frontend
|
||||||
container_name: gta-frontend
|
container_name: gtaDev-frontend
|
||||||
hostname: frontend
|
hostname: frontend
|
||||||
ports:
|
ports:
|
||||||
- "3013:80"
|
- "3015:90"
|
||||||
environment:
|
environment:
|
||||||
- VITE_API_URL=http://backend:3000
|
- VITE_API_URL=http://backend:3004
|
||||||
networks:
|
networks:
|
||||||
- gta-network
|
- gtaDev-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
gta-network:
|
gtaDev-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
229
package-lock.json
generated
229
package-lock.json
generated
@@ -6,9 +6,11 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"framer-motion": "^12.23.22",
|
"framer-motion": "^12.23.22",
|
||||||
"node-cron": "^4.2.1"
|
"node-cron": "^4.2.1",
|
||||||
|
"react-datepicker": "^9.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
@@ -79,7 +81,6 @@
|
|||||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
@@ -675,7 +676,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -699,15 +699,14 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.5.0",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
||||||
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
|
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -717,9 +716,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.5.0",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
||||||
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
|
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -1180,6 +1179,59 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -2017,27 +2069,6 @@
|
|||||||
"@sinonjs/commons": "^3.0.1"
|
"@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": {
|
"node_modules/@testing-library/jest-dom": {
|
||||||
"version": "6.8.0",
|
"version": "6.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
|
||||||
@@ -2094,9 +2125,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -2104,13 +2135,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -2946,7 +2970,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001737",
|
"caniuse-lite": "^1.0.30001737",
|
||||||
"electron-to-chromium": "^1.5.211",
|
"electron-to-chromium": "^1.5.211",
|
||||||
@@ -3236,6 +3259,15 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"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": {
|
"node_modules/co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@@ -3390,6 +3422,16 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -3478,13 +3520,6 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -5464,7 +5499,6 @@
|
|||||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssstyle": "^4.2.1",
|
"cssstyle": "^4.2.1",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@@ -5579,16 +5613,6 @@
|
|||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.18",
|
"version": "0.30.18",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
||||||
@@ -6284,34 +6308,6 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -6414,38 +6410,27 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react-datepicker": {
|
||||||
"version": "19.1.1",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz",
|
||||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
"integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"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": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"@floating-ui/react": "^0.27.15",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -6619,13 +6604,6 @@
|
|||||||
"node": ">=v12.22.7"
|
"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": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -7130,6 +7108,12 @@
|
|||||||
"url": "https://opencollective.com/synckit"
|
"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": {
|
"node_modules/test-exclude": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
@@ -7246,7 +7230,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -7531,7 +7514,6 @@
|
|||||||
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
|
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -7648,7 +7630,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"framer-motion": "^12.23.22",
|
"framer-motion": "^12.23.22",
|
||||||
"node-cron": "^4.2.1"
|
"node-cron": "^4.2.1",
|
||||||
|
"react-datepicker": "^9.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -26,12 +26,12 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 80,
|
port: 90,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
allowedHosts: ['mygta.ensup-adm.net', 'localhost'],
|
allowedHosts: ['mygta-dev.ensup-adm.net', 'localhost'],
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://gta-backend:3000',
|
target: 'http://backend:3004',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
configure: (proxy, options) => {
|
configure: (proxy, options) => {
|
||||||
@@ -39,7 +39,7 @@ export default defineConfig({
|
|||||||
console.log('Proxy error:', err);
|
console.log('Proxy error:', err);
|
||||||
});
|
});
|
||||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
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
|
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"]
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ COPY . .
|
|||||||
RUN mkdir -p /app/uploads/medical
|
RUN mkdir -p /app/uploads/medical
|
||||||
|
|
||||||
# Expose the port
|
# Expose the port
|
||||||
EXPOSE 3000
|
EXPOSE 3004
|
||||||
|
|
||||||
# Start the server
|
# Start the server
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "gta-backend",
|
"name": "gta-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "GTA Backend API",
|
"description": "GTA Backend API",
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"mysql2": "^3.6.5",
|
"mssql": "^10.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"node-cron": "^3.0.3"
|
"node-cron": "^3.0.3"
|
||||||
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import mysql from 'mysql2/promise';
|
import sql from 'mssql';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
@@ -8,117 +9,562 @@ const PORT = 3000;
|
|||||||
app.use(cors({ origin: '*' }));
|
app.use(cors({ origin: '*' }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Configuration de connexion MySQL
|
// Configuration Azure AD
|
||||||
const dbConfig = {
|
const AZURE_CONFIG = {
|
||||||
host: '192.168.0.4',
|
tenantId: '9840a2a0-6ae1-4688-b03d-d2ec291be0f9',
|
||||||
user: 'wpuser',
|
clientId: '4bb4cc24-bac3-427c-b02c-5d14fc67b561',
|
||||||
password: '-2b/)ru5/Bi8P[7_',
|
clientSecret: 'gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t',
|
||||||
database: 'DemandeConge',
|
groupId: 'c1ea877c-6bca-4f47-bfad-f223640813a0'
|
||||||
port: 3306,
|
|
||||||
charset: 'utf8mb4',
|
|
||||||
connectTimeout: 60000,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ CRÉER LE POOL ICI, AU NIVEAU GLOBAL
|
// Configuration SQL Server
|
||||||
const pool = mysql.createPool(dbConfig);
|
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) => {
|
app.get('/api/db-status', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT COUNT(*) AS count FROM CollaborateurAD');
|
const result = await pool.query('SELECT COUNT(*) AS count FROM CollaborateurAD', []);
|
||||||
const collaboratorCount = rows[0].count;
|
const collaboratorCount = result[0]?.count || 0;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Connexion à la base OK',
|
message: 'Connexion SQL Server OK',
|
||||||
collaboratorCount,
|
collaboratorCount,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur connexion base:', error);
|
console.error('Erreur connexion:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Erreur de connexion à la base',
|
message: 'Erreur connexion base',
|
||||||
error: error.message,
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route sync unitaire
|
||||||
app.post('/api/initial-sync', async (req, res) => {
|
app.post('/api/initial-sync', async (req, res) => {
|
||||||
let conn;
|
|
||||||
try {
|
try {
|
||||||
conn = await pool.getConnection();
|
const email = (req.body.mail || req.body.userPrincipalName)?.toLowerCase().trim();
|
||||||
const email = req.body.mail || req.body.userPrincipalName;
|
const entraUserId = req.body.id;
|
||||||
const entraId = req.body.id;
|
|
||||||
console.log('🔄 Initial Sync pour:', email);
|
|
||||||
|
|
||||||
// 1. Chercher user
|
if (!email) {
|
||||||
const [users] = await conn.query('SELECT * FROM CollaborateurAD WHERE email = ?', [email]);
|
return res.json({ success: false, message: 'Email manquant' });
|
||||||
let userId;
|
}
|
||||||
let userRole;
|
|
||||||
|
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) {
|
if (users.length > 0) {
|
||||||
// UPDATE
|
const user = users[0];
|
||||||
userId = users[0].id;
|
|
||||||
userRole = users[0].role;
|
|
||||||
await conn.query('UPDATE CollaborateurAD SET entraUserId = ?, DerniereConnexion = NOW() WHERE id = ?', [entraId, userId]);
|
|
||||||
console.log('✅ User mis à jour:', userId);
|
|
||||||
} else {
|
|
||||||
// INSERT
|
|
||||||
const [resInsert] = await conn.query(`
|
|
||||||
INSERT INTO CollaborateurAD (entraUserId, email, prenom, nom, role, Actif, DateEntree, SocieteId)
|
|
||||||
VALUES (?, ?, ?, ?, 'Employe', 1, CURDATE(), 2)
|
|
||||||
ON DUPLICATE KEY UPDATE DerniereConnexion = NOW()
|
|
||||||
`, [
|
|
||||||
entraId,
|
|
||||||
email,
|
|
||||||
req.body.givenName || '',
|
|
||||||
req.body.surname || ''
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (resInsert.insertId === 0) {
|
if (user.actif === 0) {
|
||||||
const [u] = await conn.query('SELECT id, role FROM CollaborateurAD WHERE email = ?', [email]);
|
return res.json({ authorized: false, message: 'Compte désactivé' });
|
||||||
userId = u[0].id;
|
|
||||||
userRole = u[0].role;
|
|
||||||
} else {
|
|
||||||
userId = resInsert.insertId;
|
|
||||||
userRole = 'Employe';
|
|
||||||
}
|
}
|
||||||
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
localUserId: userId,
|
database: {
|
||||||
role: userRole
|
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) {
|
} catch (error) {
|
||||||
console.error('❌ Erreur check-user-groups:', error);
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
authorized: false,
|
success: false,
|
||||||
error: error.message
|
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
@@ -39,7 +39,7 @@ function AppContent() {
|
|||||||
<Route
|
<Route
|
||||||
path="/demandes"
|
path="/demandes"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'RH', 'Admin']}>
|
<ProtectedRoute allowedRoles={['Validateur', 'Validatrice', 'Collaborateur', 'Collaboratrice', 'Apprenti', 'RH', 'Admin', 'Directeur de campus', 'Directrice de campus']}>
|
||||||
<Requests />
|
<Requests />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ function AppContent() {
|
|||||||
<Route
|
<Route
|
||||||
path="/compte-rendu-activites"
|
path="/compte-rendu-activites"
|
||||||
element={
|
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 />
|
<CompteRenduActivites />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,46 @@
|
|||||||
// authConfig.js
|
// authConfig.js
|
||||||
|
|
||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
const protocol = window.location.protocol;
|
const protocol = window.location.protocol;
|
||||||
|
|
||||||
// Détection environnements (utile pour le debug)
|
// Détection environnements
|
||||||
const isProduction = hostname === "mygta.ensup-adm.net";
|
const isProduction = hostname === "mygta.ensup-adm.net";
|
||||||
|
|
||||||
// --- API URL ---
|
// ✅ EXPORT : API URL
|
||||||
// On utilise TOUJOURS /api car le proxy Vite (port 80) va rediriger vers le backend (port 3000)
|
|
||||||
// Cela évite les problèmes CORS et les problèmes de ports fermés (8000)
|
|
||||||
export const API_BASE_URL = "/api";
|
export const API_BASE_URL = "/api";
|
||||||
|
|
||||||
// --- MSAL Config ---
|
// ✅ EXPORT : MSAL Config - OPTIMISÉ POUR MOBILE iOS
|
||||||
export const msalConfig = {
|
export const msalConfig = {
|
||||||
auth: {
|
auth: {
|
||||||
clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561",
|
clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561",
|
||||||
authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9",
|
authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9",
|
||||||
|
redirectUri: isProduction
|
||||||
// En prod, on force l'URL sans slash final pour être propre
|
? "https://mygta.ensup-adm.net"
|
||||||
redirectUri: isProduction
|
: `${protocol}//${hostname}`,
|
||||||
? "https://mygta.ensup-adm.net"
|
navigateToLoginRequestUrl: false, // ✅ false pour éviter double redirection
|
||||||
|
postLogoutRedirectUri: isProduction
|
||||||
|
? "https://mygta.ensup-adm.net"
|
||||||
: `${protocol}//${hostname}`,
|
: `${protocol}//${hostname}`,
|
||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
cacheLocation: "sessionStorage",
|
cacheLocation: "localStorage",
|
||||||
storeAuthStateInCookie: false,
|
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 = {
|
export const loginRequest = {
|
||||||
scopes: [
|
scopes: [
|
||||||
"User.Read",
|
"User.Read",
|
||||||
@@ -37,11 +49,14 @@ export const loginRequest = {
|
|||||||
"GroupMember.Read.All",
|
"GroupMember.Read.All",
|
||||||
"Mail.Send",
|
"Mail.Send",
|
||||||
],
|
],
|
||||||
|
prompt: "select_account",
|
||||||
|
responseMode: "fragment",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ Log de configuration au démarrage
|
||||||
console.log("🔧 Config Auth:", {
|
console.log("🔧 Config Auth:", {
|
||||||
hostname,
|
hostname,
|
||||||
protocol,
|
protocol,
|
||||||
API_BASE_URL,
|
API_BASE_URL,
|
||||||
redirectUri: msalConfig.auth.redirectUri,
|
redirectUri: msalConfig.auth.redirectUri
|
||||||
});
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -149,7 +149,7 @@ const GlobalTutorial = ({ userId, userRole }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: '[data-tour="cp-n-1"]',
|
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',
|
placement: 'top',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -164,7 +164,7 @@ const GlobalTutorial = ({ userId, userRole }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: '[data-tour="recup"]',
|
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',
|
placement: 'top',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { X, Upload, AlertCircle } from 'lucide-react';
|
import { X, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
const NewLeaveRequestModal = ({
|
const NewLeaveRequestModal = ({
|
||||||
onClose,
|
onClose,
|
||||||
@@ -17,22 +17,82 @@ const NewLeaveRequestModal = ({
|
|||||||
types: preselectedType ? [preselectedType] : [],
|
types: preselectedType ? [preselectedType] : [],
|
||||||
startDate: preselectedStartDate || '',
|
startDate: preselectedStartDate || '',
|
||||||
endDate: preselectedEndDate || '',
|
endDate: preselectedEndDate || '',
|
||||||
reason: '',
|
reason: ''
|
||||||
medicalDocuments: []
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [repartition, setRepartition] = useState({});
|
const [repartition, setRepartition] = useState({});
|
||||||
const [periodeSelection, setPeriodeSelection] = useState({});
|
const [periodeSelection, setPeriodeSelection] = useState({});
|
||||||
const [totalDays, setTotalDays] = useState(0);
|
const [totalDays, setTotalDays] = useState(0);
|
||||||
const [saturdayCount, setSaturdayCount] = useState(0);
|
const [saturdayCount, setSaturdayCount] = useState(0);
|
||||||
|
const [holidayCount, setHolidayCount] = useState(0);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
// ⭐ État pour les données des compteurs
|
|
||||||
const [countersData, setCountersData] = useState(null);
|
const [countersData, setCountersData] = useState(null);
|
||||||
const [isLoadingCounters, setIsLoadingCounters] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchCounters = async () => {
|
const fetchCounters = async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
@@ -60,7 +120,6 @@ const NewLeaveRequestModal = ({
|
|||||||
fetchCounters();
|
fetchCounters();
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
// ⭐ Utiliser les données des compteurs
|
|
||||||
const safeCounters = countersData ? {
|
const safeCounters = countersData ? {
|
||||||
availableCP: parseFloat(countersData.data?.totalDisponible?.cp || 0),
|
availableCP: parseFloat(countersData.data?.totalDisponible?.cp || 0),
|
||||||
availableRTT: parseFloat(countersData.data?.totalDisponible?.rtt || 0),
|
availableRTT: parseFloat(countersData.data?.totalDisponible?.rtt || 0),
|
||||||
@@ -85,35 +144,45 @@ const NewLeaveRequestModal = ({
|
|||||||
}
|
}
|
||||||
}, [preselectedStartDate, preselectedEndDate, preselectedType]);
|
}, [preselectedStartDate, preselectedEndDate, preselectedType]);
|
||||||
|
|
||||||
|
// ⭐ Calcul des jours ouvrés en excluant les jours fériés
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.startDate && formData.endDate) {
|
if (formData.startDate && formData.endDate) {
|
||||||
const start = new Date(formData.startDate);
|
const start = new Date(formData.startDate);
|
||||||
const end = new Date(formData.endDate);
|
const end = new Date(formData.endDate);
|
||||||
let workingDays = 0;
|
let workingDays = 0;
|
||||||
let saturdays = 0;
|
let saturdays = 0;
|
||||||
|
let holidays = 0;
|
||||||
|
|
||||||
const current = new Date(start);
|
const current = new Date(start);
|
||||||
while (current <= end) {
|
while (current <= end) {
|
||||||
const dayOfWeek = current.getDay();
|
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++;
|
saturdays++;
|
||||||
} else if (dayOfWeek !== 0) {
|
} else if (dayOfWeek !== 0) {
|
||||||
workingDays++;
|
workingDays++;
|
||||||
}
|
}
|
||||||
|
|
||||||
current.setDate(current.getDate() + 1);
|
current.setDate(current.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalDays(workingDays);
|
setTotalDays(workingDays);
|
||||||
setSaturdayCount(saturdays);
|
setSaturdayCount(saturdays);
|
||||||
|
setHolidayCount(holidays);
|
||||||
|
|
||||||
console.log('📊 Calcul période:', {
|
console.log('📊 Calcul période:', {
|
||||||
debut: formData.startDate,
|
debut: formData.startDate,
|
||||||
fin: formData.endDate,
|
fin: formData.endDate,
|
||||||
joursOuvres: workingDays,
|
joursOuvres: workingDays,
|
||||||
samedis: saturdays
|
samedis: saturdays,
|
||||||
|
joursFeries: holidays
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [formData.startDate, formData.endDate]);
|
}, [formData.startDate, formData.endDate, publicHolidays]);
|
||||||
|
|
||||||
const getMinDate = () => {
|
const getMinDate = () => {
|
||||||
const tomorrow = new Date();
|
const tomorrow = new Date();
|
||||||
@@ -121,6 +190,13 @@ const NewLeaveRequestModal = ({
|
|||||||
return tomorrow.toISOString().split('T')[0];
|
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(() => {
|
useEffect(() => {
|
||||||
if (formData.types.length > 0) {
|
if (formData.types.length > 0) {
|
||||||
const newRepartition = {};
|
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 = () => {
|
const validateForm = () => {
|
||||||
console.log('\n🔍 === VALIDATION FORMULAIRE ===');
|
console.log('\n🔍 === VALIDATION FORMULAIRE ===');
|
||||||
|
|
||||||
// 🔥 AJOUTER CES LOGS
|
|
||||||
console.log('📊 countersData:', countersData);
|
console.log('📊 countersData:', countersData);
|
||||||
console.log('📊 countersData.success:', countersData?.success);
|
console.log('📊 countersData.success:', countersData?.success);
|
||||||
console.log('📊 countersData.data:', countersData?.data);
|
console.log('📊 countersData.data:', countersData?.data);
|
||||||
@@ -211,7 +245,6 @@ const NewLeaveRequestModal = ({
|
|||||||
console.log('📊 totalDisponible.cp:', countersData?.data?.totalDisponible?.cp);
|
console.log('📊 totalDisponible.cp:', countersData?.data?.totalDisponible?.cp);
|
||||||
console.log('📊 safeCounters:', safeCounters);
|
console.log('📊 safeCounters:', safeCounters);
|
||||||
|
|
||||||
// Vérifications de base
|
|
||||||
if (formData.types.length === 0) {
|
if (formData.types.length === 0) {
|
||||||
setError('Veuillez sélectionner au moins un type de congé');
|
setError('Veuillez sélectionner au moins un type de congé');
|
||||||
return false;
|
return false;
|
||||||
@@ -227,6 +260,17 @@ const NewLeaveRequestModal = ({
|
|||||||
const start = new Date(formData.startDate);
|
const start = new Date(formData.startDate);
|
||||||
const end = new Date(formData.endDate);
|
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()) {
|
if (start.getTime() === today.getTime() || end.getTime() === today.getTime()) {
|
||||||
setError("Vous ne pouvez pas poser un congé pour aujourd'hui.");
|
setError("Vous ne pouvez pas poser un congé pour aujourd'hui.");
|
||||||
return false;
|
return false;
|
||||||
@@ -242,27 +286,12 @@ const NewLeaveRequestModal = ({
|
|||||||
return false;
|
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) {
|
if (!countersData || !countersData.data || !countersData.data.totalDisponible) {
|
||||||
console.error('❌ Données compteurs non disponibles pour validation !');
|
console.error('❌ Données compteurs non disponibles pour validation !');
|
||||||
setError('Erreur : Les compteurs ne sont pas chargés. Veuillez réessayer.');
|
setError('Erreur : Les compteurs ne sont pas chargés. Veuillez réessayer.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculer les jours demandés par type
|
|
||||||
const joursDemandesParType = {};
|
const joursDemandesParType = {};
|
||||||
|
|
||||||
if (formData.types.length === 1) {
|
if (formData.types.length === 1) {
|
||||||
@@ -283,7 +312,6 @@ const NewLeaveRequestModal = ({
|
|||||||
console.log('📊 Jours demandés:', joursDemandesParType);
|
console.log('📊 Jours demandés:', joursDemandesParType);
|
||||||
console.log('📊 Soldes disponibles:', safeCounters);
|
console.log('📊 Soldes disponibles:', safeCounters);
|
||||||
|
|
||||||
// Vérifier CP
|
|
||||||
if (joursDemandesParType['CP'] > 0) {
|
if (joursDemandesParType['CP'] > 0) {
|
||||||
const cpDemande = joursDemandesParType['CP'];
|
const cpDemande = joursDemandesParType['CP'];
|
||||||
const cpDisponible = safeCounters.availableCP;
|
const cpDisponible = safeCounters.availableCP;
|
||||||
@@ -296,7 +324,6 @@ const NewLeaveRequestModal = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier RTT
|
|
||||||
if (joursDemandesParType['RTT'] > 0) {
|
if (joursDemandesParType['RTT'] > 0) {
|
||||||
const rttDemande = joursDemandesParType['RTT'];
|
const rttDemande = joursDemandesParType['RTT'];
|
||||||
const rttDisponible = safeCounters.availableRTT;
|
const rttDisponible = safeCounters.availableRTT;
|
||||||
@@ -309,7 +336,6 @@ const NewLeaveRequestModal = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier Récup
|
|
||||||
if (joursDemandesParType['Récup'] > 0) {
|
if (joursDemandesParType['Récup'] > 0) {
|
||||||
const recupDemande = joursDemandesParType['Récup'];
|
const recupDemande = joursDemandesParType['Récup'];
|
||||||
const recupDisponible = safeCounters.availableRecup;
|
const recupDisponible = safeCounters.availableRecup;
|
||||||
@@ -326,8 +352,6 @@ const NewLeaveRequestModal = ({
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
@@ -395,10 +419,6 @@ const NewLeaveRequestModal = ({
|
|||||||
|
|
||||||
formDataToSend.append('Repartition', JSON.stringify(repartitionArray));
|
formDataToSend.append('Repartition', JSON.stringify(repartitionArray));
|
||||||
|
|
||||||
formData.medicalDocuments.forEach((file) => {
|
|
||||||
formDataToSend.append('medicalDocuments', file);
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch('/api/submitLeaveRequest', {
|
const response = await fetch('/api/submitLeaveRequest', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formDataToSend
|
body: formDataToSend
|
||||||
@@ -431,43 +451,15 @@ const NewLeaveRequestModal = ({
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTypeDisabled = (typeKey) => {
|
// ⭐ Ajout de Formation en bleu pour les apprentis
|
||||||
const hasABS = formData.types.includes('ABS');
|
|
||||||
const hasOtherTypes = formData.types.some(t => t !== 'ABS');
|
|
||||||
|
|
||||||
if (hasABS && typeKey !== 'ABS') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasOtherTypes && typeKey === 'ABS') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDisabledTooltip = (typeKey) => {
|
|
||||||
if (formData.types.includes('ABS') && typeKey !== 'ABS') {
|
|
||||||
return '⚠️ Un arrêt maladie ne peut pas être mélangé avec d\'autres types';
|
|
||||||
}
|
|
||||||
if (formData.types.some(t => t !== 'ABS') && typeKey === 'ABS') {
|
|
||||||
return '⚠️ Un arrêt maladie ne peut pas être mélangé avec d\'autres types';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// ⭐ Inclure les détails des compteurs dans availableTypes
|
|
||||||
// ⭐ Inclure les détails des compteurs dans availableTypes
|
|
||||||
const availableTypes = userRole === 'Apprenti'
|
const availableTypes = userRole === 'Apprenti'
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'CP',
|
key: 'CP',
|
||||||
label: 'Congé(s) payé(s)',
|
label: 'Congé(s) payé(s)',
|
||||||
// ✅ Afficher seulement le solde actuel (sans anticipé)
|
|
||||||
available: countersData?.data?.cpN?.solde || 0,
|
available: countersData?.data?.cpN?.solde || 0,
|
||||||
details: countersData?.data?.cpN
|
details: countersData?.data?.cpN
|
||||||
},
|
},
|
||||||
{ key: 'ABS', label: 'Arrêt maladie' },
|
|
||||||
{ key: 'Formation', label: 'Formation' },
|
{ key: 'Formation', label: 'Formation' },
|
||||||
{
|
{
|
||||||
key: 'Récup',
|
key: 'Récup',
|
||||||
@@ -479,14 +471,12 @@ const NewLeaveRequestModal = ({
|
|||||||
{
|
{
|
||||||
key: 'CP',
|
key: 'CP',
|
||||||
label: 'Congé(s) payé(s)',
|
label: 'Congé(s) payé(s)',
|
||||||
// ✅ Afficher seulement le solde actuel (sans anticipé)
|
|
||||||
available: countersData?.data?.cpN?.solde || 0,
|
available: countersData?.data?.cpN?.solde || 0,
|
||||||
details: countersData?.data?.cpN
|
details: countersData?.data?.cpN
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'RTT',
|
key: 'RTT',
|
||||||
label: 'RTT',
|
label: 'RTT',
|
||||||
// ✅ Afficher seulement le solde actuel (sans anticipé)
|
|
||||||
available: countersData?.data?.rttN?.solde || 0,
|
available: countersData?.data?.rttN?.solde || 0,
|
||||||
details: countersData?.data?.rttN
|
details: countersData?.data?.rttN
|
||||||
},
|
},
|
||||||
@@ -494,11 +484,9 @@ const NewLeaveRequestModal = ({
|
|||||||
key: 'Récup',
|
key: 'Récup',
|
||||||
label: 'Récupération',
|
label: 'Récupération',
|
||||||
available: countersData?.data?.recupN?.solde || 0
|
available: countersData?.data?.recupN?.solde || 0
|
||||||
},
|
}
|
||||||
{ key: 'ABS', label: 'Arrêt maladie' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<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">
|
<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>
|
||||||
|
|
||||||
<div className="p-6 space-y-5">
|
<div className="p-6 space-y-5">
|
||||||
{/* ⭐ BLOC SOLDES DÉTAILLÉS */}
|
|
||||||
|
|
||||||
|
|
||||||
{/* Loading */}
|
|
||||||
{isLoadingCounters && (
|
{isLoadingCounters && (
|
||||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 text-center">
|
<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>
|
<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>
|
</label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{availableTypes.map(type => {
|
{availableTypes.map(type => {
|
||||||
const disabled = isTypeDisabled(type.key);
|
|
||||||
const tooltip = getDisabledTooltip(type.key);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={type.key}
|
key={type.key}
|
||||||
className={`flex items-center gap-3 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
className="flex items-center gap-3 cursor-pointer"
|
||||||
title={tooltip}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.types.includes(type.key)}
|
checked={formData.types.includes(type.key)}
|
||||||
onChange={() => handleTypeToggle(type.key)}
|
onChange={() => handleTypeToggle(type.key)}
|
||||||
disabled={disabled}
|
className="w-4 h-4 rounded border-gray-300"
|
||||||
className={`w-4 h-4 rounded border-gray-300 ${disabled ? 'cursor-not-allowed' : ''}`}
|
|
||||||
/>
|
/>
|
||||||
<span className={`px-2.5 py-1 rounded text-sm font-medium ${type.key === 'CP' ? 'bg-blue-100 text-blue-800' :
|
<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 === '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'
|
'bg-purple-100 text-purple-800'
|
||||||
}`}>
|
}`}>
|
||||||
{type.label}
|
{type.label}
|
||||||
@@ -570,17 +549,18 @@ const NewLeaveRequestModal = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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>
|
</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 className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-900 mb-2">
|
<label className="block text-sm font-medium text-gray-900 mb-2">
|
||||||
@@ -589,10 +569,32 @@ const NewLeaveRequestModal = ({
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.startDate}
|
value={formData.startDate}
|
||||||
min={getMinDate()}
|
min={formData.types.includes('Formation') ? getMinDateFormation() : getMinDate()}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, startDate: e.target.value }))}
|
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"
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-900 mb-2">
|
<label className="block text-sm font-medium text-gray-900 mb-2">
|
||||||
@@ -601,13 +603,52 @@ const NewLeaveRequestModal = ({
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.endDate}
|
value={formData.endDate}
|
||||||
min={getMinDate()}
|
min={formData.startDate || (formData.types.includes('Formation') ? getMinDateFormation() : getMinDate())}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, endDate: e.target.value }))}
|
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"
|
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>
|
||||||
</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]) && (
|
{formData.types.length === 1 && ['CP', 'RTT', 'Récup'].includes(formData.types[0]) && (
|
||||||
<div className="border-t border-gray-200 pt-4">
|
<div className="border-t border-gray-200 pt-4">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
@@ -659,7 +700,7 @@ const NewLeaveRequestModal = ({
|
|||||||
Répartition des {totalDays} jours ouvrés
|
Répartition des {totalDays} jours ouvrés
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-500 mb-4">
|
<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>
|
</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{formData.types.map((type) => {
|
{formData.types.map((type) => {
|
||||||
@@ -751,76 +792,6 @@ const NewLeaveRequestModal = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{error && (
|
||||||
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
<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" />
|
<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;
|
||||||
@@ -1,22 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }) => {
|
const ProtectedRoute = ({ children, allowedRoles = [] }) => {
|
||||||
const { user, isLoading } = useAuth();
|
const { isAuthorized, user, isLoading } = useAuth();
|
||||||
|
|
||||||
if (isLoading) {
|
// ✅ FIX MOBILE : Attendre la fin du chargement avant de rediriger
|
||||||
return (
|
if (isLoading) {
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
return (
|
||||||
<div className="text-center">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
<div className="text-center">
|
||||||
<p className="text-gray-600">Chargement...</p>
|
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-cyan-600 mx-auto mb-4"></div>
|
||||||
</div>
|
<p className="text-gray-600 font-medium">Chargement en cours...</p>
|
||||||
</div>
|
</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;
|
export default ProtectedRoute;
|
||||||
@@ -134,8 +134,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
data-tour="dashboard"
|
data-tour="dashboard"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/dashboard")
|
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"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
: "text-gray-700 hover:bg-gray-50"
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Home className="w-5 h-5" />
|
<Home className="w-5 h-5" />
|
||||||
@@ -147,8 +147,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
data-tour="demandes"
|
data-tour="demandes"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/demandes")
|
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"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
: "text-gray-700 hover:bg-gray-50"
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FileText className="w-5 h-5" />
|
<FileText className="w-5 h-5" />
|
||||||
@@ -160,8 +160,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
data-tour="calendrier"
|
data-tour="calendrier"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/calendrier")
|
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"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
: "text-gray-700 hover:bg-gray-50"
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Calendar className="w-5 h-5" />
|
<Calendar className="w-5 h-5" />
|
||||||
@@ -175,12 +175,12 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
data-tour="compte-rendu"
|
data-tour="compte-rendu"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/compte-rendu-activites")
|
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"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
: "text-gray-700 hover:bg-gray-50"
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Clock className="w-5 h-5" />
|
<Clock className="w-5 h-5" />
|
||||||
<span className="font-medium">Compte-Rendu</span>
|
<span className="font-medium">CRA</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -190,8 +190,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
data-tour="mon-equipe"
|
data-tour="mon-equipe"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(teamPath)
|
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"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
: "text-gray-700 hover:bg-gray-50"
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Users className="w-5 h-5" />
|
<Users className="w-5 h-5" />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import * as msal from '@azure/msal-browser';
|
import { useMsal } from '@azure/msal-react';
|
||||||
// ✅ Correction: Import de API_BASE_URL
|
import { loginRequest, API_BASE_URL } from '../authConfig';
|
||||||
import { msalConfig, loginRequest, API_BASE_URL } from '../authConfig';
|
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
@@ -11,19 +10,28 @@ export const useAuth = () => {
|
|||||||
return context;
|
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 }) => {
|
export const AuthProvider = ({ children }) => {
|
||||||
|
const { instance, accounts, inProgress } = useMsal();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [userGroups, setUserGroups] = useState([]);
|
const [userGroups, setUserGroups] = useState([]);
|
||||||
const [isAuthorized, setIsAuthorized] = useState(false);
|
const [isAuthorized, setIsAuthorized] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isMsalInitialized, setIsMsalInitialized] = useState(false);
|
|
||||||
|
|
||||||
// ✅ Fonction corrigée pour construire l'URL
|
|
||||||
const getApiUrl = (endpoint) => {
|
const getApiUrl = (endpoint) => {
|
||||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
||||||
// API_BASE_URL est "/api", donc cela retourne "/api/endpoint"
|
|
||||||
return `${API_BASE_URL}/${cleanEndpoint}`;
|
return `${API_BASE_URL}/${cleanEndpoint}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,11 +74,11 @@ export const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Utilisateur synchronisé:', entraUser.userPrincipalName);
|
console.log('✅ Utilisateur synchronisé:', entraUser.userPrincipalName);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur synchronisation utilisateur:', error);
|
console.error('❌ Erreur synchronisation utilisateur:', error);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -85,63 +93,26 @@ export const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Full sync terminée:', data);
|
console.log('✅ Full sync terminée:', data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur full sync:', error);
|
console.error('❌ Erreur full sync:', error);
|
||||||
}
|
}
|
||||||
return null;
|
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
|
// --- Gestion login réussi
|
||||||
const handleSuccessfulAuth = async (authResponse) => {
|
const handleSuccessfulAuth = async (authResponse) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('🔐 Traitement authentification réussie...');
|
||||||
const account = authResponse.account;
|
const account = authResponse.account;
|
||||||
const accessToken = authResponse.accessToken;
|
const accessToken = authResponse.accessToken;
|
||||||
|
|
||||||
|
if (!account || !accessToken) {
|
||||||
|
throw new Error('Données d\'authentification incomplètes');
|
||||||
|
}
|
||||||
|
|
||||||
let entraUser = {
|
let entraUser = {
|
||||||
id: account.homeAccountId,
|
id: account.homeAccountId,
|
||||||
displayName: account.name,
|
displayName: account.name,
|
||||||
@@ -149,29 +120,39 @@ export const AuthProvider = ({ children }) => {
|
|||||||
mail: account.username
|
mail: account.username
|
||||||
};
|
};
|
||||||
|
|
||||||
const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
|
// Appel Graph API pour enrichir les données
|
||||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
console.log('📞 Appel Graph API...');
|
||||||
});
|
try {
|
||||||
|
const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
|
||||||
|
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
if (graphResponse.ok) {
|
if (graphResponse.ok) {
|
||||||
const graphData = await graphResponse.json();
|
const graphData = await graphResponse.json();
|
||||||
entraUser = { ...entraUser, ...graphData };
|
entraUser = { ...entraUser, ...graphData };
|
||||||
|
console.log('✅ Données Graph récupérées');
|
||||||
|
}
|
||||||
|
} catch (graphError) {
|
||||||
|
console.warn('⚠️ Erreur Graph API:', graphError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1️⃣ Synchroniser l’utilisateur connecté
|
// Synchronisation utilisateur
|
||||||
|
console.log('🔄 Synchronisation utilisateur...');
|
||||||
const syncResult = await syncUserToDatabase(entraUser, accessToken);
|
const syncResult = await syncUserToDatabase(entraUser, accessToken);
|
||||||
|
|
||||||
// 2️⃣ Full sync si admin
|
|
||||||
if (syncResult?.role === 'Admin') {
|
if (syncResult?.role === 'Admin') {
|
||||||
console.log('Admin détecté → lancement full sync...');
|
console.log('👑 Admin détecté → lancement full sync...');
|
||||||
await fullSyncDatabase(accessToken);
|
await fullSyncDatabase(accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3️⃣ Vérifier groupes
|
// Vérification des groupes
|
||||||
|
console.log('🔍 Vérification groupes...');
|
||||||
const authResult = await checkUserAuthorization(entraUser.userPrincipalName, accessToken);
|
const authResult = await checkUserAuthorization(entraUser.userPrincipalName, accessToken);
|
||||||
|
|
||||||
if (authResult.authorized) {
|
if (authResult.authorized) {
|
||||||
setUser({
|
console.log('✅ Utilisateur autorisé');
|
||||||
|
|
||||||
|
const userData = {
|
||||||
id: syncResult?.localUserId || entraUser.id,
|
id: syncResult?.localUserId || entraUser.id,
|
||||||
CollaborateurADId: syncResult?.localUserId,
|
CollaborateurADId: syncResult?.localUserId,
|
||||||
entraUserId: entraUser.id,
|
entraUserId: entraUser.id,
|
||||||
@@ -188,17 +169,95 @@ export const AuthProvider = ({ children }) => {
|
|||||||
typeContrat: syncResult?.typeContrat || '37h',
|
typeContrat: syncResult?.typeContrat || '37h',
|
||||||
dateEntree: syncResult?.dateEntree || null,
|
dateEntree: syncResult?.dateEntree || null,
|
||||||
groups: authResult.groups
|
groups: authResult.groups
|
||||||
});
|
};
|
||||||
|
|
||||||
|
setUser(userData);
|
||||||
setIsAuthorized(true);
|
setIsAuthorized(true);
|
||||||
|
console.log('✅ Connexion réussie:', userData.email);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
console.error('❌ Utilisateur non autorisé');
|
||||||
throw new Error('Utilisateur non autorisé - pas membre des groupes requis');
|
throw new Error('Utilisateur non autorisé - pas membre des groupes requis');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la gestion de l\'authentification:', error);
|
console.error('❌ Erreur handleSuccessfulAuth:', error);
|
||||||
throw 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
|
// --- Connexion classique
|
||||||
const login = async (email, password) => {
|
const login = async (email, password) => {
|
||||||
try {
|
try {
|
||||||
@@ -234,15 +293,28 @@ export const AuthProvider = ({ children }) => {
|
|||||||
// --- Connexion Office 365
|
// --- Connexion Office 365
|
||||||
const loginWithO365 = async () => {
|
const loginWithO365 = async () => {
|
||||||
try {
|
try {
|
||||||
await ensureMsalInitialized();
|
const useRedirect = shouldUseRedirect();
|
||||||
const authResponse = await msalInstance.loginPopup(loginRequest);
|
console.log(`🔐 Connexion O365: ${useRedirect ? 'REDIRECT' : 'POPUP'}`);
|
||||||
await handleSuccessfulAuth(authResponse);
|
|
||||||
return true;
|
if (useRedirect) {
|
||||||
} catch (error) {
|
await instance.loginRedirect(loginRequest);
|
||||||
console.error('Erreur login Office 365:', error);
|
} else {
|
||||||
if (error.message?.includes('non autorisé')) {
|
try {
|
||||||
throw new Error('Accès refusé: Vous n\'êtes pas membre d\'un groupe autorisé.');
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -250,12 +322,23 @@ export const AuthProvider = ({ children }) => {
|
|||||||
// --- Déconnexion
|
// --- Déconnexion
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
const accounts = msalInstance.getAllAccounts();
|
const useRedirect = shouldUseRedirect();
|
||||||
|
|
||||||
if (accounts.length > 0) {
|
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) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la déconnexion:', error);
|
console.error('Erreur déconnexion:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setUserGroups([]);
|
setUserGroups([]);
|
||||||
@@ -266,11 +349,11 @@ export const AuthProvider = ({ children }) => {
|
|||||||
// --- Obtenir token API
|
// --- Obtenir token API
|
||||||
const getAccessToken = async () => {
|
const getAccessToken = async () => {
|
||||||
try {
|
try {
|
||||||
await ensureMsalInitialized();
|
if (accounts.length === 0) {
|
||||||
const accounts = msalInstance.getAllAccounts();
|
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,
|
...loginRequest,
|
||||||
account: accounts[0]
|
account: accounts[0]
|
||||||
});
|
});
|
||||||
@@ -278,6 +361,19 @@ export const AuthProvider = ({ children }) => {
|
|||||||
return response.accessToken;
|
return response.accessToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur obtention token:', 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;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -296,4 +392,4 @@ export const AuthProvider = ({ children }) => {
|
|||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthContext;
|
export default AuthContext;
|
||||||
@@ -3,15 +3,121 @@ import { createRoot } from 'react-dom/client';
|
|||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import { MsalProvider } from "@azure/msal-react";
|
import { MsalProvider } from "@azure/msal-react";
|
||||||
import { PublicClientApplication } from "@azure/msal-browser";
|
import { PublicClientApplication, EventType } from "@azure/msal-browser";
|
||||||
import { msalConfig } from "./authConfig";
|
import { msalConfig } from "./authConfig";
|
||||||
|
|
||||||
|
// ✅ CRITIQUE : Créer l'instance MSAL
|
||||||
const msalInstance = new PublicClientApplication(msalConfig);
|
const msalInstance = new PublicClientApplication(msalConfig);
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
// ✅ CRITIQUE : Fonction d'initialisation asynchrone
|
||||||
<StrictMode>
|
async function initializeApp() {
|
||||||
<MsalProvider instance={msalInstance}>
|
console.log('🚀 Initialisation de l\'application...');
|
||||||
<App />
|
console.log('🔗 Hash actuel:', window.location.hash);
|
||||||
</MsalProvider>
|
console.log('📍 URL complète:', window.location.href);
|
||||||
</StrictMode>
|
|
||||||
);
|
// ✅ 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();
|
||||||
@@ -51,6 +51,9 @@ const Calendar = () => {
|
|||||||
|
|
||||||
const [initialFiltersSet, setInitialFiltersSet] = useState(false);
|
const [initialFiltersSet, setInitialFiltersSet] = useState(false);
|
||||||
|
|
||||||
|
// ⭐ Liste des employés à exclure de l'affichage
|
||||||
|
const EXCLUDED_EMPLOYEES = ['Kevin Lambert'];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
setIsMobile(window.innerWidth < 1024);
|
setIsMobile(window.innerWidth < 1024);
|
||||||
@@ -66,7 +69,7 @@ const Calendar = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
|
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(() => {
|
useEffect(() => {
|
||||||
if (accounts.length > 0) {
|
if (accounts.length > 0) {
|
||||||
@@ -152,9 +155,9 @@ const Calendar = () => {
|
|||||||
|
|
||||||
if (role === 'president' || role === 'rh' || role === 'admin' ||
|
if (role === 'president' || role === 'rh' || role === 'admin' ||
|
||||||
role === 'directeur de campus' || role === 'directrice de campus' ||
|
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 += `&selectedSociete=${encodeURIComponent(selectedSociete || 'all')}`;
|
||||||
url += `&selectedCampus=${encodeURIComponent(selectedCampus || 'all')}`;
|
url += `&selectedCampus=${encodeURIComponent(selectedCampus || 'all')}`;
|
||||||
url += `&selectedService=${encodeURIComponent(selectedService || 'all')}`;
|
url += `&selectedService=${encodeURIComponent(selectedService || 'all')}`;
|
||||||
@@ -171,8 +174,21 @@ const Calendar = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTeamLeaves(data.leaves || []);
|
// ⭐ Filtrer les congés des employés exclus
|
||||||
setFilters(data.filters || {});
|
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) {
|
} catch (error) {
|
||||||
console.error('Erreur récupération congés équipe:', error);
|
console.error('Erreur récupération congés équipe:', error);
|
||||||
@@ -279,7 +295,6 @@ const Calendar = () => {
|
|||||||
}
|
}
|
||||||
}, [filters.defaultCampus, isFirstLoad, role]);
|
}, [filters.defaultCampus, isFirstLoad, role]);
|
||||||
|
|
||||||
// ⭐ Initialisation des filtres par défaut pour collaborateur/apprenti (UNE SEULE FOIS)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialFiltersSet &&
|
if (!initialFiltersSet &&
|
||||||
filters.defaultCampus &&
|
filters.defaultCampus &&
|
||||||
@@ -300,11 +315,32 @@ const Calendar = () => {
|
|||||||
}
|
}
|
||||||
}, [filters.defaultCampus, filters.defaultService, filters.defaultSociete, initialFiltersSet, role]);
|
}, [filters.defaultCampus, filters.defaultService, filters.defaultSociete, initialFiltersSet, role]);
|
||||||
|
|
||||||
// ⭐ Rechargement quand les filtres changent (TOUJOURS)
|
|
||||||
useEffect(() => {
|
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 === '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:", {
|
console.log("🔄 Rechargement données:", {
|
||||||
societe: selectedSociete,
|
societe: selectedSociete,
|
||||||
@@ -357,22 +393,16 @@ const Calendar = () => {
|
|||||||
|
|
||||||
for (let day = 1; day <= daysInMonth; day++) {
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
const currentDay = new Date(year, month, day);
|
const currentDay = new Date(year, month, day);
|
||||||
const dayOfWeek = currentDay.getDay();
|
days.push(currentDay);
|
||||||
|
|
||||||
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
|
|
||||||
days.push(currentDay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return days;
|
return days;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ SIMPLIFIÉ : Le backend filtre déjà
|
|
||||||
const getBaseFilteredLeaves = () => {
|
const getBaseFilteredLeaves = () => {
|
||||||
return teamLeaves;
|
return teamLeaves;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ SIMPLIFIÉ : Pas de cas spécial pour collaborateur
|
|
||||||
const getAllEmployees = () => {
|
const getAllEmployees = () => {
|
||||||
if (!filters.employees || filters.employees.length === 0) {
|
if (!filters.employees || filters.employees.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -385,12 +415,12 @@ const Calendar = () => {
|
|||||||
service: emp.service || ''
|
service: emp.service || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Dédoublonner
|
|
||||||
const uniqueEmployees = [];
|
const uniqueEmployees = [];
|
||||||
const seenNames = new Set();
|
const seenNames = new Set();
|
||||||
|
|
||||||
for (const emp of employeeList) {
|
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);
|
seenNames.add(emp.name);
|
||||||
uniqueEmployees.push(emp);
|
uniqueEmployees.push(emp);
|
||||||
}
|
}
|
||||||
@@ -408,15 +438,16 @@ const Calendar = () => {
|
|||||||
filteredEmployees = allEmployees.filter(emp => selectedEmployees.includes(emp.name));
|
filteredEmployees = allEmployees.filter(emp => selectedEmployees.includes(emp.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⭐ Exclure "president" de la condition de tri automatique
|
||||||
const shouldAutoSort = (
|
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' &&
|
selectedCampus === 'all' &&
|
||||||
selectedSociete === 'all' &&
|
selectedSociete === 'all' &&
|
||||||
selectedService === 'all'
|
selectedService === 'all'
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldSortByService = (
|
const shouldSortByService = (
|
||||||
['collaborateur', 'collaboratrice', 'apprenti'].includes(role) &&
|
['collaborateur', 'collaboratrice', 'apprenti', 'validateur', 'validatrice'].includes(role) &&
|
||||||
selectedService === 'all'
|
selectedService === 'all'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -478,7 +509,7 @@ const Calendar = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSortByService) {
|
if (shouldSortByService) {
|
||||||
console.log("🔄 Tri par service pour collaborateur");
|
console.log("🔄 Tri par service pour collaborateur/validateur");
|
||||||
return filteredEmployees.sort((a, b) => {
|
return filteredEmployees.sort((a, b) => {
|
||||||
const serviceCompare = (a.service || '').localeCompare(b.service || '');
|
const serviceCompare = (a.service || '').localeCompare(b.service || '');
|
||||||
if (serviceCompare !== 0) return serviceCompare;
|
if (serviceCompare !== 0) return serviceCompare;
|
||||||
@@ -497,7 +528,6 @@ const Calendar = () => {
|
|||||||
return baseFiltered.filter(leave => displayedNames.includes(leave.employeename));
|
return baseFiltered.filter(leave => displayedNames.includes(leave.employeename));
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ useMemo simplifié
|
|
||||||
const allEmployeesData = useMemo(() => {
|
const allEmployeesData = useMemo(() => {
|
||||||
return getAllEmployees();
|
return getAllEmployees();
|
||||||
}, [filters.employees]);
|
}, [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: '' };
|
if (!leave) return { tailwindClass: '', hexColor: '' };
|
||||||
|
|
||||||
const status = leave.statut?.toLowerCase();
|
const status = leave.statut?.toLowerCase();
|
||||||
const type = leave.type?.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') {
|
if (status === 'en attente' || status === 'pending' || status === 'en attente de validation') {
|
||||||
return { tailwindClass: 'bg-orange-400', hexColor: '#fb923c' };
|
return { tailwindClass: 'bg-orange-400', hexColor: '#fb923c' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type) {
|
// 🟢 PRIORITÉ 4 : Si validé, on applique les couleurs selon le 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' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.includes('formation')) {
|
// JPO/SF NORMAL (non RH) = MAGENTA aussi
|
||||||
return { tailwindClass: 'bg-blue-400', hexColor: '#60a5fa' };
|
if (isJPOSF) {
|
||||||
}
|
return { tailwindClass: '', hexColor: '#d946ef' };
|
||||||
|
|
||||||
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' };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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' };
|
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)
|
? (detailedCounters.cpN?.solde || 0) + (detailedCounters.cpN1?.solde || 0) + (detailedCounters.rttN?.solde || 0)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const canViewAllFilters = ['president', 'rh', 'admin', 'directeur de campus', 'directrice de campus', 'collaborateur', 'collaboratrice', 'apprenti'].includes(role);
|
// ⭐ Exclure "president" de la liste des rôles autorisés pour les filtres
|
||||||
const canViewCampusFilters = ['president', 'rh', 'admin', 'directeur de campus', 'directrice de campus'].includes(role);
|
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 = [
|
const activeFiltersCount = [
|
||||||
employeeFilter !== "all" ? employeeFilter : null,
|
employeeFilter !== "all" ? employeeFilter : null,
|
||||||
@@ -784,7 +852,7 @@ const Calendar = () => {
|
|||||||
].filter(Boolean).length;
|
].filter(Boolean).length;
|
||||||
|
|
||||||
const resetToDefaultFilters = () => {
|
const resetToDefaultFilters = () => {
|
||||||
if (['collaborateur', 'collaboratrice', 'apprenti'].includes(role)) {
|
if (['collaborateur', 'collaboratrice', 'apprenti', 'validateur', 'validatrice'].includes(role)) {
|
||||||
const userEmployee = filters.employees?.find(emp =>
|
const userEmployee = filters.employees?.find(emp =>
|
||||||
emp.name === `${user.prenom} ${user.nom}`
|
emp.name === `${user.prenom} ${user.nom}`
|
||||||
);
|
);
|
||||||
@@ -802,10 +870,10 @@ const Calendar = () => {
|
|||||||
setSelectedEmployees([]);
|
setSelectedEmployees([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLeaveCell = (leave) => {
|
const renderLeaveCell = (leave, date = null) => {
|
||||||
if (!leave) return null;
|
if (!leave) return null;
|
||||||
|
|
||||||
const colorObj = getLeaveColor(leave);
|
const colorObj = getLeaveColor(leave, date);
|
||||||
const bgColor = colorObj.hexColor || '#4ade80';
|
const bgColor = colorObj.hexColor || '#4ade80';
|
||||||
|
|
||||||
let details;
|
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 (
|
return (
|
||||||
<div className="flex h-screen bg-gray-50" onMouseMove={handleMouseMove}>
|
<div className="flex h-screen bg-gray-50" onMouseMove={handleMouseMove}>
|
||||||
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
||||||
@@ -954,7 +1042,7 @@ const Calendar = () => {
|
|||||||
onClick={resetToDefaultFilters}
|
onClick={resetToDefaultFilters}
|
||||||
className="text-sm text-blue-600 hover:text-blue-700"
|
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 aux valeurs par défaut'
|
||||||
: 'Réinitialiser les filtres'
|
: 'Réinitialiser les filtres'
|
||||||
}
|
}
|
||||||
@@ -999,7 +1087,7 @@ const Calendar = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Campus filter */}
|
{/* Campus filter */}
|
||||||
{filters.campus && filters.campus.length > 0 && (
|
{canViewCampusFilters && filters.campus && filters.campus.length > 0 && (
|
||||||
<div data-tour="filtre-campus">
|
<div data-tour="filtre-campus">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Campus
|
Campus
|
||||||
@@ -1127,7 +1215,7 @@ const Calendar = () => {
|
|||||||
{canViewCampusFilters && employee.societe && (
|
{canViewCampusFilters && employee.societe && (
|
||||||
<> • {employee.societe}</>
|
<> • {employee.societe}</>
|
||||||
)}
|
)}
|
||||||
{(role === 'president' || role === 'rh' || role === 'admin') && employee.campus && (
|
{(role === 'rh' || role === 'admin') && employee.campus && (
|
||||||
<> • {employee.campus}</>
|
<> • {employee.campus}</>
|
||||||
)}
|
)}
|
||||||
{(role === 'directeur de campus' || role === 'directrice de campus') && employee.societe === 'Ensup Solution & Support' && employee.campus !== 'N/A' && (
|
{(role === 'directeur de campus' || role === 'directrice de campus') && employee.societe === 'Ensup Solution & Support' && employee.campus !== 'N/A' && (
|
||||||
@@ -1147,7 +1235,7 @@ const Calendar = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Calendar */}
|
{/* CALENDRIER DESKTOP */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
|
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -1243,7 +1331,7 @@ const Calendar = () => {
|
|||||||
{canViewCampusFilters && employee.societe && (
|
{canViewCampusFilters && employee.societe && (
|
||||||
<> • {employee.societe}</>
|
<> • {employee.societe}</>
|
||||||
)}
|
)}
|
||||||
{(role === 'president' || role === 'rh' || role === 'admin') && employee.campus && (
|
{(role === 'rh' || role === 'admin') && employee.campus && (
|
||||||
<> • {employee.campus}</>
|
<> • {employee.campus}</>
|
||||||
)}
|
)}
|
||||||
{(role === 'directeur de campus' || role === 'directrice de campus') && employee.societe === 'Ensup Solution & Support' && employee.campus !== 'N/A' && (
|
{(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 saturday = isSaturday(date);
|
||||||
const sunday = isSunday(date);
|
const sunday = isSunday(date);
|
||||||
const isToday = date.toDateString() === new Date().toDateString();
|
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;
|
let details, hasMatin = false, hasApresMidi = false;
|
||||||
if (weekendAlreadyHasLeave) {
|
if (leave) {
|
||||||
try {
|
try {
|
||||||
if (typeof leave.detailsconges === 'string') {
|
if (typeof leave.detailsconges === 'string') {
|
||||||
details = JSON.parse(leave.detailsconges);
|
details = JSON.parse(leave.detailsconges);
|
||||||
} else if (Array.isArray(leave.detailsconges)) {
|
} else if (Array.isArray(leave.detailsconges)) {
|
||||||
details = leave.detailsconges;
|
details = leave.detailsconges;
|
||||||
}
|
}
|
||||||
hasMatin = details.some(d => d.periode === 'Matin');
|
hasMatin = details?.some(d => d.periode === 'Matin');
|
||||||
hasApresMidi = details.some(d => d.periode === 'Après-midi');
|
hasApresMidi = details?.some(d => d.periode === 'Après-midi');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Erreur parsing:', e);
|
console.error('Erreur parsing:', e);
|
||||||
}
|
}
|
||||||
@@ -1283,60 +1379,69 @@ const Calendar = () => {
|
|||||||
inRange ? 'bg-blue-100' :
|
inRange ? 'bg-blue-100' :
|
||||||
past ? 'bg-gray-50 opacity-60' :
|
past ? 'bg-gray-50 opacity-60' :
|
||||||
holiday ? 'bg-gray-600' :
|
holiday ? 'bg-gray-600' :
|
||||||
(sunday || saturday) && !weekendAlreadyHasLeave ? 'bg-gray-200' :
|
isWeekend && !isRecupWeekend ? 'bg-gray-200' : ''
|
||||||
weekendAlreadyHasLeave ? 'cursor-not-allowed' :
|
} ${leave || (employee.name === `${user.prenom} ${user.nom}` && !past && !holiday && !isWeekend)
|
||||||
''
|
? ''
|
||||||
|
: 'cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
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);
|
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 })}
|
onMouseEnter={() => !isMobile && leave && setHoveredLeave({ employee, leave, date })}
|
||||||
onMouseLeave={() => setHoveredLeave(null)}
|
onMouseLeave={() => setHoveredLeave(null)}
|
||||||
title={
|
title={
|
||||||
weekendAlreadyHasLeave
|
isRecupWeekend ? `${saturday ? 'Samedi' : 'Dimanche'} - Récupération` :
|
||||||
? `${saturday ? 'Samedi' : 'Dimanche'} saisi : ${leave?.type}${hasMatin ? ' - Matin' : ''}${hasApresMidi ? ' - Après-midi' : ''} - Non modifiable`
|
isHoliday(date) ? getHolidayName(date) :
|
||||||
: isHoliday(date)
|
sunday ? 'Dimanche - Non sélectionnable' :
|
||||||
? getHolidayName(date)
|
saturday ? 'Samedi - Non sélectionnable' :
|
||||||
: sunday
|
''
|
||||||
? 'Dimanche - Non sélectionnable'
|
|
||||||
: saturday
|
|
||||||
? 'Samedi - Non sélectionnable'
|
|
||||||
: ''
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{/* JOUR FÉRIÉ */}
|
||||||
{holiday ? (
|
{holiday ? (
|
||||||
<div className="h-6 w-6 mx-auto bg-gray-700 rounded" title={getHolidayName(date)}></div>
|
<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">
|
/* 🆕 RÉCUP SUR WEEKEND → MAGENTA */
|
||||||
{weekendAlreadyHasLeave ? (
|
) : 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>}
|
{hasMatin && (
|
||||||
{hasApresMidi && <div className="absolute bottom-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>}
|
<div className="absolute top-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>
|
||||||
{!hasMatin && !hasApresMidi && <div className="h-full w-full" style={{ backgroundColor: '#d946ef' }}></div>}
|
)}
|
||||||
</>
|
{hasApresMidi && (
|
||||||
) : (
|
<div className="absolute bottom-0 left-0 right-0 h-3" style={{ backgroundColor: '#d946ef' }}></div>
|
||||||
<div className="h-full w-full" style={{ backgroundColor: '#d1d5db' }}></div>
|
)}
|
||||||
|
{!hasMatin && !hasApresMidi && (
|
||||||
|
<div className="h-full w-full" style={{ backgroundColor: '#d946ef' }}></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : saturday ? (
|
|
||||||
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden cursor-not-allowed">
|
/* WEEKEND NORMAL SANS CONGÉ → GRIS */
|
||||||
{weekendAlreadyHasLeave ? (
|
) : isWeekend && !leave ? (
|
||||||
<>
|
<div className="h-6 w-6 mx-auto bg-gray-300 rounded"></div>
|
||||||
{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>}
|
/* WEEKEND AVEC AUTRE CONGÉ → GRIS AUSSI */
|
||||||
{!hasMatin && !hasApresMidi && <div className="h-full w-full" style={{ backgroundColor: '#d946ef' }}></div>}
|
) : isWeekend && leave ? (
|
||||||
</>
|
<div className="h-6 w-6 mx-auto bg-gray-300 rounded"
|
||||||
) : (
|
title={`${saturday ? 'Samedi' : 'Dimanche'} - Congé non modifiable`}>
|
||||||
<div className="h-full w-full" style={{ backgroundColor: '#d1d5db' }}></div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
/* CONGÉ NORMAL EN SEMAINE */
|
||||||
) : leave ? (
|
) : leave ? (
|
||||||
renderLeaveCell(leave)
|
renderLeaveCell(leave, date)
|
||||||
|
|
||||||
|
/* CELLULE VIDE CLIQUABLE */
|
||||||
) : employee.name === `${user.prenom} ${user.nom}` && !past && !holiday ? (
|
) : 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>
|
<div className="h-6 w-6 mx-auto bg-gray-100 rounded"></div>
|
||||||
)}
|
)}
|
||||||
@@ -1371,7 +1476,7 @@ const Calendar = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-gray-400 rounded"></div>
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 bg-gray-700 rounded"></div>
|
<div className="w-3 h-3 bg-gray-700 rounded"></div>
|
||||||
@@ -1385,7 +1490,129 @@ const Calendar = () => {
|
|||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1439,7 +1666,7 @@ const Calendar = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hover tooltip */}
|
{/* Tooltip (désactivé, ne montre plus le type) */}
|
||||||
{hoveredLeave && !isMobile && (
|
{hoveredLeave && !isMobile && (
|
||||||
<div
|
<div
|
||||||
className="fixed bg-white rounded-lg shadow-xl border p-4 z-50 min-w-[250px]"
|
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,
|
top: mousePosition.y + 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Tooltip content - TO BE COMPLETED */}
|
|
||||||
<div className="text-sm font-semibold">{hoveredLeave.employee.name}</div>
|
<div className="text-sm font-semibold">{hoveredLeave.employee.name}</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">{hoveredLeave.leave.type}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1523,4 +1748,4 @@ const Calendar = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Calendar;
|
export default Calendar;
|
||||||
@@ -6,7 +6,7 @@ import { Users, CheckCircle, XCircle, Clock, Calendar, FileText, Menu, Eye, Mess
|
|||||||
const Collaborateur = () => {
|
const Collaborateur = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const isEmployee = user?.role === 'Collaborateur'||'Apprenti';
|
const isEmployee = user?.role === 'Collaborateur' || 'Apprenti';
|
||||||
const [teamMembers, setTeamMembers] = useState([]);
|
const [teamMembers, setTeamMembers] = useState([]);
|
||||||
const [pendingRequests, setPendingRequests] = useState([]);
|
const [pendingRequests, setPendingRequests] = useState([]);
|
||||||
const [allRequests, setAllRequests] = 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">
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
||||||
{isEmployee ? 'Mon équipe 👥' : 'Gestion d\'équipe 👥'}
|
{isEmployee ? 'Mon équipe 👥' : 'Gestion d\'équipe 👥'}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
@@ -222,9 +222,9 @@ const Collaborateur = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
|
|||||||
@@ -110,7 +110,15 @@ const CompteRenduActivites = () => {
|
|||||||
const congesData = await congesResponse.json();
|
const congesData = await congesResponse.json();
|
||||||
|
|
||||||
if (congesData.success) {
|
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) {
|
} catch (error) {
|
||||||
@@ -118,7 +126,7 @@ const CompteRenduActivites = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [userId, annee, mois, user?.role]);
|
}, [userId, annee, mois, user?.role, user?.prenom, user?.nom]);
|
||||||
|
|
||||||
// Charger les stats annuelles
|
// Charger les stats annuelles
|
||||||
const loadStatsAnnuelles = useCallback(async () => {
|
const loadStatsAnnuelles = useCallback(async () => {
|
||||||
@@ -163,7 +171,7 @@ const CompteRenduActivites = () => {
|
|||||||
return selectedYear === previousYear && selectedMonth === previousMonth;
|
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 getDaysInMonth = () => {
|
||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
const month = currentDate.getMonth();
|
const month = currentDate.getMonth();
|
||||||
@@ -174,7 +182,7 @@ const CompteRenduActivites = () => {
|
|||||||
// Jour de la semaine du 1er (0=dimanche, 1=lundi, ..., 6=samedi)
|
// Jour de la semaine du 1er (0=dimanche, 1=lundi, ..., 6=samedi)
|
||||||
let firstDayOfWeek = firstDay.getDay();
|
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;
|
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
|
||||||
|
|
||||||
const days = [];
|
const days = [];
|
||||||
@@ -184,15 +192,9 @@ const CompteRenduActivites = () => {
|
|||||||
days.push(null);
|
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++) {
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
const currentDay = new Date(year, month, day);
|
days.push(new Date(year, month, day));
|
||||||
const dayOfWeek = currentDay.getDay();
|
|
||||||
|
|
||||||
// Exclure les dimanches (0)
|
|
||||||
if (dayOfWeek !== 0) {
|
|
||||||
days.push(currentDay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return days;
|
return days;
|
||||||
@@ -201,31 +203,47 @@ const CompteRenduActivites = () => {
|
|||||||
const getJourData = (date) => {
|
const getJourData = (date) => {
|
||||||
const dateStr = formatDateToString(date);
|
const dateStr = formatDateToString(date);
|
||||||
const found = joursActifs.find(j => {
|
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;
|
let jourDateStr = j.JourDate;
|
||||||
if (j.JourDate instanceof Date) {
|
if (j.JourDate instanceof Date) {
|
||||||
jourDateStr = formatDateToString(j.JourDate);
|
jourDateStr = formatDateToString(j.JourDate);
|
||||||
} else if (typeof j.JourDate === 'string') {
|
} 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];
|
jourDateStr = j.JourDate.split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = jourDateStr === dateStr;
|
const match = jourDateStr === dateStr;
|
||||||
console.log('Comparaison:', jourDateStr, 'vs', dateStr, 'match:', match);
|
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
if (found) {
|
|
||||||
console.log('✅ Jour trouvé:', dateStr, found);
|
|
||||||
}
|
|
||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ⭐ Détection des congés avec logs
|
||||||
const isJourEnConge = (date) => {
|
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 start = new Date(conge.startdate);
|
||||||
const end = new Date(conge.enddate);
|
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)
|
// Vérifier si le jour est STRICTEMENT dans le passé (pas aujourd'hui)
|
||||||
@@ -263,11 +281,18 @@ const CompteRenduActivites = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⭐ Bloquer les week-ends
|
||||||
|
if (isWeekend(date)) {
|
||||||
|
showInfo('Les samedis et dimanches ne peuvent pas être saisis', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isHoliday(date)) {
|
if (isHoliday(date)) {
|
||||||
showInfo(`Jour férié : ${getHolidayName(date)} - Saisie impossible`, 'info');
|
showInfo(`Jour férié : ${getHolidayName(date)} - Saisie impossible`, 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⭐ Bloquer les congés
|
||||||
if (isJourEnConge(date)) {
|
if (isJourEnConge(date)) {
|
||||||
showInfo('Vous êtes en congé ce jour - Saisie impossible', 'info');
|
showInfo('Vous êtes en congé ce jour - Saisie impossible', 'info');
|
||||||
return;
|
return;
|
||||||
@@ -292,10 +317,25 @@ const CompteRenduActivites = () => {
|
|||||||
|
|
||||||
// Sauvegarder un jour
|
// Sauvegarder un jour
|
||||||
const handleSaveJour = async () => {
|
const handleSaveJour = async () => {
|
||||||
if ((!selectedJour.reposQuotidien || !selectedJour.reposHebdo)) {
|
// ⭐ Vérifier uniquement le repos quotidien pour lundi-jeudi
|
||||||
if (!selectedJour.commentaire || selectedJour.commentaire.trim() === '') {
|
// Pour vendredi, vérifier les deux
|
||||||
showInfo('Commentaire obligatoire en cas de non-respect des repos', 'warning');
|
const isVendredi = selectedJour.date.getDay() === 5;
|
||||||
return;
|
|
||||||
|
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,
|
date: selectedJour.dateStr,
|
||||||
jour_travaille: selectedJour.jourTravaille,
|
jour_travaille: selectedJour.jourTravaille,
|
||||||
repos_quotidien: selectedJour.reposQuotidien,
|
repos_quotidien: selectedJour.reposQuotidien,
|
||||||
repos_hebdo: selectedJour.reposHebdo,
|
repos_hebdo: isVendredi ? selectedJour.reposHebdo : true, // true par défaut pour lundi-jeudi
|
||||||
commentaire: selectedJour.commentaire,
|
commentaire: selectedJour.commentaire,
|
||||||
rh_override: isRH
|
rh_override: isRH
|
||||||
})
|
})
|
||||||
@@ -323,7 +363,6 @@ const CompteRenduActivites = () => {
|
|||||||
await loadStatsAnnuelles();
|
await loadStatsAnnuelles();
|
||||||
setShowSaisieModal(false);
|
setShowSaisieModal(false);
|
||||||
showInfo('✅ Jour enregistré', 'success');
|
showInfo('✅ Jour enregistré', 'success');
|
||||||
console.log('Données rechargées après sauvegarde');
|
|
||||||
} else {
|
} else {
|
||||||
showInfo(data.message || 'Erreur lors de la sauvegarde', 'error');
|
showInfo(data.message || 'Erreur lors de la sauvegarde', 'error');
|
||||||
}
|
}
|
||||||
@@ -375,8 +414,6 @@ const CompteRenduActivites = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const formatDateToString = (date) => {
|
const formatDateToString = (date) => {
|
||||||
if (!date) return null;
|
if (!date) return null;
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
@@ -505,7 +542,6 @@ const CompteRenduActivites = () => {
|
|||||||
<p className="text-sm opacity-90">Jours travaillés</p>
|
<p className="text-sm opacity-90">Jours travaillés</p>
|
||||||
<p className="text-3xl font-bold">{statsAnnuelles.totalJoursTravailles || 0}</p>
|
<p className="text-3xl font-bold">{statsAnnuelles.totalJoursTravailles || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -562,23 +598,21 @@ const CompteRenduActivites = () => {
|
|||||||
<FileText className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
<span className="hidden sm:inline">Saisie en masse</span>
|
<span className="hidden sm:inline">Saisie en masse</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Calendrier */}
|
{/* ⭐ Calendrier 7 colonnes (Lun-Dim) */}
|
||||||
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
|
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
|
||||||
<div className="grid grid-cols-6 gap-2 p-4 bg-gray-50">
|
<div className="grid grid-cols-7 gap-2 p-4 bg-gray-50">
|
||||||
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'].map(day => (
|
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'].map(day => (
|
||||||
<div key={day} className="text-center font-semibold text-gray-700 text-sm">
|
<div key={day} className="text-center font-semibold text-gray-700 text-sm">
|
||||||
{day}
|
{day}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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) => {
|
{days.map((date, index) => {
|
||||||
// Case vide pour le décalage
|
// Case vide pour le décalage
|
||||||
if (date === null) {
|
if (date === null) {
|
||||||
@@ -589,10 +623,10 @@ const CompteRenduActivites = () => {
|
|||||||
|
|
||||||
const jourData = getJourData(date);
|
const jourData = getJourData(date);
|
||||||
const enConge = isJourEnConge(date);
|
const enConge = isJourEnConge(date);
|
||||||
|
const weekend = isWeekend(date);
|
||||||
const ferie = isHoliday(date);
|
const ferie = isHoliday(date);
|
||||||
const isPast = isPastOnly(date);
|
const isPast = isPastOnly(date);
|
||||||
const isToday = date.toDateString() === new Date().toDateString();
|
const isToday = date.toDateString() === new Date().toDateString();
|
||||||
const jourVerrouille = isJourVerrouille(date);
|
|
||||||
|
|
||||||
// Déterminer la classe de fond
|
// Déterminer la classe de fond
|
||||||
let bgClass = 'bg-white hover:bg-gray-50';
|
let bgClass = 'bg-white hover:bg-gray-50';
|
||||||
@@ -601,7 +635,12 @@ const CompteRenduActivites = () => {
|
|||||||
if (ferie) {
|
if (ferie) {
|
||||||
bgClass = 'bg-gray-700 text-white';
|
bgClass = 'bg-gray-700 text-white';
|
||||||
cursorClass = 'cursor-not-allowed';
|
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) {
|
} else if (enConge) {
|
||||||
|
// ⭐ Congés en violet et non cliquables
|
||||||
bgClass = 'bg-purple-100';
|
bgClass = 'bg-purple-100';
|
||||||
cursorClass = 'cursor-not-allowed';
|
cursorClass = 'cursor-not-allowed';
|
||||||
} else if (jourData) {
|
} else if (jourData) {
|
||||||
@@ -623,17 +662,20 @@ const CompteRenduActivites = () => {
|
|||||||
`}
|
`}
|
||||||
title={
|
title={
|
||||||
ferie ? getHolidayName(date) :
|
ferie ? getHolidayName(date) :
|
||||||
enConge ? 'En congé' :
|
weekend ? 'Week-end - Non saisissable' :
|
||||||
jourData ? 'Jour saisi - Cliquer pour modifier' :
|
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' :
|
<div className={`text-right text-sm font-semibold mb-2 flex items-center justify-end gap-1 ${ferie ? 'text-white' :
|
||||||
jourData ? 'text-gray-700' :
|
weekend ? 'text-gray-500' :
|
||||||
'text-gray-700'
|
enConge ? 'text-purple-700' :
|
||||||
|
jourData ? 'text-gray-700' :
|
||||||
|
'text-gray-700'
|
||||||
}`}>
|
}`}>
|
||||||
{date.getDate()}
|
{date.getDate()}
|
||||||
{jourData && !ferie && !enConge && (
|
{jourData && !ferie && !enConge && !weekend && (
|
||||||
<Lock className="w-3 h-3 text-gray-600" />
|
<Lock className="w-3 h-3 text-gray-600" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -642,6 +684,10 @@ const CompteRenduActivites = () => {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-xs text-white font-bold truncate">{getHolidayName(date)}</div>
|
<div className="text-xs text-white font-bold truncate">{getHolidayName(date)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : weekend ? (
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs text-gray-500 font-semibold">Week-end</div>
|
||||||
|
</div>
|
||||||
) : enConge ? (
|
) : enConge ? (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-xs text-purple-700 font-semibold">En congé</div>
|
<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" />
|
<Lock className="w-3 h-3 text-gray-600" />
|
||||||
<span>Jour saisi (grisé)</span>
|
<span>Jour saisi (grisé)</span>
|
||||||
</div>
|
</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="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 bg-purple-100 border-2 border-gray-200 rounded"></div>
|
<div className="w-4 h-4 bg-purple-100 border-2 border-gray-200 rounded"></div>
|
||||||
<span>En congé</span>
|
<span>En congé</span>
|
||||||
@@ -725,8 +775,6 @@ const CompteRenduActivites = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
||||||
|
|
||||||
{selectedJour.jourTravaille && (
|
{selectedJour.jourTravaille && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
@@ -748,26 +796,29 @@ const CompteRenduActivites = () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* ⭐ Afficher repos hebdomadaire uniquement le vendredi */}
|
||||||
<label className="flex items-start gap-3 cursor-pointer">
|
{selectedJour.date.getDay() === 5 && (
|
||||||
<input
|
<div>
|
||||||
type="checkbox"
|
<label className="flex items-start gap-3 cursor-pointer">
|
||||||
checked={selectedJour.reposHebdo}
|
<input
|
||||||
onChange={(e) => setSelectedJour({ ...selectedJour, reposHebdo: e.target.checked })}
|
type="checkbox"
|
||||||
className="w-5 h-5 text-blue-600 rounded mt-0.5"
|
checked={selectedJour.reposHebdo}
|
||||||
/>
|
onChange={(e) => setSelectedJour({ ...selectedJour, reposHebdo: e.target.checked })}
|
||||||
<div>
|
className="w-5 h-5 text-blue-600 rounded mt-0.5"
|
||||||
<span className="text-gray-700 font-medium block">
|
/>
|
||||||
Respect du repos hebdomadaire
|
<div>
|
||||||
</span>
|
<span className="text-gray-700 font-medium block">
|
||||||
<span className="text-xs text-gray-500">
|
Respect du repos hebdomadaire
|
||||||
35 heures consécutives minimum (24h + 11h)
|
</span>
|
||||||
</span>
|
<span className="text-xs text-gray-500">
|
||||||
</div>
|
35 heures consécutives minimum (24h + 11h)
|
||||||
</label>
|
</span>
|
||||||
</div>
|
</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">
|
<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">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Commentaire obligatoire
|
Commentaire obligatoire
|
||||||
@@ -823,7 +874,7 @@ const CompteRenduActivites = () => {
|
|||||||
<SaisieMasseModal
|
<SaisieMasseModal
|
||||||
mois={mois}
|
mois={mois}
|
||||||
annee={annee}
|
annee={annee}
|
||||||
days={days.filter(d => d !== null)} // Filtrer les cases vides
|
days={days.filter(d => d !== null)}
|
||||||
congesData={congesData}
|
congesData={congesData}
|
||||||
holidays={holidays}
|
holidays={holidays}
|
||||||
onClose={() => setShowSaisieMasse(false)}
|
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 SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, onSave, isSaving }) => {
|
||||||
const [selectedDays, setSelectedDays] = useState([]);
|
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'];
|
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||||
|
|
||||||
const isJourEnConge = (date) => {
|
const isJourEnConge = (date) => {
|
||||||
|
const checkDate = new Date(date);
|
||||||
|
checkDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
return congesData.some(conge => {
|
return congesData.some(conge => {
|
||||||
const start = new Date(conge.startdate);
|
const start = new Date(conge.startdate);
|
||||||
const end = new Date(conge.enddate);
|
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 isPastOnly = (date) => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
@@ -867,6 +932,11 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleDay = (date) => {
|
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);
|
const dateStr = formatDateToString(date);
|
||||||
if (selectedDays.includes(dateStr)) {
|
if (selectedDays.includes(dateStr)) {
|
||||||
setSelectedDays(selectedDays.filter(d => d !== dateStr));
|
setSelectedDays(selectedDays.filter(d => d !== dateStr));
|
||||||
@@ -876,8 +946,9 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectAllWorkingDays = () => {
|
const selectAllWorkingDays = () => {
|
||||||
|
// ⭐ Exclure week-ends, congés et jours fériés
|
||||||
const workingDays = days
|
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));
|
.map(date => formatDateToString(date));
|
||||||
|
|
||||||
setSelectedDays(workingDays);
|
setSelectedDays(workingDays);
|
||||||
@@ -902,7 +973,7 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
|||||||
onSave(joursTravailles);
|
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 getDaysWithOffset = () => {
|
||||||
const year = annee;
|
const year = annee;
|
||||||
const month = mois - 1;
|
const month = mois - 1;
|
||||||
@@ -918,7 +989,6 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
|||||||
daysWithOffset.push(null);
|
daysWithOffset.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter les jours réels
|
|
||||||
daysWithOffset.push(...days);
|
daysWithOffset.push(...days);
|
||||||
|
|
||||||
return daysWithOffset;
|
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">
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
||||||
<p className="text-sm text-blue-800">
|
<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.
|
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.
|
||||||
Les repos quotidien et hebdomadaire seront considérés comme respectés.
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -947,7 +1024,8 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
|||||||
Sélectionner tous les jours ouvrés disponibles
|
Sélectionner tous les jours ouvrés disponibles
|
||||||
</button>
|
</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) => {
|
{daysWithOffset.map((date, index) => {
|
||||||
// Case vide
|
// Case vide
|
||||||
if (date === null) {
|
if (date === null) {
|
||||||
@@ -959,26 +1037,33 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
|||||||
const dateStr = formatDateToString(date);
|
const dateStr = formatDateToString(date);
|
||||||
const enConge = isJourEnConge(date);
|
const enConge = isJourEnConge(date);
|
||||||
const ferie = isHoliday(date);
|
const ferie = isHoliday(date);
|
||||||
|
const weekend = isWeekend(date);
|
||||||
const isPast = isPastOnly(date);
|
const isPast = isPastOnly(date);
|
||||||
const isSelected = selectedDays.includes(dateStr);
|
const isSelected = selectedDays.includes(dateStr);
|
||||||
const isToday = date.toDateString() === new Date().toDateString();
|
const isToday = date.toDateString() === new Date().toDateString();
|
||||||
|
|
||||||
|
// ⭐ Les week-ends ne sont pas sélectionnables
|
||||||
|
const isDisabled = !isPast || weekend || enConge || ferie;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => !enConge && !ferie && isPast && toggleDay(date)}
|
onClick={() => toggleDay(date)}
|
||||||
className={`
|
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' : ''}
|
${isToday ? 'border-cyan-500 bg-cyan-100' : ''}
|
||||||
${ferie ? 'bg-gray-700 text-white cursor-not-allowed' : ''}
|
${ferie ? 'bg-gray-700 text-white cursor-not-allowed' : ''}
|
||||||
|
${weekend ? 'bg-gray-100 cursor-not-allowed' : ''}
|
||||||
${enConge ? 'bg-purple-100 cursor-not-allowed' : ''}
|
${enConge ? 'bg-purple-100 cursor-not-allowed' : ''}
|
||||||
${!isPast ? 'opacity-30 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>
|
<div className="font-semibold">{date.getDate()}</div>
|
||||||
{isToday && <div className="text-xs mt-1">Aujourd'hui</div>}
|
{isToday && <div className="text-xs mt-1">Aujourd'hui</div>}
|
||||||
{ferie && <div className="text-xs mt-1">Férié</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>}
|
{enConge && <div className="text-xs mt-1 text-purple-700">Congé</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,29 @@ import { useMsal } from "@azure/msal-react";
|
|||||||
import { loginRequest } from "../authConfig";
|
import { loginRequest } from "../authConfig";
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 Dashboard = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [graphToken, setGraphToken] = useState(null);
|
const [graphToken, setGraphToken] = useState(null);
|
||||||
@@ -27,7 +50,6 @@ const Dashboard = () => {
|
|||||||
const [recentRequests, setRecentRequests] = useState([]);
|
const [recentRequests, setRecentRequests] = useState([]);
|
||||||
const [teamLeaves, setTeamLeaves] = useState([]);
|
const [teamLeaves, setTeamLeaves] = useState([]);
|
||||||
const [isUpdatingCounters, setIsUpdatingCounters] = useState(false);
|
const [isUpdatingCounters, setIsUpdatingCounters] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const userId = user?.id || user?.CollaborateurADId || user?.ID;
|
const userId = user?.id || user?.CollaborateurADId || user?.ID;
|
||||||
|
|
||||||
@@ -188,7 +210,7 @@ const Dashboard = () => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const filteredLeaves = (data.leaves || []).filter(leave => {
|
let filteredLeaves = (data.leaves || []).filter(leave => {
|
||||||
const startDate = new Date(leave.startdate);
|
const startDate = new Date(leave.startdate);
|
||||||
const endDate = new Date(leave.enddate);
|
const endDate = new Date(leave.enddate);
|
||||||
const startMonth = startDate.getMonth();
|
const startMonth = startDate.getMonth();
|
||||||
@@ -201,6 +223,14 @@ const Dashboard = () => {
|
|||||||
(startDate <= new Date(currentYear, currentMonth, 1) &&
|
(startDate <= new Date(currentYear, currentMonth, 1) &&
|
||||||
endDate >= new Date(currentYear, currentMonth + 1, 0));
|
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);
|
setTeamLeaves(filteredLeaves);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -466,7 +496,26 @@ const Dashboard = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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)} />
|
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
<div className="flex-1 lg:ml-60">
|
<div className="flex-1 lg:ml-60">
|
||||||
<div className="p-4 lg:p-8 w-full">
|
<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 className="flex justify-between items-center mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2 christmas-title">
|
||||||
Bonjour, {user?.name || user?.prenom || 'Utilisateur'} 👋
|
Bonjour, {user?.name || user?.prenom || 'Utilisateur'} 👋🎄
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm lg:text-base text-gray-600">
|
<p className="text-sm lg:text-base text-gray-600">
|
||||||
Vos soldes de congés au {detailedCounters ? formatDate(detailedCounters.dateReference) : '...'}
|
Vos soldes de congés au {detailedCounters ? formatDate(detailedCounters.dateReference) : '...'}
|
||||||
@@ -804,7 +853,6 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* RACCOURCI COMPTE-RENDU ACTIVITÉS (si forfait jour) */}
|
{/* RACCOURCI COMPTE-RENDU ACTIVITÉS (si forfait jour) */}
|
||||||
{(user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfait_jour') && (
|
{(user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfait_jour') && (
|
||||||
<div
|
<div
|
||||||
@@ -869,7 +917,6 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{detailedCounters.cpN && (
|
{detailedCounters.cpN && (
|
||||||
<div data-tour="cp-n" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
<div data-tour="cp-n" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||||
{isUpdatingCounters && (
|
{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="bg-gradient-to-r from-purple-500 to-purple-600 p-4 text-white">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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>
|
</div>
|
||||||
<Calendar className="w-8 h-8 opacity-80" />
|
<Calendar className="w-8 h-8 opacity-80" />
|
||||||
</div>
|
</div>
|
||||||
@@ -977,7 +1024,7 @@ const Dashboard = () => {
|
|||||||
<span className="text-2xl font-bold text-purple-600">{detailedCounters.recupN.solde.toFixed(2)}j</span>
|
<span className="text-2xl font-bold text-purple-600">{detailedCounters.recupN.solde.toFixed(2)}j</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
<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>
|
<span className="text-lg font-bold text-green-600">+{detailedCounters.recupN.acquis.toFixed(2)}j</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
@@ -1037,7 +1084,136 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 🎄 STYLES NOËL */}
|
||||||
<style>{`
|
<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 {
|
@keyframes slideInRight {
|
||||||
from {
|
from {
|
||||||
transform: translateX(400px);
|
transform: translateX(400px);
|
||||||
@@ -1053,4 +1229,4 @@ const Dashboard = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
|||||||
@@ -1,37 +1,52 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
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 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 EmployeeDetails = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [employee, setEmployee] = useState(null);
|
const [employee, setEmployee] = useState(null);
|
||||||
const [requests, setRequests] = useState([]);
|
const [requests, setRequests] = useState([]);
|
||||||
|
const [detailedCounters, setDetailedCounters] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchEmployeeData();
|
fetchEmployeeData();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
// Dans EmployeeDetails.jsx, modifier fetchEmployeeData:
|
|
||||||
const fetchEmployeeData = async () => {
|
const fetchEmployeeData = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// 1️⃣ Données employé (avec compteurs inclus)
|
|
||||||
const resEmployee = await fetch(`/api/getEmploye?id=${id}`);
|
const resEmployee = await fetch(`/api/getEmploye?id=${id}`);
|
||||||
const dataEmployee = await resEmployee.json();
|
const dataEmployee = await resEmployee.json();
|
||||||
console.log("Réponse API employé:", dataEmployee);
|
|
||||||
|
|
||||||
if (!dataEmployee.success) {
|
if (!dataEmployee.success) {
|
||||||
setEmployee(null);
|
setEmployee(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Les compteurs sont déjà dans la réponse
|
|
||||||
setEmployee(dataEmployee.employee);
|
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 resRequests = await fetch(`/api/getEmployeRequest?id=${id}`);
|
||||||
const dataRequests = await resRequests.json();
|
const dataRequests = await resRequests.json();
|
||||||
|
|
||||||
@@ -46,64 +61,257 @@ const EmployeeDetails = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusIcon = (status) => {
|
const getStatusConfig = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'Validée':
|
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 'Refusée':
|
||||||
case 'Annulé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:
|
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>;
|
const getTypeContratLabel = (type) => {
|
||||||
if (!employee) return <p className="text-center p-6">Collaborateur introuvable</p>;
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
<div className="min-h-screen bg-gray-50 flex">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
<div className="flex-1 lg:ml-60 p-6">
|
<div className="flex-1 lg:ml-60 p-6 lg:p-8">
|
||||||
<h1 className="text-2xl font-bold mb-2">{employee.Prenom} {employee.Nom}</h1>
|
{/* Bouton retour */}
|
||||||
<p className="text-gray-600 mb-6">{employee.Email}</p>
|
<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 */}
|
{/* Profil employé */}
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 mb-6">
|
||||||
<div className="bg-white p-4 rounded-xl shadow">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
<p className="text-sm text-gray-600">Congés restants</p>
|
{/* Avatar */}
|
||||||
<p className="text-xl font-bold">{employee.conges_restants || 0} jours</p>
|
<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">
|
||||||
</div>
|
<span className="text-2xl font-bold text-white">
|
||||||
<div className="bg-white p-4 rounded-xl shadow">
|
{employee.Prenom?.charAt(0)}{employee.Nom?.charAt(0)}
|
||||||
<p className="text-sm text-gray-600">RTT restants</p>
|
</span>
|
||||||
<p className="text-xl font-bold">{employee.rtt_restants || 0} jours</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Historique des congés */}
|
{/* Compteurs */}
|
||||||
<h2 className="text-lg font-semibold mb-4">Historique des congés</h2>
|
{detailedCounters && (
|
||||||
<div className="space-y-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
{requests.length === 0 ? (
|
{detailedCounters.cpN1 && (
|
||||||
<p className="text-gray-500">Aucune demande</p>
|
<CounterCard
|
||||||
) : (
|
label="CP N-1"
|
||||||
requests.map((r) => (
|
solde={detailedCounters.cpN1.solde}
|
||||||
<div key={r.Id} className="bg-white p-4 rounded-xl shadow border flex justify-between items-center">
|
acquis={detailedCounters.cpN1.reporte}
|
||||||
<div>
|
pris={detailedCounters.cpN1.pris}
|
||||||
<p className="font-medium">{r.type} - {r.days}j</p>
|
color="blue"
|
||||||
<p className="text-sm text-gray-600">{r.date_display}</p>
|
icon={Calendar}
|
||||||
</div>
|
/>
|
||||||
<div className="flex items-center">
|
)}
|
||||||
{getStatusIcon(r.status)}
|
|
||||||
<span className="text-sm text-gray-700">{r.status}</span>
|
{detailedCounters.cpN && (
|
||||||
</div>
|
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EmployeeDetails;
|
export default EmployeeDetails;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
@@ -8,23 +8,39 @@ const Login = () => {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const navigate = useNavigate();
|
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 () => {
|
const handleO365Login = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
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) {
|
if (!success) {
|
||||||
setError("Erreur lors de la connexion Office 365");
|
setError("Erreur lors de la connexion Office 365");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate('/dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate('/dashboard');
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur O365:', 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.');
|
setError('Accès refusé : Vous devez être membre d\'un groupe autorisé dans votre organisation.');
|
||||||
} else if (error.message?.includes('AADSTS')) {
|
} else if (error.message?.includes('AADSTS')) {
|
||||||
setError('Erreur d\'authentification Azure AD. Contactez votre administrateur.');
|
setError('Erreur d\'authentification Azure AD. Contactez votre administrateur.');
|
||||||
|
} else if (error.errorCode === 'user_cancelled') {
|
||||||
|
setError('Connexion annulée');
|
||||||
} else {
|
} else {
|
||||||
setError(error.message || "Erreur lors de la connexion Office 365");
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex flex-col lg:flex-row">
|
<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 */}
|
{/* Image côté gauche */}
|
||||||
@@ -120,4 +150,4 @@ const Login = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
X,
|
X,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
@@ -31,7 +32,6 @@ const Manager = () => {
|
|||||||
const [comment, setComment] = useState("");
|
const [comment, setComment] = useState("");
|
||||||
const [isValidating, setIsValidating] = useState(false);
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.id) fetchTeamData();
|
if (user?.id) fetchTeamData();
|
||||||
}, [user]);
|
}, [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 () => {
|
const fetchTeamMembers = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/getTeamMembers?manager_id=${user.id}`);
|
const res = await fetch(`/api/getTeamMembers?manager_id=${user.id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) setTeamMembers(data.team_members || []);
|
|
||||||
else setTeamMembers([]);
|
console.log('📊 getTeamMembers:', {
|
||||||
} catch {
|
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([]);
|
setTeamMembers([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ SIMPLIFIÉ - Le backend gère tout le filtrage
|
||||||
const fetchPendingRequests = async () => {
|
const fetchPendingRequests = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/getPendingRequests?manager_id=${user.id}`);
|
const res = await fetch(`/api/getPendingRequests?manager_id=${user.id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) setPendingRequests(data.requests || []);
|
|
||||||
else setPendingRequests([]);
|
console.log('📊 getPendingRequests:', {
|
||||||
} catch {
|
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([]);
|
setPendingRequests([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ SIMPLIFIÉ - Le backend gère tout le filtrage
|
||||||
const fetchAllTeamRequests = async () => {
|
const fetchAllTeamRequests = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/getAllTeamRequests?SuperieurId=${user.id}`);
|
const res = await fetch(`/api/getAllTeamRequests?SuperieurId=${user.id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) setAllRequests(data.requests || []);
|
|
||||||
else setAllRequests([]);
|
console.log('📊 getAllTeamRequests:', {
|
||||||
} catch {
|
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([]);
|
setAllRequests([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -87,9 +129,11 @@ const Manager = () => {
|
|||||||
const openValidationModal = (request, action) => {
|
const openValidationModal = (request, action) => {
|
||||||
setValidationModal({ request, action });
|
setValidationModal({ request, action });
|
||||||
setComment("");
|
setComment("");
|
||||||
|
setIsValidating(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeValidationModal = () => {
|
const closeValidationModal = () => {
|
||||||
|
if (isValidating) return;
|
||||||
setValidationModal(null);
|
setValidationModal(null);
|
||||||
setComment("");
|
setComment("");
|
||||||
};
|
};
|
||||||
@@ -102,49 +146,48 @@ const Manager = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleValidateRequest(request.id, action, comment);
|
if (isValidating) return;
|
||||||
closeValidationModal();
|
|
||||||
|
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 = '') => {
|
const handleValidateRequest = async (requestId, action, comment = '') => {
|
||||||
if (!user || !user.id) {
|
if (!user || !user.id) {
|
||||||
alert('❌ Utilisateur non identifié');
|
throw new Error('Utilisateur non identifié');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const response = await fetch('/api/validateRequest', {
|
||||||
setIsValidating(true); // ✅ Maintenant défini
|
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', {
|
const data = await response.json();
|
||||||
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();
|
if (!data.success) {
|
||||||
|
throw new Error(data.message || 'Erreur lors de la validation');
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
fetchPendingRequests(),
|
||||||
|
fetchAllTeamRequests()
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showToast = (type, message) => {
|
const showToast = (type, message) => {
|
||||||
setToast({ type, message });
|
setToast({ type, message });
|
||||||
setTimeout(() => setToast(null), 4000);
|
setTimeout(() => setToast(null), 4000);
|
||||||
@@ -199,6 +242,7 @@ const Manager = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen bg-gray-50 flex overflow-hidden">
|
<div className="relative min-h-screen bg-gray-50 flex overflow-hidden">
|
||||||
{/* Toast Notification */}
|
{/* Toast Notification */}
|
||||||
@@ -305,7 +349,9 @@ const Manager = () => {
|
|||||||
onChange={(e) => setComment(e.target.value)}
|
onChange={(e) => setComment(e.target.value)}
|
||||||
placeholder={validationModal.action === "approve" ? "Ajouter un commentaire..." : "Expliquer le motif du refus..."}
|
placeholder={validationModal.action === "approve" ? "Ajouter un commentaire..." : "Expliquer le motif du refus..."}
|
||||||
rows={4}
|
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-red-300 focus:ring-red-500 focus:border-red-500"
|
||||||
: "border-gray-300 focus:ring-blue-500 focus:border-blue-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">
|
<div className="p-6 border-t border-gray-100 flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={closeValidationModal}
|
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
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={confirmValidation}
|
onClick={confirmValidation}
|
||||||
disabled={validationModal.action === "reject" && !comment.trim()}
|
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 ${validationModal.action === "approve" ? "bg-green-600 hover:bg-green-700" : "bg-red-600 hover:bg-red-700"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -383,15 +437,17 @@ const Manager = () => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => openValidationModal(r, "approve")}
|
onClick={() => openValidationModal(r, "approve")}
|
||||||
className="flex-1 bg-green-600 text-white px-3 py-2 rounded-lg hover:bg-green-700 text-sm"
|
disabled={isValidating}
|
||||||
data-tour="approuver-btn" >
|
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" />
|
<CheckCircle className="w-4 h-4 inline mr-1" />
|
||||||
Approuver
|
Approuver
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openValidationModal(r, "reject")}
|
onClick={() => openValidationModal(r, "reject")}
|
||||||
className="flex-1 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 text-sm"
|
disabled={isValidating}
|
||||||
data-tour="refuser-btn" >
|
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" />
|
<XCircle className="w-4 h-4 inline mr-1" />
|
||||||
Refuser
|
Refuser
|
||||||
</button>
|
</button>
|
||||||
@@ -417,7 +473,7 @@ const Manager = () => {
|
|||||||
key={m.id}
|
key={m.id}
|
||||||
onClick={() => navigate(`/employee/${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"
|
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="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
<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">
|
<span className="text-blue-600 font-medium text-sm">
|
||||||
@@ -488,16 +544,15 @@ const Manager = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
))
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<GlobalTutorial userId={user?.id} userRole={user?.role} />
|
<GlobalTutorial userId={user?.id} userRole={user?.role} />
|
||||||
</div >
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,18 +43,15 @@ const Requests = () => {
|
|||||||
const [sseConnected, setSseConnected] = useState(false);
|
const [sseConnected, setSseConnected] = useState(false);
|
||||||
const [toasts, setToasts] = useState([]);
|
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 [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [requestToDelete, setRequestToDelete] = useState(null);
|
const [requestToDelete, setRequestToDelete] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
// 🎯 STATES POUR LE TUTORIEL
|
// 🎯 STATES POUR LE TUTORIEL
|
||||||
const [runTour, setRunTour] = useState(false);
|
const [runTour, setRunTour] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 🎯 DÉCLENCHER LE TUTORIEL À CHAQUE FOIS
|
// 🎯 DÉCLENCHER LE TUTORIEL À CHAQUE FOIS
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId && !isLoading) {
|
if (userId && !isLoading) {
|
||||||
@@ -95,7 +92,6 @@ const Requests = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accounts.length > 0) {
|
if (accounts.length > 0) {
|
||||||
const request = {
|
const request = {
|
||||||
@@ -140,6 +136,10 @@ const Requests = () => {
|
|||||||
throw new Error('Le serveur PHP ne répond pas correctement');
|
throw new Error('Le serveur PHP ne répond pas correctement');
|
||||||
}
|
}
|
||||||
if (data.success) {
|
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 || []);
|
setAllRequests(data.requests || []);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.message || 'Erreur lors de la récupération des demandes');
|
throw new Error(data.message || 'Erreur lors de la récupération des demandes');
|
||||||
@@ -184,18 +184,22 @@ const Requests = () => {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ⭐ NOUVELLE FONCTION : Modifier une demande
|
// ⭐ FONCTION CORRIGÉE : Modifier une demande
|
||||||
const handleEditRequest = (request) => {
|
const handleEditRequest = (request) => {
|
||||||
if (request.status !== 'En attente') {
|
console.log('🔍 DEBUG - Request à éditer:', request);
|
||||||
showToast('⚠️ Vous ne pouvez modifier que les demandes en attente', 'warning');
|
console.log('🔍 DEBUG - Request ID:', request.id);
|
||||||
return;
|
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);
|
setRequestToEdit(request);
|
||||||
setShowEditRequestModal(true);
|
setShowEditRequestModal(true);
|
||||||
|
|
||||||
|
console.log('✅ Modal d\'édition devrait s\'ouvrir');
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ NOUVELLE FONCTION : Supprimer une demande
|
// ⭐ FONCTION : Supprimer/Annuler une demande
|
||||||
// ⭐ NOUVELLE FONCTION : Annuler une demande (En attente OU Validée, si date future)
|
|
||||||
const handleDeleteRequest = async (requestId) => {
|
const handleDeleteRequest = async (requestId) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -209,7 +213,7 @@ const Requests = () => {
|
|||||||
console.log('⚠️ Demande non trouvée dans l\'état local, récupération via API...');
|
console.log('⚠️ Demande non trouvée dans l\'état local, récupération via API...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/getRequests?user_id=${user.id}`);
|
const response = await fetch(`/api/getRequests?user_id=${userId}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.requests) {
|
if (result.success && result.requests) {
|
||||||
@@ -246,7 +250,7 @@ const Requests = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ CONFIRMATION AVEC MODAL AU LIEU DE alert()
|
// ⭐ CONFIRMATION AVEC MODAL
|
||||||
setRequestToDelete(request);
|
setRequestToDelete(request);
|
||||||
setShowDeleteConfirm(true);
|
setShowDeleteConfirm(true);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -257,6 +261,7 @@ const Requests = () => {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fonction helper pour formater les dates
|
// Fonction helper pour formater les dates
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
@@ -264,8 +269,7 @@ const Requests = () => {
|
|||||||
return date.toLocaleDateString('fr-FR');
|
return date.toLocaleDateString('fr-FR');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ⭐ FONCTION : Confirmer la suppression
|
||||||
// ⭐ NOUVELLE FONCTION : Confirmer la suppression (sans setDeletedRequests)
|
|
||||||
const confirmDeleteRequest = async () => {
|
const confirmDeleteRequest = async () => {
|
||||||
if (!requestToDelete) return;
|
if (!requestToDelete) return;
|
||||||
|
|
||||||
@@ -339,6 +343,7 @@ const Requests = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connexion SSE
|
// Connexion SSE
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
@@ -405,7 +410,7 @@ const Requests = () => {
|
|||||||
if (userId) {
|
if (userId) {
|
||||||
refreshAllData();
|
refreshAllData();
|
||||||
}
|
}
|
||||||
}, [userId]);
|
}, [userId, refreshAllData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let filtered = allRequests;
|
let filtered = allRequests;
|
||||||
@@ -427,7 +432,7 @@ const Requests = () => {
|
|||||||
|
|
||||||
setFilteredRequests(filtered);
|
setFilteredRequests(filtered);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [allRequests, searchTerm, statusFilter, typeFilter]);
|
}, [allRequests, searchTerm, statusFilter, typeFilter]);
|
||||||
|
|
||||||
const indexOfLastRequest = currentPage * requestsPerPage;
|
const indexOfLastRequest = currentPage * requestsPerPage;
|
||||||
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
|
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
|
||||||
@@ -441,7 +446,7 @@ const Requests = () => {
|
|||||||
case 'En attente': return 'bg-yellow-100 text-yellow-800';
|
case 'En attente': return 'bg-yellow-100 text-yellow-800';
|
||||||
case 'Validée': return 'bg-green-100 text-green-800';
|
case 'Validée': return 'bg-green-100 text-green-800';
|
||||||
case 'Refusée': return 'bg-red-100 text-red-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';
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -588,7 +593,6 @@ const Requests = () => {
|
|||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900">Mes demandes</h1>
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900">Mes demandes</h1>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -601,7 +605,7 @@ const Requests = () => {
|
|||||||
<RefreshCw className={`w-5 h-5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
<RefreshCw className={`w-5 h-5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-tour="nouvelle-demande"
|
data-tour="nouvelle-demande"
|
||||||
onClick={() => setShowNewRequestModal(true)}
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compteurs */}
|
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Left: list */}
|
{/* Left: list */}
|
||||||
@@ -632,7 +634,7 @@ const Requests = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
data-tour="filtres"
|
data-tour="filtres"
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
className="flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50"
|
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>
|
<p className="text-gray-500">Chargement...</p>
|
||||||
</div>
|
</div>
|
||||||
) : currentRequests.length === 0 ? (
|
) : 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" />
|
<Info className="w-12 h-12 mx-auto text-gray-300 mb-3" />
|
||||||
<p className="text-gray-500">Aucune demande trouvée</p>
|
<p className="text-gray-500">Aucune demande trouvée</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-3" data-tour="liste-demandes">
|
<div className="space-y-3" data-tour="liste-demandes">
|
||||||
{currentRequests.map((request) => (
|
{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 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">
|
<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">
|
<div className="mt-3 flex justify-between items-center text-sm">
|
||||||
<span className="text-gray-500">{request.submittedDisplay}</span>
|
<span className="text-gray-500">{request.submittedDisplay}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Bouton Modifier (seulement si En attente) */}
|
{/* ⭐ Bouton Modifier - Plus de restriction sur le statut */}
|
||||||
{request.status === 'En attente' && (
|
<button
|
||||||
<button
|
onClick={() => handleEditRequest(request)}
|
||||||
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"
|
||||||
className="text-blue-600 hover:text-blue-700 flex items-center gap-1 px-2 py-1 hover:bg-blue-50 rounded"
|
title="Modifier"
|
||||||
title="Modifier"
|
>
|
||||||
>
|
<Edit2 className="w-4 h-4" />
|
||||||
<Edit2 className="w-4 h-4" />
|
<span className="hidden sm:inline">Modifier</span>
|
||||||
<span className="hidden sm:inline">Modifier</span>
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Bouton Supprimer */}
|
{/* Bouton Annuler */}
|
||||||
{request.status === 'En attente' && (
|
{request.status !== 'Annulée' && request.status !== 'Refusée' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteRequest(request.id)}
|
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"
|
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>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MedicalDocuments demandeId={selectedRequest.id} />
|
<MedicalDocuments demandeId={selectedRequest.id} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions dans la sidebar */}
|
{/* Actions dans la sidebar */}
|
||||||
<div className="mt-6 pt-4 border-t space-y-2">
|
<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
|
<button
|
||||||
onClick={() => handleEditRequest(selectedRequest)}
|
onClick={() => handleDeleteRequest(selectedRequest.id)}
|
||||||
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"
|
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" />
|
<X className="w-4 h-4" />
|
||||||
Modifier cette demande
|
Annuler cette demande
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -865,10 +867,12 @@ const Requests = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal d'édition */}
|
{/* ⭐ MODAL D'ÉDITION CORRIGÉE */}
|
||||||
{showEditRequestModal && requestToEdit && detailedCounters && (
|
{showEditRequestModal && requestToEdit && detailedCounters && (
|
||||||
<EditLeaveRequestModal
|
<EditLeaveRequestModal
|
||||||
|
isOpen={showEditRequestModal}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
console.log('❌ Fermeture du modal d\'édition');
|
||||||
setShowEditRequestModal(false);
|
setShowEditRequestModal(false);
|
||||||
setRequestToEdit(null);
|
setRequestToEdit(null);
|
||||||
}}
|
}}
|
||||||
@@ -889,10 +893,12 @@ const Requests = () => {
|
|||||||
accessToken={graphToken}
|
accessToken={graphToken}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
userEmail={user.email}
|
userEmail={user.email}
|
||||||
userRole={user.role}
|
userName={`${user.prenom} ${user.nom}`}
|
||||||
userName={`${user.prenom} ${user.nom}`} // ⭐ CORRIGÉ : ajout des accolades {}
|
|
||||||
onRequestUpdated={() => {
|
onRequestUpdated={() => {
|
||||||
|
console.log('✅ Demande mise à jour, rafraîchissement...');
|
||||||
refreshAllData();
|
refreshAllData();
|
||||||
|
setShowEditRequestModal(false);
|
||||||
|
setRequestToEdit(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://192.168.0.4:3000',
|
target: 'http://192.168.0.3:3004',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false
|
secure: false
|
||||||
},
|
},
|
||||||
'/uploads': {
|
'/uploads': {
|
||||||
target: 'http://192.168.0.4:3000',
|
target: 'http://192.168.0.3:3004',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false
|
secure: false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user