Compare commits
1 Commits
master
..
bbebf6f44a
| Author | SHA1 | Date | |
|---|---|---|---|
| bbebf6f44a |
+8
-2
@@ -1,4 +1,10 @@
|
|||||||
|
# Configuration Base de données MySQL
|
||||||
|
DB_SERVER=192.168.0.4
|
||||||
|
DB_DATABASE=DemandeConge
|
||||||
|
DB_PASSWORD=-2b/)ru5/Bi8P[7_
|
||||||
|
DB_ENCRYPT=true
|
||||||
|
DB_TRUST_SERVER_CERTIFICATE=true
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
COLLABORATEURS_URL=http://localhost:3000
|
COLLABORATEURS_URL=http://localhost:3000
|
||||||
RH_URL=http://localhost:3001
|
RH_URL=http://localhost:3001
|
||||||
@@ -7,7 +13,7 @@ WEBHOOK_SECRET=secret-rh-2025
|
|||||||
|
|
||||||
|
|
||||||
# Configuration Serveur
|
# Configuration Serveur
|
||||||
PORT=3000
|
PORT=3001
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# Configuration Azure AD (Microsoft Graph)
|
# Configuration Azure AD (Microsoft Graph)
|
||||||
|
|||||||
Generated
+1
-2
@@ -103,8 +103,7 @@
|
|||||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-
|
"follow-redirects": "^1.15.6",
|
||||||
s": "^1.15.6",
|
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.4",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mssql": "^11.0.1",
|
"mysql2": "^3.15.1",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"pdfkit": "^0.17.2"
|
"pdfkit": "^0.17.2"
|
||||||
},
|
},
|
||||||
|
|||||||
+2493
-5447
File diff suppressed because it is too large
Load Diff
+27
-26
@@ -1,45 +1,46 @@
|
|||||||
networks:
|
version: '3.8' # ← AJOUTÉ (manquant !)
|
||||||
gtarh:
|
|
||||||
external: false
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./Backend
|
context: ./Backend
|
||||||
dockerfile: DockerFileGTARH.backend
|
dockerfile: DockerFileGTARH.backend
|
||||||
image: ouijdaneim/gtarh-backend:latest
|
|
||||||
container_name: gtarh-backend
|
container_name: gtarh-backend
|
||||||
environment:
|
|
||||||
- USER_UID=1000
|
|
||||||
- USER_GID=1000
|
|
||||||
- DB_SERVER=192.168.0.3
|
|
||||||
- DB_DATABASE=gta
|
|
||||||
- DB_USER=gta_app
|
|
||||||
- DB_PASSWORD=GTA2025!Secure
|
|
||||||
- DB_ENCRYPT=true
|
|
||||||
- DB_TRUST_SERVER_CERTIFICATE=true
|
|
||||||
- PORT=3000
|
|
||||||
- NODE_ENV=production
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- gtarh
|
|
||||||
ports:
|
ports:
|
||||||
- "8013:3000"
|
- "8013:3000"
|
||||||
|
env_file:
|
||||||
|
- ./Backend/.env
|
||||||
|
environment:
|
||||||
|
- DB_HOST=192.168.0.3
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
- COLLABORATEURS_URL=http://gtarh-backend:3000
|
||||||
|
- RH_URL=http://gtarh-backend:3000
|
||||||
|
- WEBHOOK_SECRET=secret-rh-2025
|
||||||
|
networks:
|
||||||
|
- gtarh-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./src/DockerFileGTARH.Frontend
|
dockerfile: ./src/DockerFileGTARH.Frontend
|
||||||
args:
|
args:
|
||||||
- VITE_API_URL=https://mygta-rh.ensup-adm.net
|
- VITE_API_URL=https://mygta-rh.ensup-adm.net:8013
|
||||||
image: ouijdaneim/gtarh-frontend:latest
|
|
||||||
container_name: gtarh-frontend
|
container_name: gtarh-frontend
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- gtarh
|
|
||||||
ports:
|
ports:
|
||||||
- "3014:3014"
|
- "3014:3014"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
networks:
|
||||||
|
- gtarh-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
gtarh-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
|
driver: local
|
||||||
|
|||||||
Generated
+22
-170
@@ -55,14 +55,12 @@
|
|||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"mysql2": "^3.15.1",
|
"mysql2": "^3.15.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"pdfkit": "^0.17.2",
|
|
||||||
"pdfkit-table": "^0.1.99",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.61.1",
|
"react-hook-form": "^7.61.1",
|
||||||
"react-resizable-panels": "^2.1.9",
|
"react-resizable-panels": "^2.1.9",
|
||||||
"react-router-dom": "^6.30.2",
|
"react-router-dom": "^6.30.1",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
@@ -75,7 +73,7 @@
|
|||||||
"@eslint/js": "^9.32.0",
|
"@eslint/js": "^9.32.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@types/react": "^18.3.27",
|
"@types/react": "^18.3.23",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||||
@@ -2664,9 +2662,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.1",
|
"version": "1.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||||
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
|
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -3277,15 +3275,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@swc/helpers": {
|
|
||||||
"version": "0.5.18",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
|
||||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/types": {
|
"node_modules/@swc/types": {
|
||||||
"version": "0.1.25",
|
"version": "0.1.25",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
|
||||||
@@ -3480,15 +3469,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.27",
|
"version": "18.3.25",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz",
|
||||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
"integrity": "sha512-oSVZmGtDPmRZtVDqvdKUi/qgCsWp5IDY29wp8na8Bj4B3cc99hfNzvNhlMkVVxctkAOGUA3Km7MMpBHAnWfcIA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
@@ -4227,15 +4216,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brotli": {
|
|
||||||
"version": "1.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
|
||||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "^1.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.26.3",
|
"version": "4.26.3",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
|
||||||
@@ -4495,15 +4475,6 @@
|
|||||||
"url": "https://polar.sh/cva"
|
"url": "https://polar.sh/cva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/clone": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -4690,12 +4661,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/crypto-js": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -4709,9 +4674,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
@@ -4906,12 +4871,6 @@
|
|||||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dfa": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -5464,6 +5423,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-equals": {
|
"node_modules/fast-equals": {
|
||||||
@@ -5606,23 +5566,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/fontkit": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.12",
|
|
||||||
"brotli": "^1.3.2",
|
|
||||||
"clone": "^2.1.2",
|
|
||||||
"dfa": "^1.2.0",
|
|
||||||
"fast-deep-equal": "^3.1.3",
|
|
||||||
"restructure": "^3.0.0",
|
|
||||||
"tiny-inflate": "^1.0.3",
|
|
||||||
"unicode-properties": "^1.4.0",
|
|
||||||
"unicode-trie": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@@ -6191,13 +6134,6 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jpeg-exif": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
|
|
||||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -6436,25 +6372,6 @@
|
|||||||
"url": "https://github.com/sponsors/antonk52"
|
"url": "https://github.com/sponsors/antonk52"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/linebreak": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "0.0.8",
|
|
||||||
"unicode-trie": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/linebreak/node_modules/base64-js": {
|
|
||||||
"version": "0.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
|
||||||
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
@@ -7187,28 +7104,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pdfkit": {
|
|
||||||
"version": "0.17.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz",
|
|
||||||
"integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"fontkit": "^2.0.4",
|
|
||||||
"jpeg-exif": "^1.1.4",
|
|
||||||
"linebreak": "^1.1.0",
|
|
||||||
"png-js": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pdfkit-table": {
|
|
||||||
"version": "0.1.99",
|
|
||||||
"resolved": "https://registry.npmjs.org/pdfkit-table/-/pdfkit-table-0.1.99.tgz",
|
|
||||||
"integrity": "sha512-BaunFJ7cvKqIPONzX9FXUpeC/3iJeLo0v2nVNjCccAxdMXKvDMBjxyF4wZhKOYlmP+nYZcikf9ByVJeCzjNK6g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pdfkit": "latest"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -7245,11 +7140,6 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/png-js": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -7691,12 +7581,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.30.2",
|
"version": "6.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
|
||||||
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
|
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.1"
|
"@remix-run/router": "1.23.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -7706,13 +7596,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "6.30.2",
|
"version": "6.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
|
||||||
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
|
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.1",
|
"@remix-run/router": "1.23.0",
|
||||||
"react-router": "6.30.2"
|
"react-router": "6.30.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -7902,12 +7792,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/restructure": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
@@ -8585,12 +8469,6 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tiny-inflate": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tiny-invariant": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
@@ -8756,32 +8634,6 @@
|
|||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unicode-properties": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "^1.3.0",
|
|
||||||
"unicode-trie": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unicode-trie": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pako": "^0.2.5",
|
|
||||||
"tiny-inflate": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unicode-trie/node_modules/pako": {
|
|
||||||
"version": "0.2.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
|
||||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
|||||||
+2
-4
@@ -58,14 +58,12 @@
|
|||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"mysql2": "^3.15.1",
|
"mysql2": "^3.15.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"pdfkit": "^0.17.2",
|
|
||||||
"pdfkit-table": "^0.1.99",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.61.1",
|
"react-hook-form": "^7.61.1",
|
||||||
"react-resizable-panels": "^2.1.9",
|
"react-resizable-panels": "^2.1.9",
|
||||||
"react-router-dom": "^6.30.2",
|
"react-router-dom": "^6.30.1",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
@@ -78,7 +76,7 @@
|
|||||||
"@eslint/js": "^9.32.0",
|
"@eslint/js": "^9.32.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@types/react": "^18.3.27",
|
"@types/react": "^18.3.23",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Check, ChevronsUpDown } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "@/components/ui/command";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface Validateur {
|
|
||||||
id: number;
|
|
||||||
nom: string;
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidateurComboboxProps {
|
|
||||||
validateurs: Validateur[];
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ValidateurCombobox({ validateurs, value, onChange, placeholder = "Sélectionner..." }: ValidateurComboboxProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const selected = validateurs.find((v) => String(v.id) === value);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={open}
|
|
||||||
className="w-full justify-between font-normal"
|
|
||||||
>
|
|
||||||
{selected ? `${selected.nom} (${selected.role})` : placeholder}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="Rechercher un validateur..." />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>Aucun validateur trouvé.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{validateurs.map((v) => (
|
|
||||||
<CommandItem
|
|
||||||
key={v.id}
|
|
||||||
value={`${v.nom} ${v.email}`}
|
|
||||||
onSelect={() => {
|
|
||||||
onChange(String(v.id) === value ? "" : String(v.id));
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
value === String(v.id) ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>{v.nom} <span className="text-muted-foreground">({v.role})</span></span>
|
|
||||||
<span className="text-xs text-muted-foreground">{v.email}</span>
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+669
-514
File diff suppressed because it is too large
Load Diff
+380
-626
File diff suppressed because it is too large
Load Diff
+202
-461
@@ -1,28 +1,23 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { ArrowLeft, Download, FileSpreadsheet } from "lucide-react";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { ArrowLeft, Download, FileSpreadsheet, Sunrise, Sunset, Sun, Calendar, ClipboardList } from "lucide-react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import ExcelJS from 'exceljs';
|
import ExcelJS from 'exceljs';
|
||||||
|
|
||||||
interface CongeDetail {
|
interface CongeDetail {
|
||||||
nb: number;
|
nb: number;
|
||||||
dates: string;
|
dates: string;
|
||||||
periodes?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutreConge {
|
interface AutreConge {
|
||||||
type: string;
|
type: string;
|
||||||
nb: number;
|
nb: number;
|
||||||
dates: string;
|
dates: string;
|
||||||
periodes?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataPaie {
|
interface DataPaie {
|
||||||
@@ -33,34 +28,13 @@ interface DataPaie {
|
|||||||
cp: CongeDetail;
|
cp: CongeDetail;
|
||||||
aap: CongeDetail;
|
aap: CongeDetail;
|
||||||
am: CongeDetail;
|
am: CongeDetail;
|
||||||
jposf: CongeDetail;
|
|
||||||
recup: CongeDetail;
|
|
||||||
autres: AutreConge[];
|
autres: AutreConge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExportPaie = () => {
|
const ExportPaie = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// États pour le mode Mois/Année
|
|
||||||
const [mois, setMois] = useState(new Date().getMonth() + 1);
|
const [mois, setMois] = useState(new Date().getMonth() + 1);
|
||||||
const [annee, setAnnee] = useState(new Date().getFullYear());
|
const [annee, setAnnee] = useState(new Date().getFullYear());
|
||||||
|
|
||||||
// ── NOUVEAU : filtre optionnel par date de création dans l'onglet "Par mois" ──
|
|
||||||
const [filtrerParCreation, setFiltrerParCreation] = useState(false);
|
|
||||||
const [dateCreationDebut, setDateCreationDebut] = useState("");
|
|
||||||
const [dateCreationFin, setDateCreationFin] = useState("");
|
|
||||||
|
|
||||||
// États pour le mode Plage de dates (période du congé)
|
|
||||||
const [dateDebut, setDateDebut] = useState("");
|
|
||||||
const [dateFin, setDateFin] = useState("");
|
|
||||||
|
|
||||||
// États pour le mode Plage de saisie (date de création de la demande)
|
|
||||||
const [dateSaisieDebut, setDateSaisieDebut] = useState("");
|
|
||||||
const [dateSaisieFin, setDateSaisieFin] = useState("");
|
|
||||||
|
|
||||||
// État pour le mode sélectionné
|
|
||||||
const [modeExport, setModeExport] = useState<"mois" | "plage" | "saisie">("mois");
|
|
||||||
|
|
||||||
const [dataPaie, setDataPaie] = useState<DataPaie[]>([]);
|
const [dataPaie, setDataPaie] = useState<DataPaie[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -73,113 +47,20 @@ const ExportPaie = () => {
|
|||||||
{ value: 11, label: 'Novembre' }, { value: 12, label: 'Décembre' }
|
{ value: 11, label: 'Novembre' }, { value: 12, label: 'Décembre' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const formatDateAvecPeriode = (date: string, periode?: string) => {
|
|
||||||
if (!periode || periode === 'Journée' || periode === 'Journée entière') {
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
const periodeAbregee = periode === 'Matin' ? 'M' : periode === 'Après-midi' ? 'AM' : '';
|
|
||||||
return periodeAbregee ? `${date} (${periodeAbregee})` : date;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPeriodeIcon = (periode?: string) => {
|
|
||||||
if (periode === 'Matin') {
|
|
||||||
return <Sunrise className="w-3 h-3 inline mr-1 text-orange-500" />;
|
|
||||||
} else if (periode === 'Après-midi') {
|
|
||||||
return <Sunset className="w-3 h-3 inline mr-1 text-blue-500" />;
|
|
||||||
} else if (periode === 'Journée' || periode === 'Journée entière') {
|
|
||||||
return <Sun className="w-3 h-3 inline mr-1 text-yellow-500" />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPeriodeLabel = (periode?: string) => {
|
|
||||||
if (periode === 'Matin') return 'Matin';
|
|
||||||
if (periode === 'Après-midi') return 'Après-midi';
|
|
||||||
if (periode === 'Journée' || periode === 'Journée entière') return 'Journée';
|
|
||||||
return 'Journée';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fonction pour générer le rapport selon le mode sélectionné
|
|
||||||
const genererRapport = async () => {
|
const genererRapport = async () => {
|
||||||
// ── Validation spécifique au mode "mois" avec filtre création actif ──
|
|
||||||
if (modeExport === "mois" && filtrerParCreation) {
|
|
||||||
if (!dateCreationDebut || !dateCreationFin) {
|
|
||||||
toast.error("Veuillez sélectionner une date de début et une date de fin de création");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (new Date(dateCreationDebut) > new Date(dateCreationFin)) {
|
|
||||||
toast.error("La date de début de création doit être antérieure à la date de fin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modeExport === "plage") {
|
|
||||||
if (!dateDebut || !dateFin) {
|
|
||||||
toast.error("Veuillez sélectionner une date de début et une date de fin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (new Date(dateDebut) > new Date(dateFin)) {
|
|
||||||
toast.error("La date de début doit être antérieure à la date de fin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modeExport === "saisie") {
|
|
||||||
if (!dateSaisieDebut || !dateSaisieFin) {
|
|
||||||
toast.error("Veuillez sélectionner une date de début et une date de fin de saisie");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (new Date(dateSaisieDebut) > new Date(dateSaisieFin)) {
|
|
||||||
toast.error("La date de début doit être antérieure à la date de fin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
let url = '';
|
const response = await fetch(
|
||||||
|
`/api/export/paie?mois=${mois}&annee=${annee}`,
|
||||||
if (modeExport === "mois") {
|
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||||||
// ── Construction de l'URL avec paramètres optionnels de création ──
|
);
|
||||||
url = `/api/export/paie?mois=${mois}&annee=${annee}`;
|
|
||||||
if (filtrerParCreation && dateCreationDebut && dateCreationFin) {
|
|
||||||
url += `&dateSaisieDebut=${dateCreationDebut}&dateSaisieFin=${dateCreationFin}`;
|
|
||||||
}
|
|
||||||
} else if (modeExport === "plage") {
|
|
||||||
url = `/api/export/paie-plage?dateDebut=${dateDebut}&dateFin=${dateFin}`;
|
|
||||||
} else {
|
|
||||||
url = `/api/export/paie-saisie?dateSaisieDebut=${dateSaisieDebut}&dateSaisieFin=${dateSaisieFin}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Erreur serveur: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('📊 Données reçues:', data);
|
|
||||||
setDataPaie(data);
|
|
||||||
|
|
||||||
if (modeExport === "mois") {
|
console.log('📊 Données reçues:', data);
|
||||||
const suffix = filtrerParCreation && dateCreationDebut && dateCreationFin
|
|
||||||
? `, saisies du ${new Date(dateCreationDebut).toLocaleDateString('fr-FR')} au ${new Date(dateCreationFin).toLocaleDateString('fr-FR')}`
|
setDataPaie(data);
|
||||||
: '';
|
toast.success(`Rapport généré avec succès (${data.length} employé${data.length > 1 ? 's' : ''})`);
|
||||||
toast.success(`Rapport généré avec succès (${data.length} employé${data.length > 1 ? 's' : ''}${suffix})`);
|
|
||||||
} else if (modeExport === "plage") {
|
|
||||||
const debut = new Date(dateDebut);
|
|
||||||
const fin = new Date(dateFin);
|
|
||||||
const nbJours = Math.ceil((fin.getTime() - debut.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
||||||
toast.success(`Rapport généré avec succès (${data.length} employé${data.length > 1 ? 's' : ''}, ${nbJours} jours)`);
|
|
||||||
} else {
|
|
||||||
const debut = new Date(dateSaisieDebut);
|
|
||||||
const fin = new Date(dateSaisieFin);
|
|
||||||
const nbJours = Math.ceil((fin.getTime() - debut.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
||||||
toast.success(`Rapport généré avec succès (${data.length} employé${data.length > 1 ? 's' : ''}, demandes saisies sur ${nbJours} jours)`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Erreur:', error);
|
console.error('❌ Erreur:', error);
|
||||||
toast.error("Erreur lors de la génération du rapport");
|
toast.error("Erreur lors de la génération du rapport");
|
||||||
@@ -188,56 +69,30 @@ const ExportPaie = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Titre du rapport selon le mode
|
|
||||||
const getTitreRapport = () => {
|
|
||||||
if (modeExport === "mois") {
|
|
||||||
const base = `Rapport des congés - ${moisOptions.find(m => m.value === mois)?.label} ${annee}`;
|
|
||||||
if (filtrerParCreation && dateCreationDebut && dateCreationFin) {
|
|
||||||
return `${base} (saisies du ${new Date(dateCreationDebut).toLocaleDateString('fr-FR')} au ${new Date(dateCreationFin).toLocaleDateString('fr-FR')})`;
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
} else if (modeExport === "plage") {
|
|
||||||
return `Rapport des congés - Du ${new Date(dateDebut).toLocaleDateString('fr-FR')} au ${new Date(dateFin).toLocaleDateString('fr-FR')}`;
|
|
||||||
} else {
|
|
||||||
return `Rapport des congés - Demandes saisies du ${new Date(dateSaisieDebut).toLocaleDateString('fr-FR')} au ${new Date(dateSaisieFin).toLocaleDateString('fr-FR')}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const exporterExcel = async () => {
|
const exporterExcel = async () => {
|
||||||
if (dataPaie.length === 0) return toast.error("Aucune donnée à exporter");
|
if (dataPaie.length === 0) return toast.error("Aucune donnée à exporter");
|
||||||
|
|
||||||
const moisLabel = moisOptions.find(m => m.value === mois)?.label || 'Inconnu';
|
const moisLabel = moisOptions.find(m => m.value === mois)?.label || 'Inconnu';
|
||||||
|
|
||||||
let nomFichier = '';
|
|
||||||
if (modeExport === "mois") {
|
|
||||||
nomFichier = filtrerParCreation && dateCreationDebut && dateCreationFin
|
|
||||||
? `conges_paie_${moisLabel}_${annee}_saisies_${dateCreationDebut}_${dateCreationFin}.xlsx`
|
|
||||||
: `conges_paie_${moisLabel}_${annee}.xlsx`;
|
|
||||||
} else if (modeExport === "plage") {
|
|
||||||
nomFichier = `conges_paie_${dateDebut}_${dateFin}.xlsx`;
|
|
||||||
} else {
|
|
||||||
nomFichier = `conges_saisie_${dateSaisieDebut}_${dateSaisieFin}.xlsx`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new ExcelJS.Workbook();
|
||||||
const sheetTitle = modeExport === "saisie" ? `Saisies ${dateSaisieDebut}` : `Congés ${moisLabel}`;
|
const worksheet = workbook.addWorksheet(`Congés ${moisLabel}`);
|
||||||
const worksheet = workbook.addWorksheet(sheetTitle);
|
|
||||||
|
|
||||||
|
// ✅ MODIFICATION : Colonnes sans Email
|
||||||
worksheet.columns = [
|
worksheet.columns = [
|
||||||
{ header: 'Collaborateur', key: 'collaborateur', width: 20 },
|
{ header: 'Collaborateur', key: 'collaborateur', width: 20 },
|
||||||
{ header: 'Service', key: 'service', width: 20 },
|
{ header: 'Service', key: 'service', width: 20 },
|
||||||
{ header: 'RTT Nb', key: 'rtt_nb', width: 10 },
|
{ header: 'RTT Nb', key: 'rtt_nb', width: 10 },
|
||||||
{ header: 'RTT Dates', key: 'rtt_dates', width: 35 },
|
{ header: 'RTT Dates', key: 'rtt_dates', width: 25 },
|
||||||
{ header: 'CP Nb', key: 'cp_nb', width: 10 },
|
{ header: 'CP Nb', key: 'cp_nb', width: 10 },
|
||||||
{ header: 'CP Dates', key: 'cp_dates', width: 35 },
|
{ header: 'CP Dates', key: 'cp_dates', width: 25 },
|
||||||
{ header: 'AAP Nb', key: 'aap_nb', width: 10 },
|
{ header: 'AAP Nb', key: 'aap_nb', width: 10 },
|
||||||
{ header: 'AAP Dates', key: 'aap_dates', width: 35 },
|
{ header: 'AAP Dates', key: 'aap_dates', width: 25 },
|
||||||
{ header: 'AM Nb', key: 'am_nb', width: 10 },
|
{ header: 'AM Nb', key: 'am_nb', width: 10 },
|
||||||
{ header: 'AM Dates', key: 'am_dates', width: 35 },
|
{ header: 'AM Dates', key: 'am_dates', width: 25 },
|
||||||
{ header: 'Autres Type', key: 'autres_type', width: 20 },
|
{ header: 'Autres Type', key: 'autres_type', width: 20 },
|
||||||
{ header: 'Autres Nb', key: 'autres_nb', width: 10 },
|
{ header: 'Autres Nb', key: 'autres_nb', width: 10 },
|
||||||
{ header: 'Autres Dates', key: 'autres_dates', width: 40 }
|
{ header: 'Autres Dates', key: 'autres_dates', width: 30 }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Style de l'en-tête
|
// Style de l'en-tête
|
||||||
@@ -250,6 +105,7 @@ const ExportPaie = () => {
|
|||||||
fgColor: { argb: 'FF7e5aa2' }
|
fgColor: { argb: 'FF7e5aa2' }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bordures ÉPAISSES pour l'en-tête
|
||||||
worksheet.getRow(1).eachCell((cell) => {
|
worksheet.getRow(1).eachCell((cell) => {
|
||||||
cell.border = {
|
cell.border = {
|
||||||
top: { style: 'medium', color: { argb: 'FF000000' } },
|
top: { style: 'medium', color: { argb: 'FF000000' } },
|
||||||
@@ -261,26 +117,24 @@ const ExportPaie = () => {
|
|||||||
|
|
||||||
let currentRow = 2;
|
let currentRow = 2;
|
||||||
|
|
||||||
dataPaie.forEach((row) => {
|
// Ajouter les données avec fusion de cellules
|
||||||
|
dataPaie.forEach((row, employeIndex) => {
|
||||||
const startRow = currentRow;
|
const startRow = currentRow;
|
||||||
|
|
||||||
|
// ✅ MODIFICATION : Extraire uniquement le nom (dernier mot)
|
||||||
const nomComplet = row.employe.split(' ');
|
const nomComplet = row.employe.split(' ');
|
||||||
const nomSeul = nomComplet[nomComplet.length - 1];
|
const nomSeul = nomComplet[nomComplet.length - 1]; // Prend le dernier mot (le nom)
|
||||||
|
|
||||||
|
// Calculer le nombre maximum de lignes nécessaires pour cet employé
|
||||||
const rttDatesArray = row.rtt?.dates ? row.rtt.dates.split(' ; ') : [];
|
const rttDatesArray = row.rtt?.dates ? row.rtt.dates.split(' ; ') : [];
|
||||||
const rttPeriodesArray = row.rtt?.periodes ? row.rtt.periodes.split(' ; ') : [];
|
|
||||||
const cpDatesArray = row.cp?.dates ? row.cp.dates.split(' ; ') : [];
|
const cpDatesArray = row.cp?.dates ? row.cp.dates.split(' ; ') : [];
|
||||||
const cpPeriodesArray = row.cp?.periodes ? row.cp.periodes.split(' ; ') : [];
|
|
||||||
const aapDatesArray = row.aap?.dates ? row.aap.dates.split(' ; ') : [];
|
const aapDatesArray = row.aap?.dates ? row.aap.dates.split(' ; ') : [];
|
||||||
const aapPeriodesArray = row.aap?.periodes ? row.aap.periodes.split(' ; ') : [];
|
|
||||||
const amDatesArray = row.am?.dates ? row.am.dates.split(' ; ') : [];
|
const amDatesArray = row.am?.dates ? row.am.dates.split(' ; ') : [];
|
||||||
const amPeriodesArray = row.am?.periodes ? row.am.periodes.split(' ; ') : [];
|
|
||||||
|
|
||||||
|
// Gérer les "Autres"
|
||||||
let autresDatesArray: string[][] = [];
|
let autresDatesArray: string[][] = [];
|
||||||
let autresPeriodesArray: string[][] = [];
|
|
||||||
if (row.autres && row.autres.length > 0) {
|
if (row.autres && row.autres.length > 0) {
|
||||||
autresDatesArray = row.autres.map(a => a.dates.split(' ; '));
|
autresDatesArray = row.autres.map(a => a.dates.split(' ; '));
|
||||||
autresPeriodesArray = row.autres.map(a => a.periodes ? a.periodes.split(' ; ') : []);
|
|
||||||
}
|
}
|
||||||
const maxAutresDates = Math.max(...autresDatesArray.map(arr => arr.length), 1);
|
const maxAutresDates = Math.max(...autresDatesArray.map(arr => arr.length), 1);
|
||||||
|
|
||||||
@@ -292,17 +146,20 @@ const ExportPaie = () => {
|
|||||||
maxAutresDates
|
maxAutresDates
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ajouter les lignes nécessaires
|
||||||
for (let i = 0; i < maxRows; i++) {
|
for (let i = 0; i < maxRows; i++) {
|
||||||
const rowData: any = {};
|
const rowData: any = {};
|
||||||
|
|
||||||
|
// Première ligne : inclure nom et service (sans email)
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
rowData.collaborateur = nomSeul;
|
rowData.collaborateur = nomSeul; // ✅ Uniquement le nom
|
||||||
rowData.service = row.service;
|
rowData.service = row.service;
|
||||||
rowData.rtt_nb = row.rtt?.nb || 0;
|
rowData.rtt_nb = row.rtt?.nb || 0;
|
||||||
rowData.cp_nb = row.cp?.nb || 0;
|
rowData.cp_nb = row.cp?.nb || 0;
|
||||||
rowData.aap_nb = row.aap?.nb || 0;
|
rowData.aap_nb = row.aap?.nb || 0;
|
||||||
rowData.am_nb = row.am?.nb || 0;
|
rowData.am_nb = row.am?.nb || 0;
|
||||||
|
|
||||||
|
// Autres (première ligne)
|
||||||
if (row.autres && row.autres.length > 0) {
|
if (row.autres && row.autres.length > 0) {
|
||||||
rowData.autres_type = row.autres.map(a => a.type).join('\n');
|
rowData.autres_type = row.autres.map(a => a.type).join('\n');
|
||||||
rowData.autres_nb = row.autres.map(a => a.nb).join('\n');
|
rowData.autres_nb = row.autres.map(a => a.nb).join('\n');
|
||||||
@@ -318,18 +175,23 @@ const ExportPaie = () => {
|
|||||||
rowData.autres_nb = '';
|
rowData.autres_nb = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
rowData.rtt_dates = rttDatesArray[i] ? formatDateAvecPeriode(rttDatesArray[i], rttPeriodesArray[i] || 'Journée') : '';
|
// Dates RTT
|
||||||
rowData.cp_dates = cpDatesArray[i] ? formatDateAvecPeriode(cpDatesArray[i], cpPeriodesArray[i] || 'Journée') : '';
|
rowData.rtt_dates = rttDatesArray[i] || '';
|
||||||
rowData.aap_dates = aapDatesArray[i] ? formatDateAvecPeriode(aapDatesArray[i], aapPeriodesArray[i] || 'Journée') : '';
|
|
||||||
rowData.am_dates = amDatesArray[i] ? formatDateAvecPeriode(amDatesArray[i], amPeriodesArray[i] || 'Journée') : '';
|
|
||||||
|
|
||||||
|
// Dates CP
|
||||||
|
rowData.cp_dates = cpDatesArray[i] || '';
|
||||||
|
|
||||||
|
// Dates AAP
|
||||||
|
rowData.aap_dates = aapDatesArray[i] || '';
|
||||||
|
|
||||||
|
// Dates AM
|
||||||
|
rowData.am_dates = amDatesArray[i] || '';
|
||||||
|
|
||||||
|
// Dates Autres
|
||||||
if (row.autres && row.autres.length > 0) {
|
if (row.autres && row.autres.length > 0) {
|
||||||
const allAutresDates = row.autres.map((a, autreIdx) => {
|
const allAutresDates = row.autres.map(a => {
|
||||||
const dates = a.dates.split(' ; ');
|
const dates = a.dates.split(' ; ');
|
||||||
const periodes = autresPeriodesArray[autreIdx] || [];
|
return dates[i] || '';
|
||||||
const date = dates[i] || '';
|
|
||||||
const periode = periodes[i] || 'Journée';
|
|
||||||
return formatDateAvecPeriode(date, periode);
|
|
||||||
}).filter(d => d !== '').join(' | ');
|
}).filter(d => d !== '').join(' | ');
|
||||||
rowData.autres_dates = allAutresDates;
|
rowData.autres_dates = allAutresDates;
|
||||||
} else {
|
} else {
|
||||||
@@ -342,34 +204,44 @@ const ExportPaie = () => {
|
|||||||
|
|
||||||
const endRow = currentRow - 1;
|
const endRow = currentRow - 1;
|
||||||
|
|
||||||
|
// ✅ MODIFICATION : Fusionner les cellules (ajusté pour la nouvelle structure)
|
||||||
if (maxRows > 1) {
|
if (maxRows > 1) {
|
||||||
worksheet.mergeCells(`A${startRow}:A${endRow}`);
|
worksheet.mergeCells(`A${startRow}:A${endRow}`); // Collaborateur
|
||||||
worksheet.mergeCells(`B${startRow}:B${endRow}`);
|
worksheet.mergeCells(`B${startRow}:B${endRow}`); // Service
|
||||||
worksheet.mergeCells(`C${startRow}:C${endRow}`);
|
worksheet.mergeCells(`C${startRow}:C${endRow}`); // RTT Nb
|
||||||
worksheet.mergeCells(`E${startRow}:E${endRow}`);
|
worksheet.mergeCells(`E${startRow}:E${endRow}`); // CP Nb
|
||||||
worksheet.mergeCells(`G${startRow}:G${endRow}`);
|
worksheet.mergeCells(`G${startRow}:G${endRow}`); // AAP Nb
|
||||||
worksheet.mergeCells(`I${startRow}:I${endRow}`);
|
worksheet.mergeCells(`I${startRow}:I${endRow}`); // AM Nb
|
||||||
worksheet.mergeCells(`K${startRow}:K${endRow}`);
|
worksheet.mergeCells(`K${startRow}:K${endRow}`); // Autres Type
|
||||||
worksheet.mergeCells(`L${startRow}:L${endRow}`);
|
worksheet.mergeCells(`L${startRow}:L${endRow}`); // Autres Nb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Appliquer les styles pour ce groupe de lignes
|
||||||
for (let r = startRow; r <= endRow; r++) {
|
for (let r = startRow; r <= endRow; r++) {
|
||||||
const currentRowObj = worksheet.getRow(r);
|
const currentRowObj = worksheet.getRow(r);
|
||||||
currentRowObj.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
currentRowObj.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
||||||
|
|
||||||
|
// Hauteur de ligne
|
||||||
currentRowObj.height = 25;
|
currentRowObj.height = 25;
|
||||||
|
|
||||||
currentRowObj.getCell(3).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE9D5FF' } };
|
// ✅ MODIFICATION : Couleurs ajustées (colonnes décalées)
|
||||||
currentRowObj.getCell(4).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE9D5FF' } };
|
currentRowObj.getCell(3).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE9D5FF' } }; // RTT Nb
|
||||||
currentRowObj.getCell(5).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFDBEAFE' } };
|
currentRowObj.getCell(4).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE9D5FF' } }; // RTT Dates
|
||||||
currentRowObj.getCell(6).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFDBEAFE' } };
|
|
||||||
currentRowObj.getCell(7).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD1FAE5' } };
|
|
||||||
currentRowObj.getCell(8).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD1FAE5' } };
|
|
||||||
currentRowObj.getCell(9).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEE2E2' } };
|
|
||||||
currentRowObj.getCell(10).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEE2E2' } };
|
|
||||||
currentRowObj.getCell(11).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } };
|
|
||||||
currentRowObj.getCell(12).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } };
|
|
||||||
currentRowObj.getCell(13).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } };
|
|
||||||
|
|
||||||
|
currentRowObj.getCell(5).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFDBEAFE' } }; // CP Nb
|
||||||
|
currentRowObj.getCell(6).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFDBEAFE' } }; // CP Dates
|
||||||
|
|
||||||
|
currentRowObj.getCell(7).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD1FAE5' } }; // AAP Nb
|
||||||
|
currentRowObj.getCell(8).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD1FAE5' } }; // AAP Dates
|
||||||
|
|
||||||
|
currentRowObj.getCell(9).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEE2E2' } }; // AM Nb
|
||||||
|
currentRowObj.getCell(10).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEE2E2' } }; // AM Dates
|
||||||
|
|
||||||
|
currentRowObj.getCell(11).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } }; // Autres Type
|
||||||
|
currentRowObj.getCell(12).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } }; // Autres Nb
|
||||||
|
currentRowObj.getCell(13).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } }; // Autres Dates
|
||||||
|
|
||||||
|
// Bordures ÉPAISSES pour toutes les cellules
|
||||||
currentRowObj.eachCell((cell) => {
|
currentRowObj.eachCell((cell) => {
|
||||||
cell.border = {
|
cell.border = {
|
||||||
top: { style: 'medium', color: { argb: 'FF000000' } },
|
top: { style: 'medium', color: { argb: 'FF000000' } },
|
||||||
@@ -381,6 +253,7 @@ const ExportPaie = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Générer le fichier Excel
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
const blob = new Blob([buffer], {
|
const blob = new Blob([buffer], {
|
||||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
@@ -388,15 +261,64 @@ const ExportPaie = () => {
|
|||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = URL.createObjectURL(blob);
|
link.href = URL.createObjectURL(blob);
|
||||||
link.download = nomFichier;
|
link.download = `conges_paie_${moisLabel}_${annee}.xlsx`;
|
||||||
link.click();
|
link.click();
|
||||||
|
|
||||||
toast.success("Export Excel réussi !");
|
toast.success("Export Excel réussi avec fusion de cellules !");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur export Excel:', error);
|
console.error('Erreur export Excel:', error);
|
||||||
toast.error("Erreur lors de l'export Excel");
|
toast.error("Erreur lors de l'export Excel");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const exporterCSV = () => {
|
||||||
|
if (dataPaie.length === 0) return toast.error("Aucune donnée à exporter");
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
'Employé', 'Email', 'Service',
|
||||||
|
'RTT Nb', 'RTT Dates',
|
||||||
|
'CP Nb', 'CP Dates',
|
||||||
|
'AAP Nb', 'AAP Dates',
|
||||||
|
'AM Nb', 'AM Dates',
|
||||||
|
'Autres Type', 'Autres Nb', 'Autres Dates'
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = dataPaie.map(row => {
|
||||||
|
const baseData = [
|
||||||
|
row.employe,
|
||||||
|
row.email,
|
||||||
|
row.service,
|
||||||
|
row.rtt?.nb || 0,
|
||||||
|
row.rtt?.dates || '',
|
||||||
|
row.cp?.nb || 0,
|
||||||
|
row.cp?.dates || '',
|
||||||
|
row.aap?.nb || 0,
|
||||||
|
row.aap?.dates || '',
|
||||||
|
row.am?.nb || 0,
|
||||||
|
row.am?.dates || ''
|
||||||
|
];
|
||||||
|
|
||||||
|
if (row.autres && row.autres.length > 0) {
|
||||||
|
const autresTypes = row.autres.map(a => a.type).join(' | ');
|
||||||
|
const autresNb = row.autres.map(a => a.nb).join(' | ');
|
||||||
|
const autresDates = row.autres.map(a => a.dates).join(' | ');
|
||||||
|
return [...baseData, autresTypes, autresNb, autresDates];
|
||||||
|
} else {
|
||||||
|
return [...baseData, '', '', ''];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(';'),
|
||||||
|
...rows.map(row => row.join(';'))
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `conges_paie_${moisOptions.find(m => m.value === mois)?.label}_${annee}.csv`;
|
||||||
|
link.click();
|
||||||
|
toast.success("Export CSV réussi");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-subtle">
|
<div className="min-h-screen bg-gradient-subtle">
|
||||||
@@ -427,21 +349,6 @@ const ExportPaie = () => {
|
|||||||
<CardDescription>Sélectionnez la période pour générer le rapport</CardDescription>
|
<CardDescription>Sélectionnez la période pour générer le rapport</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Tabs value={modeExport} onValueChange={(v) => { setModeExport(v as "mois" | "plage" | "saisie"); setDataPaie([]); }} className="mb-6">
|
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
|
||||||
<TabsTrigger value="mois">Par mois</TabsTrigger>
|
|
||||||
<TabsTrigger value="plage">
|
|
||||||
<Calendar className="w-4 h-4 mr-2" />
|
|
||||||
Par plage de dates
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="saisie">
|
|
||||||
<ClipboardList className="w-4 h-4 mr-2" />
|
|
||||||
Par plage de saisie
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{/* ── Mode Mois/Année ── */}
|
|
||||||
<TabsContent value="mois">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Mois</Label>
|
<Label>Mois</Label>
|
||||||
@@ -467,177 +374,21 @@ const ExportPaie = () => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Bouton Générer le rapport toujours affiché avec fond mauve */}
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<Button
|
<Button
|
||||||
onClick={genererRapport}
|
onClick={genererRapport}
|
||||||
disabled={loading || (filtrerParCreation && (!dateCreationDebut || !dateCreationFin))}
|
disabled={loading}
|
||||||
className="w-full bg-[#7e5aa2] hover:bg-[#6b4891] text-white transition-smooth shadow-elegant"
|
className="w-full bg-[#7e5aa2] hover:bg-[#6b4891] text-white transition-smooth shadow-elegant"
|
||||||
>
|
>
|
||||||
{loading ? 'Génération...' : 'Générer le rapport'}
|
{loading ? 'Génération...' : 'Générer le rapport'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Filtre optionnel par date de création ── */}
|
|
||||||
<div className="mt-5 pt-4 border-t border-border">
|
|
||||||
<div className="flex items-center space-x-2 mb-3">
|
|
||||||
<Checkbox
|
|
||||||
id="filtrerParCreation"
|
|
||||||
checked={filtrerParCreation}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setFiltrerParCreation(checked === true);
|
|
||||||
if (!checked) {
|
|
||||||
setDateCreationDebut("");
|
|
||||||
setDateCreationFin("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="filtrerParCreation"
|
|
||||||
className="text-sm font-medium cursor-pointer select-none"
|
|
||||||
>
|
|
||||||
Filtrer aussi par date de création de la demande
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filtrerParCreation && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3 pl-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dateCreationDebut">Date de création — début</Label>
|
|
||||||
<Input
|
|
||||||
id="dateCreationDebut"
|
|
||||||
type="date"
|
|
||||||
value={dateCreationDebut}
|
|
||||||
onChange={(e) => setDateCreationDebut(e.target.value)}
|
|
||||||
className="transition-smooth"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dateCreationFin">Date de création — fin</Label>
|
|
||||||
<Input
|
|
||||||
id="dateCreationFin"
|
|
||||||
type="date"
|
|
||||||
value={dateCreationFin}
|
|
||||||
onChange={(e) => setDateCreationFin(e.target.value)}
|
|
||||||
className="transition-smooth"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{dateCreationDebut && dateCreationFin && (
|
|
||||||
<div className="md:col-span-2 p-3 bg-purple-50 border border-purple-200 rounded-lg">
|
|
||||||
<p className="text-sm text-purple-700">
|
|
||||||
<strong>Filtre actif :</strong> Congés du mois de {moisOptions.find(m => m.value === mois)?.label} {annee},
|
|
||||||
dont la demande a été créée entre le {new Date(dateCreationDebut).toLocaleDateString('fr-FR')} et le {new Date(dateCreationFin).toLocaleDateString('fr-FR')}.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Mode Plage de dates (période du congé) */}
|
|
||||||
<TabsContent value="plage">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dateDebut">Date de début du congé</Label>
|
|
||||||
<Input
|
|
||||||
id="dateDebut"
|
|
||||||
type="date"
|
|
||||||
value={dateDebut}
|
|
||||||
onChange={(e) => setDateDebut(e.target.value)}
|
|
||||||
className="transition-smooth"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dateFin">Date de fin du congé</Label>
|
|
||||||
<Input
|
|
||||||
id="dateFin"
|
|
||||||
type="date"
|
|
||||||
value={dateFin}
|
|
||||||
onChange={(e) => setDateFin(e.target.value)}
|
|
||||||
className="transition-smooth"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end">
|
|
||||||
<Button
|
|
||||||
onClick={genererRapport}
|
|
||||||
disabled={loading || !dateDebut || !dateFin}
|
|
||||||
className="w-full bg-[#7e5aa2] hover:bg-[#6b4891] text-white transition-smooth shadow-elegant"
|
|
||||||
>
|
|
||||||
{loading ? 'Génération...' : 'Générer le rapport'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{dateDebut && dateFin && (
|
|
||||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<p className="text-sm text-blue-700">
|
|
||||||
<strong>Période du congé :</strong> Du {new Date(dateDebut).toLocaleDateString('fr-FR')} au {new Date(dateFin).toLocaleDateString('fr-FR')}
|
|
||||||
{(() => {
|
|
||||||
const debut = new Date(dateDebut);
|
|
||||||
const fin = new Date(dateFin);
|
|
||||||
const nbJours = Math.ceil((fin.getTime() - debut.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
||||||
return ` (${nbJours} jour${nbJours > 1 ? 's' : ''})`;
|
|
||||||
})()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Mode Plage de saisie (date de création de la demande) */}
|
|
||||||
<TabsContent value="saisie">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dateSaisieDebut">Date de saisie début</Label>
|
|
||||||
<Input
|
|
||||||
id="dateSaisieDebut"
|
|
||||||
type="date"
|
|
||||||
value={dateSaisieDebut}
|
|
||||||
onChange={(e) => setDateSaisieDebut(e.target.value)}
|
|
||||||
className="transition-smooth"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dateSaisieFin">Date de saisie fin</Label>
|
|
||||||
<Input
|
|
||||||
id="dateSaisieFin"
|
|
||||||
type="date"
|
|
||||||
value={dateSaisieFin}
|
|
||||||
onChange={(e) => setDateSaisieFin(e.target.value)}
|
|
||||||
className="transition-smooth"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end">
|
|
||||||
<Button
|
|
||||||
onClick={genererRapport}
|
|
||||||
disabled={loading || !dateSaisieDebut || !dateSaisieFin}
|
|
||||||
className="w-full bg-[#7e5aa2] hover:bg-[#6b4891] text-white transition-smooth shadow-elegant"
|
|
||||||
>
|
|
||||||
{loading ? 'Génération...' : 'Générer le rapport'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{dateSaisieDebut && dateSaisieFin && (
|
|
||||||
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
|
||||||
<p className="text-sm text-amber-700">
|
|
||||||
<strong>Demandes saisies entre le :</strong> {new Date(dateSaisieDebut).toLocaleDateString('fr-FR')} et le {new Date(dateSaisieFin).toLocaleDateString('fr-FR')}
|
|
||||||
{(() => {
|
|
||||||
const debut = new Date(dateSaisieDebut);
|
|
||||||
const fin = new Date(dateSaisieFin);
|
|
||||||
const nbJours = Math.ceil((fin.getTime() - debut.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
||||||
return ` (${nbJours} jour${nbJours > 1 ? 's' : ''} de saisie)`;
|
|
||||||
})()}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-amber-600 mt-1">
|
|
||||||
Remonte toutes les demandes créées dans cette fenêtre, quelle que soit la période du congé.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Bouton Export Excel toujours affiché, grisé s’il n’y a pas de données */}
|
||||||
<div className="flex gap-4 mb-6">
|
<div className="flex gap-4 mb-6">
|
||||||
<Button
|
<Button
|
||||||
onClick={exporterExcel}
|
onClick={exporterExcel}
|
||||||
@@ -645,34 +396,17 @@ const ExportPaie = () => {
|
|||||||
variant="default"
|
variant="default"
|
||||||
className="bg-[#7e5aa2] hover:bg-[#6b4891] text-white transition-smooth shadow-elegant"
|
className="bg-[#7e5aa2] hover:bg-[#6b4891] text-white transition-smooth shadow-elegant"
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4 mr-2" />Export Excel (avec périodes)
|
<Download className="w-4 h-4 mr-2" />Export Excel (avec couleurs)
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tableau des données uniquement si data présente */}
|
||||||
{dataPaie.length > 0 ? (
|
{dataPaie.length > 0 ? (
|
||||||
<Card className="shadow-card border-0">
|
<Card className="shadow-card border-0">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{getTitreRapport()}</CardTitle>
|
<CardTitle>Rapport des congés - {moisOptions.find(m => m.value === mois)?.label} {annee}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
AAP = Absence autorisée payée (ex : enfant malade, récup de JPOSF, etc) • AM = Arrêt maladie
|
AAP = Absence autorisée payée (ex : enfant malade, récup de JPOFS, etc) • AM = Arrêt maladie
|
||||||
{modeExport === "saisie" && (
|
|
||||||
<span className="ml-2 text-amber-600">• Filtré par date de saisie de la demande</span>
|
|
||||||
)}
|
|
||||||
{modeExport === "mois" && filtrerParCreation && (
|
|
||||||
<span className="ml-2 text-purple-600">• Filtré par date de création de la demande</span>
|
|
||||||
)}
|
|
||||||
<br />
|
|
||||||
<div className="flex flex-wrap gap-2 mt-2">
|
|
||||||
<Badge variant="outline" className="bg-yellow-50 border-yellow-300">
|
|
||||||
<Sun className="w-3 h-3 mr-1 text-yellow-500" />Journée
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline" className="bg-orange-50 border-orange-300">
|
|
||||||
<Sunrise className="w-3 h-3 mr-1 text-orange-500" />Matin
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline" className="bg-blue-50 border-blue-300">
|
|
||||||
<Sunset className="w-3 h-3 mr-1 text-blue-500" />Après-midi
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -681,122 +415,133 @@ const ExportPaie = () => {
|
|||||||
<table className="w-full border-collapse">
|
<table className="w-full border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b-[2px] border-black bg-gray-100">
|
<tr className="border-b-[2px] border-black bg-gray-100">
|
||||||
<th className="font-bold text-black border-r-[2px] border-black p-4 text-left">Collaborateur</th>
|
<th className="font-bold text-black border-r-[2px] border-black p-4 text-left">
|
||||||
<th className="font-bold text-black border-r-[2px] border-black p-4 text-left">Service</th>
|
Collaborateur
|
||||||
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">RTT<br /><span className="text-xs font-normal">(Nb / Dates)</span></th>
|
</th>
|
||||||
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">CP<br /><span className="text-xs font-normal">(Nb / Dates)</span></th>
|
<th className="font-bold text-black border-r-[2px] border-black p-4 text-left">
|
||||||
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">AAP<br /><span className="text-xs font-normal">(Nb / Dates)</span></th>
|
Service
|
||||||
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">AM<br /><span className="text-xs font-normal">(Nb / Dates)</span></th>
|
</th>
|
||||||
<th className="font-bold text-black p-4 text-center">Autres<br /><span className="text-xs font-normal">(Type / Nb / Dates)</span></th>
|
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">
|
||||||
|
RTT<br />
|
||||||
|
<span className="text-xs font-normal">(Nb / Dates)</span>
|
||||||
|
</th>
|
||||||
|
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">
|
||||||
|
CP<br />
|
||||||
|
<span className="text-xs font-normal">(Nb / Dates)</span>
|
||||||
|
</th>
|
||||||
|
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">
|
||||||
|
AAP<br />
|
||||||
|
<span className="text-xs font-normal">(Nb / Dates)</span>
|
||||||
|
</th>
|
||||||
|
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">
|
||||||
|
AM<br />
|
||||||
|
<span className="text-xs font-normal">(Nb / Dates)</span>
|
||||||
|
</th>
|
||||||
|
<th className="font-bold text-black p-4 text-center">
|
||||||
|
Autres<br />
|
||||||
|
<span className="text-xs font-normal">(Type / Nb / Dates)</span>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{dataPaie.map((row, index) => {
|
{dataPaie.map((row, index) => (
|
||||||
const rttDates = row.rtt?.dates ? row.rtt.dates.split(' ; ') : [];
|
<tr
|
||||||
const rttPeriodes = row.rtt?.periodes ? row.rtt.periodes.split(' ; ') : [];
|
key={index}
|
||||||
const cpDates = row.cp?.dates ? row.cp.dates.split(' ; ') : [];
|
className="border-b-[2px] border-black last:border-b-0 hover:bg-gray-50"
|
||||||
const cpPeriodes = row.cp?.periodes ? row.cp.periodes.split(' ; ') : [];
|
>
|
||||||
const aapDates = row.aap?.dates ? row.aap.dates.split(' ; ') : [];
|
<td className="font-medium border-r-[2px] border-black p-4 align-top bg-white">
|
||||||
const aapPeriodes = row.aap?.periodes ? row.aap.periodes.split(' ; ') : [];
|
{row.employe}
|
||||||
const amDates = row.am?.dates ? row.am.dates.split(' ; ') : [];
|
</td>
|
||||||
const amPeriodes = row.am?.periodes ? row.am.periodes.split(' ; ') : [];
|
<td className="border-r-[2px] border-black p-4 align-top bg-white">
|
||||||
|
{row.service}
|
||||||
return (
|
</td>
|
||||||
<tr key={index} className="border-b-[2px] border-black last:border-b-0 hover:bg-gray-50">
|
|
||||||
<td className="font-medium border-r-[2px] border-black p-4 align-top bg-white">{row.employe}</td>
|
|
||||||
<td className="border-r-[2px] border-black p-4 align-top bg-white">{row.service}</td>
|
|
||||||
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
|
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
|
||||||
{row.rtt && row.rtt.nb > 0 ? (
|
{row.rtt && row.rtt.nb > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="font-semibold text-lg">{row.rtt.nb}</div>
|
<div className="font-semibold text-lg">{row.rtt.nb}</div>
|
||||||
<div className="text-xs text-gray-600 space-y-1">
|
<div className="text-xs text-gray-600 space-y-1">
|
||||||
{rttDates.map((date, idx) => (
|
{row.rtt.dates.split(' ; ').map((date, idx) => (
|
||||||
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0 flex items-center justify-center gap-1">
|
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0">
|
||||||
{getPeriodeIcon(rttPeriodes[idx])}
|
{date}
|
||||||
<span>{date}</span>
|
|
||||||
<span className="ml-1 text-xs text-gray-500">({getPeriodeLabel(rttPeriodes[idx])})</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : <span className="text-gray-400">-</span>}
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
|
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
|
||||||
{row.cp && row.cp.nb > 0 ? (
|
{row.cp && row.cp.nb > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="font-semibold text-lg">{row.cp.nb}</div>
|
<div className="font-semibold text-lg">{row.cp.nb}</div>
|
||||||
<div className="text-xs text-gray-600 space-y-1">
|
<div className="text-xs text-gray-600 space-y-1">
|
||||||
{cpDates.map((date, idx) => (
|
{row.cp.dates.split(' ; ').map((date, idx) => (
|
||||||
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0 flex items-center justify-center gap-1">
|
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0">
|
||||||
{getPeriodeIcon(cpPeriodes[idx])}
|
{date}
|
||||||
<span>{date}</span>
|
|
||||||
<span className="ml-1 text-xs text-gray-500">({getPeriodeLabel(cpPeriodes[idx])})</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : <span className="text-gray-400">-</span>}
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
|
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
|
||||||
{row.aap && row.aap.nb > 0 ? (
|
{row.aap && row.aap.nb > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="font-semibold text-lg">{row.aap.nb}</div>
|
<div className="font-semibold text-lg">{row.aap.nb}</div>
|
||||||
<div className="text-xs text-gray-600 space-y-1">
|
<div className="text-xs text-gray-600 space-y-1">
|
||||||
{aapDates.map((date, idx) => (
|
{row.aap.dates.split(' ; ').map((date, idx) => (
|
||||||
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0 flex items-center justify-center gap-1">
|
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0">
|
||||||
{getPeriodeIcon(aapPeriodes[idx])}
|
{date}
|
||||||
<span>{date}</span>
|
|
||||||
<span className="ml-1 text-xs text-gray-500">({getPeriodeLabel(aapPeriodes[idx])})</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : <span className="text-gray-400">-</span>}
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
|
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
|
||||||
{row.am && row.am.nb > 0 ? (
|
{row.am && row.am.nb > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="font-semibold text-lg">{row.am.nb}</div>
|
<div className="font-semibold text-lg">{row.am.nb}</div>
|
||||||
<div className="text-xs text-gray-600 space-y-1">
|
<div className="text-xs text-gray-600 space-y-1">
|
||||||
{amDates.map((date, idx) => (
|
{row.am.dates.split(' ; ').map((date, idx) => (
|
||||||
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0 flex items-center justify-center gap-1">
|
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0">
|
||||||
{getPeriodeIcon(amPeriodes[idx])}
|
{date}
|
||||||
<span>{date}</span>
|
|
||||||
<span className="ml-1 text-xs text-gray-500">({getPeriodeLabel(amPeriodes[idx])})</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : <span className="text-gray-400">-</span>}
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center bg-white p-4 align-top">
|
<td className="text-center bg-white p-4 align-top">
|
||||||
{row.autres && row.autres.length > 0 ? (
|
{row.autres && row.autres.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{row.autres.map((autre, idx) => {
|
{row.autres.map((autre, idx) => (
|
||||||
const autresDates = autre.dates.split(' ; ');
|
|
||||||
const autresPeriodes = autre.periodes ? autre.periodes.split(' ; ') : [];
|
|
||||||
return (
|
|
||||||
<div key={idx} className="space-y-2 pb-3 border-b-[2px] border-gray-300 last:border-0 last:pb-0">
|
<div key={idx} className="space-y-2 pb-3 border-b-[2px] border-gray-300 last:border-0 last:pb-0">
|
||||||
<div className="text-xs font-medium text-gray-700">{autre.type}</div>
|
<div className="text-xs font-medium text-gray-700">{autre.type}</div>
|
||||||
<div className="font-semibold text-lg">{autre.nb}</div>
|
<div className="font-semibold text-lg">{autre.nb}</div>
|
||||||
<div className="text-xs text-gray-600 space-y-1">
|
<div className="text-xs text-gray-600 space-y-1">
|
||||||
{autresDates.map((date, dateIdx) => (
|
{autre.dates.split(' ; ').map((date, dateIdx) => (
|
||||||
<div key={dateIdx} className="py-1 border-t border-gray-300 first:border-t-0 flex items-center justify-center gap-1">
|
<div key={dateIdx} className="py-1 border-t border-gray-300 first:border-t-0">
|
||||||
{getPeriodeIcon(autresPeriodes[dateIdx])}
|
{date}
|
||||||
<span>{date}</span>
|
|
||||||
<span className="ml-1 text-xs text-gray-500">({getPeriodeLabel(autresPeriodes[dateIdx])})</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
) : <span className="text-gray-400">-</span>}
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -807,11 +552,7 @@ const ExportPaie = () => {
|
|||||||
<Card className="shadow-card border-0">
|
<Card className="shadow-card border-0">
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-12 text-center">
|
||||||
<FileSpreadsheet className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
|
<FileSpreadsheet className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
|
||||||
<p className="text-muted-foreground text-lg">
|
<p className="text-muted-foreground text-lg">Sélectionnez une période et cliquez sur "Générer le rapport"</p>
|
||||||
{modeExport === "mois" && "Sélectionnez un mois et cliquez sur \"Générer le rapport\""}
|
|
||||||
{modeExport === "plage" && "Sélectionnez une plage de dates de congé et cliquez sur \"Générer le rapport\""}
|
|
||||||
{modeExport === "saisie" && "Sélectionnez une plage de dates de saisie et cliquez sur \"Générer le rapport\""}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+427
-819
File diff suppressed because it is too large
Load Diff
+435
-552
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -15,7 +17,6 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
|||||||
interface HistoriqueAction {
|
interface HistoriqueAction {
|
||||||
Id: number;
|
Id: number;
|
||||||
collaborateur: string;
|
collaborateur: string;
|
||||||
realisePar: string;
|
|
||||||
Action: string;
|
Action: string;
|
||||||
Details: string;
|
Details: string;
|
||||||
DateAction: string;
|
DateAction: string;
|
||||||
@@ -50,39 +51,20 @@ const Historique = () => {
|
|||||||
`/api/historique?${params.toString()}`,
|
`/api/historique?${params.toString()}`,
|
||||||
{ headers: { 'Authorization': `Bearer ${token}` } }
|
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||||||
);
|
);
|
||||||
|
|
||||||
// ⭐ VÉRIFICATION
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Erreur ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// ⭐ SÉCURITÉ : Vérifier que c'est un tableau
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
setHistorique(data);
|
setHistorique(data);
|
||||||
} else {
|
|
||||||
console.error('❌ Format historique invalide:', data);
|
|
||||||
setHistorique([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Erreur chargement historique:', error);
|
|
||||||
toast.error("Erreur lors du chargement de l'historique");
|
toast.error("Erreur lors du chargement de l'historique");
|
||||||
setHistorique([]); // ⭐ Toujours mettre un tableau vide en cas d'erreur
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ SÉCURITÉ : Vérifier que historique est un tableau avant filter
|
const historiqueFiltré = historique.filter(h => {
|
||||||
const historiqueFiltré = Array.isArray(historique)
|
|
||||||
? historique.filter(h => {
|
|
||||||
const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) ||
|
const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) ||
|
||||||
h.realisePar?.toLowerCase().includes(recherche.toLowerCase()) ||
|
|
||||||
h.Details?.toLowerCase().includes(recherche.toLowerCase());
|
h.Details?.toLowerCase().includes(recherche.toLowerCase());
|
||||||
return matchRecherche;
|
return matchRecherche;
|
||||||
})
|
});
|
||||||
: [];
|
|
||||||
|
|
||||||
const getActionIcon = (action: string) => {
|
const getActionIcon = (action: string) => {
|
||||||
if (action.includes('Validation')) return <CheckCircle className="w-4 h-4 text-success" />;
|
if (action.includes('Validation')) return <CheckCircle className="w-4 h-4 text-success" />;
|
||||||
@@ -127,13 +109,12 @@ const Historique = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = ['Date et heure', 'Collaborateur concerné', 'Action réalisée par', 'Action', 'Détails', 'Demande ID'];
|
const headers = ['Date et heure', 'Collaborateur', 'Action', 'Détails', 'Demande ID'];
|
||||||
const csvContent = [
|
const csvContent = [
|
||||||
headers.join(';'),
|
headers.join(';'),
|
||||||
...historiqueFiltré.map(row => [
|
...historiqueFiltré.map(row => [
|
||||||
formatDateTime(row.DateAction),
|
formatDateTime(row.DateAction),
|
||||||
row.collaborateur,
|
row.collaborateur,
|
||||||
row.realisePar,
|
|
||||||
row.Action,
|
row.Action,
|
||||||
row.Details,
|
row.Details,
|
||||||
row.DemandeCongeId || ''
|
row.DemandeCongeId || ''
|
||||||
@@ -302,8 +283,7 @@ const Historique = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Date et heure</TableHead>
|
<TableHead>Date et heure</TableHead>
|
||||||
<TableHead>Collaborateur concerné</TableHead>
|
<TableHead>Collaborateur</TableHead>
|
||||||
<TableHead>Action réalisée par</TableHead>
|
|
||||||
<TableHead>Action</TableHead>
|
<TableHead>Action</TableHead>
|
||||||
<TableHead>Détails</TableHead>
|
<TableHead>Détails</TableHead>
|
||||||
<TableHead className="text-center">Demande</TableHead>
|
<TableHead className="text-center">Demande</TableHead>
|
||||||
@@ -316,11 +296,6 @@ const Historique = () => {
|
|||||||
{formatDateTime(action.DateAction)}
|
{formatDateTime(action.DateAction)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{action.collaborateur}</TableCell>
|
<TableCell>{action.collaborateur}</TableCell>
|
||||||
<TableCell>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{action.realisePar || '-'}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{getActionIcon(action.Action)}
|
{getActionIcon(action.Action)}
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ const LoginDev = () => {
|
|||||||
{loading ? 'Connexion...' : 'Se connecter'}
|
{loading ? 'Connexion...' : 'Se connecter'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="text-xs text-center text-muted-foreground pt-2 border-t">
|
<p className="text-xs text-center text-muted-foreground">
|
||||||
Version 2.0.0
|
⚠️ Mode développement uniquement
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Calendar, ArrowLeft, Save, Trash2, Info, Plus } from "lucide-react";
|
import { Calendar, ArrowLeft, Save, Trash2, Info } from "lucide-react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { useSSE } from "@/hooks/useSSE";
|
import { useSSE } from "@/hooks/useSSE";
|
||||||
|
|
||||||
@@ -17,7 +16,6 @@ interface Demande {
|
|||||||
Id: number;
|
Id: number;
|
||||||
CollaborateurADId: number;
|
CollaborateurADId: number;
|
||||||
nomEmploye: string;
|
nomEmploye: string;
|
||||||
prenomEmploye: string;
|
|
||||||
emailEmploye: string;
|
emailEmploye: string;
|
||||||
service: string;
|
service: string;
|
||||||
DateDebut: string;
|
DateDebut: string;
|
||||||
@@ -32,24 +30,12 @@ interface Demande {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TypeConge {
|
|
||||||
id: number;
|
|
||||||
nom: string;
|
|
||||||
couleur: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LigneConge {
|
|
||||||
typeId: number;
|
|
||||||
nombreJours: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModificationDemande = () => {
|
const ModificationDemande = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [demande, setDemande] = useState<Demande | null>(null);
|
const [demande, setDemande] = useState<Demande | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [typesCongeDisponibles, setTypesCongeDisponibles] = useState<TypeConge[]>([]);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
dateDebut: "",
|
dateDebut: "",
|
||||||
@@ -61,7 +47,6 @@ const ModificationDemande = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
chargerDemande(parseInt(id));
|
chargerDemande(parseInt(id));
|
||||||
chargerTypesConge();
|
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
@@ -138,87 +123,6 @@ const ModificationDemande = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const chargerTypesConge = async () => {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const response = await fetch('/api/types-conge', {
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setTypesCongeDisponibles(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors du chargement des types de congé:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculerJoursOuvres = (debut: string, fin: string) => {
|
|
||||||
if (!debut || !fin) return 0;
|
|
||||||
|
|
||||||
const dateDebut = new Date(debut);
|
|
||||||
const dateFin = new Date(fin);
|
|
||||||
let jours = 0;
|
|
||||||
|
|
||||||
for (let d = new Date(dateDebut); d <= dateFin; d.setDate(d.getDate() + 1)) {
|
|
||||||
const jour = d.getDay();
|
|
||||||
if (jour !== 0 && jour !== 6) {
|
|
||||||
jours++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return jours;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ajouterLigne = () => {
|
|
||||||
const joursOuvres = calculerJoursOuvres(formData.dateDebut, formData.dateFin);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
typesConge: [...formData.typesConge, { typeId: 0, nombreJours: joursOuvres }]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const supprimerLigne = (index: number) => {
|
|
||||||
if (formData.typesConge.length > 1) {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
typesConge: formData.typesConge.filter((_, i) => i !== index)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const modifierLigne = (index: number, field: 'typeId' | 'nombreJours', value: any) => {
|
|
||||||
const nouvelles = [...formData.typesConge];
|
|
||||||
nouvelles[index] = { ...nouvelles[index], [field]: value };
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
typesConge: nouvelles
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDateChange = (field: 'dateDebut' | 'dateFin', value: string) => {
|
|
||||||
const nouvellesData = { ...formData, [field]: value };
|
|
||||||
setFormData(nouvellesData);
|
|
||||||
|
|
||||||
// Mettre à jour automatiquement le nombre de jours pour toutes les lignes
|
|
||||||
if (nouvellesData.dateDebut && nouvellesData.dateFin) {
|
|
||||||
const joursOuvres = calculerJoursOuvres(nouvellesData.dateDebut, nouvellesData.dateFin);
|
|
||||||
const nouvellesLignes = formData.typesConge.map(ligne => ({
|
|
||||||
...ligne,
|
|
||||||
nombreJours: joursOuvres
|
|
||||||
}));
|
|
||||||
setFormData({
|
|
||||||
...nouvellesData,
|
|
||||||
typesConge: nouvellesLignes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const obtenirNomType = (typeId: number): string => {
|
|
||||||
const type = typesCongeDisponibles.find(t => t.id === typeId);
|
|
||||||
return type?.nom || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// ✅ MODIFICATION AVEC VALIDATION AUTOMATIQUE
|
|
||||||
const modifierDemande = async (e: React.FormEvent) => {
|
const modifierDemande = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -230,8 +134,6 @@ const ModificationDemande = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
// 1. Modifier la demande
|
|
||||||
const response = await fetch(`/api/demandes/${id}`, {
|
const response = await fetch(`/api/demandes/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -248,40 +150,22 @@ const ModificationDemande = () => {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (response.ok) {
|
||||||
|
if (data.ancienStatut === 'Validée' && data.nouveauStatut === 'En attente') {
|
||||||
|
toast.success("Demande modifiée et remise en attente pour re-validation", {
|
||||||
|
description: "Les compteurs de congé ont été restaurés"
|
||||||
|
});
|
||||||
|
} else if (data.ancienStatut === 'Refusée' && data.nouveauStatut === 'En attente') {
|
||||||
|
toast.success("Demande modifiée et remise en attente");
|
||||||
|
} else if (data.ancienStatut === 'Annulée' && data.nouveauStatut === 'En attente') {
|
||||||
|
toast.success("Demande modifiée et remise en attente");
|
||||||
|
} else {
|
||||||
|
toast.success("Demande modifiée avec succès");
|
||||||
|
}
|
||||||
|
navigate("/dashboard");
|
||||||
|
} else {
|
||||||
throw new Error(data.error || 'Erreur lors de la modification');
|
throw new Error(data.error || 'Erreur lors de la modification');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Valider automatiquement la demande si elle n'est pas déjà validée
|
|
||||||
if (data.nouveauStatut !== 'Validée') {
|
|
||||||
const validationResponse = await fetch(`/api/demandes/${id}/valider`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const validationData = await validationResponse.json();
|
|
||||||
|
|
||||||
if (validationResponse.ok) {
|
|
||||||
toast.success("✅ Demande modifiée et validée automatiquement", {
|
|
||||||
description: "Les compteurs ont été mis à jour"
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Si la validation échoue, on informe l'utilisateur mais on considère que la modification a réussi
|
|
||||||
toast.warning("⚠️ Demande modifiée mais erreur lors de la validation automatique", {
|
|
||||||
description: validationData.error || "Veuillez valider manuellement"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// La demande était déjà validée, on informe juste de la modification
|
|
||||||
toast.success("✅ Demande modifiée avec succès", {
|
|
||||||
description: "Les compteurs ont été mis à jour"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate("/dashboard");
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error.message || "Erreur lors de la modification");
|
toast.error(error.message || "Erreur lors de la modification");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -404,17 +288,17 @@ const ModificationDemande = () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* ✅ ALERTE MISE À JOUR POUR VALIDATION AUTOMATIQUE */}
|
{/* ✅ ALERTE MISE À JOUR POUR INCLURE LE STATUT "ANNULÉE" */}
|
||||||
{peutModifier && demandeTraitee && (
|
{peutModifier && demandeTraitee && (
|
||||||
<Alert className="mb-6 border-green-500/50 bg-green-500/5">
|
<Alert className="mb-6 border-blue-500/50 bg-blue-500/5">
|
||||||
<Info className="h-4 w-4 text-green-500" />
|
<Info className="h-4 w-4 text-blue-500" />
|
||||||
<AlertDescription className="text-green-700 dark:text-green-400">
|
<AlertDescription className="text-blue-700 dark:text-blue-400">
|
||||||
{demande.Statut === 'Validée'
|
{demande.Statut === 'Validée'
|
||||||
? "✅ Cette demande a été validée. En la modifiant, elle sera automatiquement re-validée avec les nouveaux paramètres."
|
? "⚠️ Cette demande a été validée. En la modifiant, elle sera automatiquement remise en attente et les compteurs seront restaurés."
|
||||||
: demande.Statut === 'Refusée'
|
: demande.Statut === 'Refusée'
|
||||||
? "✅ Cette demande a été refusée. En la modifiant, elle sera automatiquement validée."
|
? "ℹ️ Cette demande a été refusée. En la modifiant, elle sera automatiquement remise en attente pour re-validation."
|
||||||
: demande.Statut === 'Annulée'
|
: demande.Statut === 'Annulée'
|
||||||
? "✅ Cette demande a été annulée. En la modifiant, elle sera automatiquement validée."
|
? "ℹ️ Cette demande a été annulée. En la modifiant, elle sera automatiquement remise en attente pour re-validation."
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@@ -477,16 +361,10 @@ const ModificationDemande = () => {
|
|||||||
|
|
||||||
<Card className="shadow-elegant border-0">
|
<Card className="shadow-elegant border-0">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Modifier la demande</CardTitle>
|
<CardTitle>Modifier les dates</CardTitle>
|
||||||
<CardDescription>
|
|
||||||
✅ La demande sera automatiquement validée après modification
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={modifierDemande} className="space-y-6">
|
<form onSubmit={modifierDemande} className="space-y-6">
|
||||||
{/* DATES */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold">Dates</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="dateDebut">Date de début</Label>
|
<Label htmlFor="dateDebut">Date de début</Label>
|
||||||
@@ -494,7 +372,7 @@ const ModificationDemande = () => {
|
|||||||
id="dateDebut"
|
id="dateDebut"
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.dateDebut}
|
value={formData.dateDebut}
|
||||||
onChange={(e) => handleDateChange('dateDebut', e.target.value)}
|
onChange={(e) => setFormData({ ...formData, dateDebut: e.target.value })}
|
||||||
disabled={!peutModifier}
|
disabled={!peutModifier}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -506,123 +384,13 @@ const ModificationDemande = () => {
|
|||||||
id="dateFin"
|
id="dateFin"
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.dateFin}
|
value={formData.dateFin}
|
||||||
onChange={(e) => handleDateChange('dateFin', e.target.value)}
|
onChange={(e) => setFormData({ ...formData, dateFin: e.target.value })}
|
||||||
disabled={!peutModifier}
|
disabled={!peutModifier}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.dateDebut && formData.dateFin && (
|
|
||||||
<div className="p-3 bg-primary/5 rounded-lg">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Jours ouvrés : <span className="font-bold text-primary">{calculerJoursOuvres(formData.dateDebut, formData.dateFin)}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* TYPES DE CONGÉ */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-lg font-semibold">Types de congé</h3>
|
|
||||||
{peutModifier && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={ajouterLigne}
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4 mr-1" />
|
|
||||||
Ajouter un type
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{formData.typesConge.map((ligne, index) => {
|
|
||||||
const maxJours = calculerJoursOuvres(formData.dateDebut, formData.dateFin);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className="space-y-2">
|
|
||||||
<div className="flex gap-3 items-end">
|
|
||||||
<div className="flex-1 space-y-2">
|
|
||||||
<Label>Nature de congé</Label>
|
|
||||||
<Select
|
|
||||||
value={ligne.typeId.toString()}
|
|
||||||
onValueChange={(value) => modifierLigne(index, 'typeId', parseInt(value))}
|
|
||||||
disabled={!peutModifier}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionner" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{typesCongeDisponibles.map(type => (
|
|
||||||
<SelectItem key={type.id} value={type.id.toString()}>
|
|
||||||
{type.nom}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-40 space-y-2">
|
|
||||||
<Label>
|
|
||||||
Nombre de jours
|
|
||||||
{maxJours > 0 && (
|
|
||||||
<span className="text-xs text-muted-foreground ml-1">
|
|
||||||
(max: {maxJours})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.5"
|
|
||||||
min="0"
|
|
||||||
max={maxJours}
|
|
||||||
value={ligne.nombreJours || ''}
|
|
||||||
onChange={(e) => modifierLigne(index, 'nombreJours', parseFloat(e.target.value) || 0)}
|
|
||||||
placeholder={maxJours > 0 ? maxJours.toString() : "0"}
|
|
||||||
disabled={!peutModifier}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{peutModifier && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => supprimerLigne(index)}
|
|
||||||
disabled={formData.typesConge.length === 1}
|
|
||||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ligne.typeId > 0 && ligne.nombreJours > 0 && (
|
|
||||||
<div className="text-xs px-3 py-1.5 rounded-md bg-blue-50 text-blue-700">
|
|
||||||
{obtenirNomType(ligne.typeId)}: {ligne.nombreJours} jour{ligne.nombreJours > 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.typesConge.length > 0 && (
|
|
||||||
<div className="p-3 bg-primary/5 rounded-lg">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Total : <span className="font-bold text-primary">
|
|
||||||
{formData.typesConge.reduce((sum, ligne) => sum + (ligne.nombreJours || 0), 0)} jour{formData.typesConge.reduce((sum, ligne) => sum + (ligne.nombreJours || 0), 0) > 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* COMMENTAIRE */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="commentaire">Commentaire</Label>
|
<Label htmlFor="commentaire">Commentaire</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -644,7 +412,7 @@ const ModificationDemande = () => {
|
|||||||
className="flex-1 bg-gradient-primary hover:opacity-90 transition-smooth shadow-elegant"
|
className="flex-1 bg-gradient-primary hover:opacity-90 transition-smooth shadow-elegant"
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
{loading ? 'Enregistrement...' : 'Modifier et valider automatiquement'}
|
{loading ? 'Enregistrement...' : 'Enregistrer les modifications'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
+464
-1132
File diff suppressed because it is too large
Load Diff
+130
-628
@@ -1,333 +1,104 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Calendar, Search, ArrowLeft, Users, ChevronDown, ChevronUp, UserCog, AlertTriangle } from "lucide-react";
|
import { Calendar, Search, ArrowLeft, Users } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { ValidateurCombobox } from "@/components/ValidateurCombobox";
|
|
||||||
|
|
||||||
interface Collaborateur {
|
interface Team {
|
||||||
id: number;
|
Id: number;
|
||||||
nom: string;
|
|
||||||
prenom: string;
|
|
||||||
email: string;
|
|
||||||
societe: string;
|
|
||||||
societeId: number | null;
|
|
||||||
campus: string;
|
|
||||||
campusId: number | null;
|
|
||||||
service: string;
|
service: string;
|
||||||
serviceId: number | null;
|
nombreMembres: number;
|
||||||
poste: string;
|
demandesEnAttente: number;
|
||||||
role: string;
|
campus: string;
|
||||||
typeContrat: string;
|
|
||||||
dateEntree: string;
|
|
||||||
soldeCPN: number;
|
|
||||||
soldeCPNMoins1: number;
|
|
||||||
soldeRTT: number;
|
|
||||||
soldeRecup: number;
|
|
||||||
nPlus1Id: number | null;
|
|
||||||
nPlus1Nom: string;
|
|
||||||
nPlus2Id: number | null;
|
|
||||||
nPlus2Nom: string;
|
|
||||||
nPlus3Id: number | null;
|
|
||||||
nPlus3Nom: string;
|
|
||||||
Actif: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Societe {
|
|
||||||
id: number;
|
|
||||||
nom: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Campus {
|
|
||||||
id: number;
|
|
||||||
nom: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Service {
|
|
||||||
id: number;
|
|
||||||
nom: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Validateur {
|
|
||||||
id: number;
|
|
||||||
nom: string;
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROLES_DISPONIBLES = [
|
|
||||||
"Collaborateur", "Collaboratrice",
|
|
||||||
"Validateur", "Validatrice",
|
|
||||||
"RH", "Admin",
|
|
||||||
"Directeur de campus", "Directrice de campus",
|
|
||||||
"President",
|
|
||||||
"Apprenti", "Stagiaire",
|
|
||||||
];
|
|
||||||
|
|
||||||
const EDITION_INITIALE = {
|
|
||||||
id: 0,
|
|
||||||
nom: "",
|
|
||||||
prenom: "",
|
|
||||||
email: "",
|
|
||||||
societeId: "",
|
|
||||||
campusId: "",
|
|
||||||
serviceId: "",
|
|
||||||
poste: "",
|
|
||||||
role: "",
|
|
||||||
typeContrat: "",
|
|
||||||
dateEntree: "",
|
|
||||||
nPlus1Id: "",
|
|
||||||
nPlus2Id: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const estProfilIncomplet = (c: Collaborateur) =>
|
|
||||||
!c.societeId || !c.campusId || !c.serviceId || !c.dateEntree || !c.nPlus1Id;
|
|
||||||
|
|
||||||
const Teams = () => {
|
const Teams = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [filterSociete, setFilterSociete] = useState("all");
|
const [filterDepartment, setFilterDepartment] = useState("all");
|
||||||
const [filterCampus, setFilterCampus] = useState("all");
|
const [filterCampus, setFilterCampus] = useState("all");
|
||||||
const [filterService, setFilterService] = useState("all");
|
const [teams, setTeams] = useState<Team[]>([]);
|
||||||
const [filterContrat, setFilterContrat] = useState("all");
|
|
||||||
|
|
||||||
const [collaborateurs, setCollaborateurs] = useState<Collaborateur[]>([]);
|
|
||||||
const [societes, setSocietes] = useState<Societe[]>([]);
|
|
||||||
const [campusList, setCampusList] = useState<Campus[]>([]);
|
|
||||||
const [services, setServices] = useState<Service[]>([]);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
|
|
||||||
|
|
||||||
const [validateurs, setValidateurs] = useState<Validateur[]>([]);
|
// Mapping des campus
|
||||||
const [editionOuverte, setEditionOuverte] = useState(false);
|
const getCampusFullName = (code: string): string => {
|
||||||
const [edition, setEdition] = useState(EDITION_INITIALE);
|
const mapping: Record<string, string> = {
|
||||||
const [enregistrement, setEnregistrement] = useState(false);
|
'CGY': 'Cergy',
|
||||||
|
'Marseille': 'Marseille',
|
||||||
|
'Nantes': 'Nantes',
|
||||||
|
'SQY': 'Saint-Quentin-en-Yvelines',
|
||||||
|
'Non assigné': 'Non assigné'
|
||||||
|
};
|
||||||
|
return mapping[code] || code;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chargerDonnees();
|
chargerEquipes();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const chargerValidateurs = async () => {
|
const chargerEquipes = async () => {
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const response = await fetch('/api/collaborateurs/validateurs', {
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
setValidateurs(await response.json());
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur chargement validateurs:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ouvrirEdition = (collab: Collaborateur) => {
|
|
||||||
setEdition({
|
|
||||||
id: collab.id,
|
|
||||||
nom: collab.nom || "",
|
|
||||||
prenom: collab.prenom || "",
|
|
||||||
email: collab.email || "",
|
|
||||||
societeId: collab.societeId ? String(collab.societeId) : "",
|
|
||||||
campusId: collab.campusId ? String(collab.campusId) : "",
|
|
||||||
serviceId: collab.serviceId ? String(collab.serviceId) : "",
|
|
||||||
poste: collab.poste || "",
|
|
||||||
role: collab.role || "",
|
|
||||||
typeContrat: collab.typeContrat || "",
|
|
||||||
dateEntree: collab.dateEntree ? collab.dateEntree.split("T")[0] : "",
|
|
||||||
nPlus1Id: collab.nPlus1Id ? String(collab.nPlus1Id) : "",
|
|
||||||
nPlus2Id: collab.nPlus2Id ? String(collab.nPlus2Id) : "",
|
|
||||||
});
|
|
||||||
chargerValidateurs();
|
|
||||||
setEditionOuverte(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const enregistrerEdition = async () => {
|
|
||||||
if (!edition.role) {
|
|
||||||
toast.error("Le rôle est obligatoire");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEnregistrement(true);
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const response = await fetch(`/api/collaborateurs/${edition.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
nom: edition.nom,
|
|
||||||
prenom: edition.prenom,
|
|
||||||
email: edition.email,
|
|
||||||
societeId: edition.societeId ? parseInt(edition.societeId) : null,
|
|
||||||
campusId: edition.campusId ? parseInt(edition.campusId) : null,
|
|
||||||
serviceId: edition.serviceId ? parseInt(edition.serviceId) : null,
|
|
||||||
poste: edition.poste || null,
|
|
||||||
role: edition.role || null,
|
|
||||||
typeContrat: edition.typeContrat || null,
|
|
||||||
dateEntree: edition.dateEntree || null,
|
|
||||||
nPlus1Id: edition.nPlus1Id ? parseInt(edition.nPlus1Id) : null,
|
|
||||||
nPlus2Id: edition.nPlus2Id ? parseInt(edition.nPlus2Id) : null,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(data.error || "Erreur lors de la mise à jour");
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("Collaborateur mis à jour avec succès");
|
|
||||||
setEditionOuverte(false);
|
|
||||||
chargerDonnees();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(error instanceof Error ? error.message : "Erreur lors de la mise à jour");
|
|
||||||
} finally {
|
|
||||||
setEnregistrement(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const chargerDonnees = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const headers = { 'Authorization': `Bearer ${token}` };
|
const response = await fetch('/api/equipes', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
// Charger les collaborateurs avec toutes leurs infos
|
});
|
||||||
const collabResponse = await fetch('/api/collaborateurs/details', { headers });
|
const data = await response.json();
|
||||||
if (collabResponse.ok) {
|
setTeams(data || []);
|
||||||
const collabData = await collabResponse.json();
|
|
||||||
setCollaborateurs(collabData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charger les sociétés
|
|
||||||
const societesResponse = await fetch('/api/societes', { headers });
|
|
||||||
if (societesResponse.ok) {
|
|
||||||
const societesData = await societesResponse.json();
|
|
||||||
setSocietes(societesData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charger les campus
|
|
||||||
const campusResponse = await fetch('/api/campus', { headers });
|
|
||||||
if (campusResponse.ok) {
|
|
||||||
const campusData = await campusResponse.json();
|
|
||||||
setCampusList(campusData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charger les services
|
|
||||||
const servicesResponse = await fetch('/api/services', { headers });
|
|
||||||
if (servicesResponse.ok) {
|
|
||||||
const servicesData = await servicesResponse.json();
|
|
||||||
setServices(servicesData);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur:', error);
|
console.error('Erreur:', error);
|
||||||
toast.error("Erreur lors du chargement des données");
|
toast.error("Erreur lors du chargement des équipes");
|
||||||
|
setTeams([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleRowExpansion = (id: number) => {
|
// Obtenir la liste des campus d'une équipe
|
||||||
const newExpanded = new Set(expandedRows);
|
const getCampusList = (campus: string): string[] => {
|
||||||
if (newExpanded.has(id)) {
|
if (!campus) return ['Non assigné'];
|
||||||
newExpanded.delete(id);
|
return campus.split(',').map(c => c.trim());
|
||||||
} else {
|
|
||||||
newExpanded.add(id);
|
|
||||||
}
|
|
||||||
setExpandedRows(newExpanded);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeContratLabel = (type: string) => {
|
// Filtrage des équipes
|
||||||
switch (type) {
|
const filteredTeams = teams.filter(team => {
|
||||||
case 'forfait_jour': return 'Forfait jours';
|
const campusList = getCampusList(team.campus);
|
||||||
case '37h': return '37h';
|
|
||||||
case 'Apprentissage': return 'Alternant';
|
|
||||||
case 'tempspartiel': return 'Temps partiel';
|
|
||||||
default: return type || 'Non défini';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRoleBadge = (role: string) => {
|
// Recherche
|
||||||
switch (role) {
|
const matchesSearch = !searchQuery ||
|
||||||
case 'Admin':
|
team.service?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
return <Badge variant="destructive">{role}</Badge>;
|
campusList.some(c =>
|
||||||
case 'RH':
|
c.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
return <Badge variant="default">{role}</Badge>;
|
getCampusFullName(c).toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
case 'Validateur':
|
|
||||||
return <Badge variant="secondary">{role}</Badge>;
|
|
||||||
case 'Directeur de campus':
|
|
||||||
return <Badge className="bg-purple-600">{role}</Badge>;
|
|
||||||
case 'Apprenti':
|
|
||||||
return <Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300">{role}</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge variant="outline">{role || 'Non défini'}</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTypeContratBadge = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'forfait_jour':
|
|
||||||
return <Badge variant="outline" className="bg-purple-100 text-purple-700 border-purple-300">Forfait jours</Badge>;
|
|
||||||
case '37h':
|
|
||||||
return <Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300">37h</Badge>;
|
|
||||||
case 'Apprentissage':
|
|
||||||
return <Badge variant="outline" className="bg-orange-100 text-orange-700 border-orange-300">35h</Badge>;
|
|
||||||
case 'tempspartiel':
|
|
||||||
return <Badge variant="outline" className="bg-teal-100 text-teal-700 border-teal-300">Temps partiel</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge variant="secondary">{type || 'Non défini'}</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filtrage des collaborateurs
|
|
||||||
const collaborateursFiltres = collaborateurs.filter(collab => {
|
|
||||||
const matchSearch = !searchQuery ||
|
|
||||||
`${collab.prenom} ${collab.nom}`.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
collab.email?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
collab.poste?.toLowerCase().includes(searchQuery.toLowerCase());
|
|
||||||
|
|
||||||
const matchSociete = filterSociete === "all" || collab.societe === filterSociete;
|
|
||||||
const matchCampus = filterCampus === "all" || collab.campus === filterCampus;
|
|
||||||
const matchService = filterService === "all" || collab.service === filterService;
|
|
||||||
const matchContrat = filterContrat === "all" || collab.typeContrat === filterContrat;
|
|
||||||
|
|
||||||
return matchSearch && matchSociete && matchCampus && matchService && matchContrat;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tri par nom
|
|
||||||
const collaborateursTries = [...collaborateursFiltres].sort((a, b) =>
|
|
||||||
`${a.nom} ${a.prenom}`.localeCompare(`${b.nom} ${b.prenom}`)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Profils incomplets (nouveaux arrivants synchronisés depuis Entra sans société/campus/service/date d'entrée/validateur)
|
// Filtre département
|
||||||
const profilsIncomplets = collaborateurs.filter(estProfilIncomplet);
|
const matchesDepartment = filterDepartment === "all" || team.service === filterDepartment;
|
||||||
|
|
||||||
// Listes uniques pour les filtres
|
// Filtre campus
|
||||||
const societesUniques = [...new Set(collaborateurs.map(c => c.societe).filter(Boolean))].sort();
|
const matchesCampus = filterCampus === "all" || campusList.includes(filterCampus);
|
||||||
const campusUniques = [...new Set(collaborateurs.map(c => c.campus).filter(Boolean))].sort();
|
|
||||||
const servicesUniques = [...new Set(collaborateurs.map(c => c.service).filter(Boolean))].sort();
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
return matchesSearch && matchesDepartment && matchesCampus;
|
||||||
if (!dateString) return '-';
|
});
|
||||||
return new Date(dateString).toLocaleDateString('fr-FR');
|
|
||||||
};
|
// Tri par service
|
||||||
|
const sortedTeams = [...filteredTeams].sort((a, b) =>
|
||||||
|
(a.service || '').localeCompare(b.service || '')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Liste unique des campus
|
||||||
|
const allCampus = new Set<string>();
|
||||||
|
teams.forEach(team => {
|
||||||
|
getCampusList(team.campus).forEach(c => allCampus.add(c));
|
||||||
|
});
|
||||||
|
const campusList = Array.from(allCampus).sort();
|
||||||
|
|
||||||
|
// Liste unique des services
|
||||||
|
const departments = Array.from(new Set(teams.map(t => t.service))).filter(Boolean).sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-subtle">
|
<div className="min-h-screen bg-gradient-subtle">
|
||||||
@@ -346,395 +117,126 @@ const Teams = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: "#7e5aa2" }}>
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: "#7e5aa2" }}>
|
||||||
<Users className="w-5 h-5 text-primary-foreground" />
|
<Calendar className="w-5 h-5 text-primary-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<h1 className="text-2xl font-bold text-foreground">Gestion des équipes</h1>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Gestion des collaborateurs</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">{collaborateursTries.length} collaborateur{collaborateursTries.length > 1 ? 's' : ''}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{profilsIncomplets.length > 0 && (
|
|
||||||
<Badge variant="outline" className="bg-amber-100 text-amber-800 border-amber-300 gap-1.5 px-3 py-1.5">
|
|
||||||
<AlertTriangle className="w-4 h-4" />
|
|
||||||
{profilsIncomplets.length} profil{profilsIncomplets.length > 1 ? 's' : ''} à compléter
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Dialog open={editionOuverte} onOpenChange={setEditionOuverte}>
|
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Modifier {edition.prenom} {edition.nom}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Renseignez la date d'entrée, le rôle, le validateur, la société, le campus, le service et le type de contrat.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="ed-dateEntree">Date d'entrée</Label>
|
|
||||||
<Input
|
|
||||||
id="ed-dateEntree"
|
|
||||||
type="date"
|
|
||||||
value={edition.dateEntree}
|
|
||||||
onChange={(e) => setEdition({ ...edition, dateEntree: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Rôle</Label>
|
|
||||||
<Select
|
|
||||||
value={edition.role}
|
|
||||||
onValueChange={(v) => setEdition({ ...edition, role: v })}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionner..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{ROLES_DISPONIBLES.map((r) => (
|
|
||||||
<SelectItem key={r} value={r}>{r}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Type de contrat</Label>
|
|
||||||
<Select
|
|
||||||
value={edition.typeContrat}
|
|
||||||
onValueChange={(v) => setEdition({ ...edition, typeContrat: v })}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionner..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="forfait_jour">Forfait jours</SelectItem>
|
|
||||||
<SelectItem value="37h">37h</SelectItem>
|
|
||||||
<SelectItem value="Apprentissage">Alternant</SelectItem>
|
|
||||||
<SelectItem value="tempspartiel">Temps partiel</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Société</Label>
|
|
||||||
<Select
|
|
||||||
value={edition.societeId}
|
|
||||||
onValueChange={(v) => setEdition({ ...edition, societeId: v })}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionner..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{societes.map((s) => (
|
|
||||||
<SelectItem key={s.id} value={String(s.id)}>{s.nom}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Campus</Label>
|
|
||||||
<Select
|
|
||||||
value={edition.campusId}
|
|
||||||
onValueChange={(v) => setEdition({ ...edition, campusId: v })}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionner..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{campusList.map((c) => (
|
|
||||||
<SelectItem key={c.id} value={String(c.id)}>{c.nom}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Service</Label>
|
|
||||||
<Select
|
|
||||||
value={edition.serviceId}
|
|
||||||
onValueChange={(v) => setEdition({ ...edition, serviceId: v })}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionner..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{services.map((s) => (
|
|
||||||
<SelectItem key={s.id} value={String(s.id)}>{s.nom}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 md:col-span-2">
|
|
||||||
<Label>Validateur (N+1)</Label>
|
|
||||||
<ValidateurCombobox
|
|
||||||
validateurs={validateurs}
|
|
||||||
value={edition.nPlus1Id}
|
|
||||||
onChange={(v) => setEdition({ ...edition, nPlus1Id: v })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setEditionOuverte(false)} disabled={enregistrement}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button onClick={enregistrerEdition} disabled={enregistrement}>
|
|
||||||
{enregistrement ? "Enregistrement..." : "Enregistrer"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Profils à compléter (nouveaux arrivants synchronisés depuis Entra) */}
|
|
||||||
{profilsIncomplets.length > 0 && (
|
|
||||||
<Card className="shadow-card border-0 mb-8 border-l-4 border-l-amber-400">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
|
||||||
<CardTitle>Profils à compléter</CardTitle>
|
|
||||||
</div>
|
|
||||||
<CardDescription>
|
|
||||||
Ces collaborateurs ont été créés par la synchronisation Entra mais il manque leur société, campus, service, date d'entrée ou validateur.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{profilsIncomplets.map((collab) => (
|
|
||||||
<div
|
|
||||||
key={collab.id}
|
|
||||||
className="flex items-center justify-between p-3 rounded-lg border border-amber-200 bg-amber-50"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-sm">{collab.nom} {collab.prenom}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">{collab.email}</p>
|
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
|
||||||
{!collab.societeId && <Badge variant="outline" className="text-xs bg-white">Société manquante</Badge>}
|
|
||||||
{!collab.campusId && <Badge variant="outline" className="text-xs bg-white">Campus manquant</Badge>}
|
|
||||||
{!collab.serviceId && <Badge variant="outline" className="text-xs bg-white">Service manquant</Badge>}
|
|
||||||
{!collab.dateEntree && <Badge variant="outline" className="text-xs bg-white">Date d'entrée manquante</Badge>}
|
|
||||||
{!collab.nPlus1Id && <Badge variant="outline" className="text-xs bg-white">Validateur manquant</Badge>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" onClick={() => ouvrirEdition(collab)}>
|
|
||||||
<UserCog className="w-4 h-4 mr-2" />
|
|
||||||
Compléter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Filtres */}
|
|
||||||
<Card className="shadow-card border-0 mb-8">
|
<Card className="shadow-card border-0 mb-8">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
{/* Recherche collaborateur */}
|
{/* Recherche */}
|
||||||
<div className="relative xl:col-span-1">
|
<div className="flex-1 relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Rechercher un collaborateur..."
|
placeholder="Rechercher une équipe ou un campus..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-10 transition-smooth"
|
className="pl-10 transition-smooth"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filtre Société */}
|
|
||||||
<Select value={filterSociete} onValueChange={setFilterSociete}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Toutes les sociétés" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Toutes les sociétés</SelectItem>
|
|
||||||
{societesUniques.map((soc) => (
|
|
||||||
<SelectItem key={soc} value={soc}>{soc}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{/* Filtre Campus */}
|
{/* Filtre Campus */}
|
||||||
<Select value={filterCampus} onValueChange={setFilterCampus}>
|
<Select value={filterCampus} onValueChange={setFilterCampus}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full md:w-[250px]">
|
||||||
<SelectValue placeholder="Tous les campus" />
|
<SelectValue placeholder="Tous les campus" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Tous les campus</SelectItem>
|
<SelectItem value="all">Tous les campus</SelectItem>
|
||||||
{campusUniques.map((campus) => (
|
{campusList.map((campus) => (
|
||||||
<SelectItem key={campus} value={campus}>{campus}</SelectItem>
|
<SelectItem key={campus} value={campus}>
|
||||||
|
{getCampusFullName(campus)}
|
||||||
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* Filtre Service */}
|
{/* Filtre Service */}
|
||||||
<Select value={filterService} onValueChange={setFilterService}>
|
<Select value={filterDepartment} onValueChange={setFilterDepartment}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full md:w-[200px]">
|
||||||
<SelectValue placeholder="Tous les services" />
|
<SelectValue placeholder="Tous les services" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Tous les services</SelectItem>
|
<SelectItem value="all">Tous les services</SelectItem>
|
||||||
{servicesUniques.map((service) => (
|
{departments.map((dept) => (
|
||||||
<SelectItem key={service} value={service}>{service}</SelectItem>
|
<SelectItem key={dept} value={dept}>
|
||||||
|
{dept}
|
||||||
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* Filtre Type de contrat */}
|
|
||||||
<Select value={filterContrat} onValueChange={setFilterContrat}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Tous les contrats" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les contrats</SelectItem>
|
|
||||||
<SelectItem value="forfait_jour">Forfait jours</SelectItem>
|
|
||||||
<SelectItem value="37h">37h</SelectItem>
|
|
||||||
<SelectItem value="Apprentissage">Alternant</SelectItem>
|
|
||||||
<SelectItem value="tempspartiel">Temps partiel</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Liste des collaborateurs */}
|
{/* Contenu */}
|
||||||
<Card className="shadow-card border-0">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Liste des collaborateurs</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Cliquez sur une ligne pour voir les détails complets
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||||
<p className="text-muted-foreground">Chargement des collaborateurs...</p>
|
<p className="text-muted-foreground">Chargement des équipes...</p>
|
||||||
</div>
|
</div>
|
||||||
) : collaborateursTries.length === 0 ? (
|
) : sortedTeams.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
|
<Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
|
||||||
<p className="text-muted-foreground text-lg">Aucun collaborateur trouvé</p>
|
<p className="text-muted-foreground text-lg">Aucune équipe trouvée</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<Table>
|
{sortedTeams.map((team) => {
|
||||||
<TableHeader>
|
const campusArray = getCampusList(team.campus);
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-8"></TableHead>
|
return (
|
||||||
<TableHead>Nom / Prénom</TableHead>
|
<Card
|
||||||
<TableHead>Email</TableHead>
|
key={team.Id}
|
||||||
<TableHead>Poste</TableHead>
|
className="shadow-card border-0 bg-gradient-card hover:shadow-elegant transition-smooth cursor-pointer"
|
||||||
<TableHead>Rôle</TableHead>
|
onClick={() => navigate(`/teams/${team.Id}`)}
|
||||||
<TableHead>Société</TableHead>
|
|
||||||
<TableHead>Campus</TableHead>
|
|
||||||
<TableHead>Service</TableHead>
|
|
||||||
<TableHead>Contrat</TableHead>
|
|
||||||
<TableHead className="text-center">CP N</TableHead>
|
|
||||||
<TableHead className="text-center">CP N-1</TableHead>
|
|
||||||
<TableHead className="text-center">RTT</TableHead>
|
|
||||||
<TableHead className="text-center">Récup</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{collaborateursTries.map((collab) => (
|
|
||||||
<>
|
|
||||||
<TableRow
|
|
||||||
key={collab.id}
|
|
||||||
className="cursor-pointer hover:bg-muted/50"
|
|
||||||
onClick={() => toggleRowExpansion(collab.id)}
|
|
||||||
>
|
>
|
||||||
<TableCell>
|
<CardHeader>
|
||||||
{expandedRows.has(collab.id) ? (
|
<div className="flex items-start justify-between">
|
||||||
<ChevronUp className="w-4 h-4 text-muted-foreground" />
|
<div className="flex-1">
|
||||||
) : (
|
<CardTitle className="text-lg mb-2">{team.service}</CardTitle>
|
||||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
<div className="flex flex-wrap gap-1">
|
||||||
)}
|
{campusArray.map((campus, idx) => (
|
||||||
</TableCell>
|
<Badge key={`${team.Id}-${idx}`} variant="outline" className="text-xs">
|
||||||
<TableCell className="font-medium">
|
{getCampusFullName(campus)}
|
||||||
{collab.nom} {collab.prenom}
|
</Badge>
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">
|
|
||||||
{collab.email}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">{collab.poste || '-'}</TableCell>
|
|
||||||
<TableCell>{getRoleBadge(collab.role)}</TableCell>
|
|
||||||
<TableCell>{collab.societe || '-'}</TableCell>
|
|
||||||
<TableCell>{collab.campus || '-'}</TableCell>
|
|
||||||
<TableCell>{collab.service || '-'}</TableCell>
|
|
||||||
<TableCell>{getTypeContratBadge(collab.typeContrat)}</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<span className={`font-semibold ${collab.soldeCPN <= 0 ? 'text-destructive' : 'text-green-600'}`}>
|
|
||||||
{collab.soldeCPN?.toFixed(1) || '0'}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<span className={`font-semibold ${collab.soldeCPNMoins1 <= 0 ? 'text-muted-foreground' : 'text-blue-600'}`}>
|
|
||||||
{collab.soldeCPNMoins1?.toFixed(1) || '0'}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<span className={`font-semibold ${collab.soldeRTT <= 0 ? 'text-muted-foreground' : 'text-purple-600'}`}>
|
|
||||||
{collab.typeContrat === 'Apprentissage' ? '-' : (collab.soldeRTT?.toFixed(1) || '0')}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<span className={`font-semibold ${collab.soldeRecup <= 0 ? 'text-muted-foreground' : 'text-orange-600'}`}>
|
|
||||||
{collab.soldeRecup?.toFixed(1) || '0'}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
|
|
||||||
{/* Ligne de détails expansible */}
|
|
||||||
{expandedRows.has(collab.id) && (
|
|
||||||
<TableRow className="bg-muted/30">
|
|
||||||
<TableCell colSpan={13}>
|
|
||||||
<div className="p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{/* Infos personnelles */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-semibold text-sm text-primary">Informations personnelles</h4>
|
|
||||||
<div className="text-sm space-y-1">
|
|
||||||
<p><span className="text-muted-foreground">Poste :</span> {collab.poste || '-'}</p>
|
|
||||||
<p><span className="text-muted-foreground">Date d'entrée :</span> {formatDate(collab.dateEntree)}</p>
|
|
||||||
<p><span className="text-muted-foreground">Type contrat :</span> {getTypeContratLabel(collab.typeContrat)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hiérarchie */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-semibold text-sm text-primary">Rattachement hiérarchique</h4>
|
|
||||||
<div className="text-sm space-y-1">
|
|
||||||
<p><span className="text-muted-foreground">N+1 :</span> {collab.nPlus1Nom || '-'}</p>
|
|
||||||
<p><span className="text-muted-foreground">N+2 :</span> {collab.nPlus2Nom || '-'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Compteurs détaillés */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-semibold text-sm text-primary">Compteurs de congés</h4>
|
|
||||||
<div className="text-sm space-y-1">
|
|
||||||
<p><span className="text-muted-foreground">CP année N :</span> <span className="font-medium text-green-600">{collab.soldeCPN?.toFixed(2) || '0'} j</span></p>
|
|
||||||
<p><span className="text-muted-foreground">CP année N-1 :</span> <span className="font-medium text-blue-600">{collab.soldeCPNMoins1?.toFixed(2) || '0'} j</span></p>
|
|
||||||
<p><span className="text-muted-foreground">RTT :</span> <span className="font-medium text-purple-600">{collab.typeContrat === 'Apprentissage' ? 'N/A' : `${collab.soldeRTT?.toFixed(2) || '0'} j`}</span></p>
|
|
||||||
<p><span className="text-muted-foreground">Récupération :</span> <span className="font-medium text-orange-600">{collab.soldeRecup?.toFixed(2) || '0'} j</span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center">
|
||||||
|
<Users className="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Membres</span>
|
||||||
|
<span className="font-medium text-foreground">{team.nombreMembres}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Demandes en attente</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={team.demandesEnAttente > 0
|
||||||
|
? "bg-warning/10 text-warning border-warning/20"
|
||||||
|
: "bg-success/10 text-success border-success/20"}
|
||||||
|
>
|
||||||
|
{team.demandesEnAttente}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+27
-1042
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [ { "path": "./tsconfig.app.json" } ],
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
+8
-14
@@ -1,11 +1,10 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import http from 'http'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [react()],
|
||||||
react()
|
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
@@ -14,22 +13,17 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
port: 3014,
|
port: 3014,
|
||||||
allowedHosts: [
|
allowedHosts: ['mygta-rh.ensup-adm.net', 'localhost'],
|
||||||
'mygta-rh.ensup-adm.net',
|
|
||||||
'localhost'
|
|
||||||
],
|
|
||||||
hmr: {
|
|
||||||
protocol: 'ws',
|
|
||||||
host: 'mygta-rh.ensup-adm.net',
|
|
||||||
clientPort: 80,
|
|
||||||
},
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://172.20.0.2:1433', // ← backend Node, pas SQL Server
|
target: 'http://gtarh-backend:3000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
ws: true,
|
ws: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''), // ✅ RETIRE /api
|
||||||
|
configure: (proxy, options) => {
|
||||||
|
options.agent = new http.Agent({ family: 4 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user