Compare commits

..

1 Commits

Author SHA1 Message Date
oimer bbebf6f44a GTARH_Fonctionnel_V1 2025-12-02 17:57:33 +01:00
21 changed files with 5850 additions and 12330 deletions
+8 -2
View File
@@ -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)
+1 -2
View File
@@ -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"
} }
+1 -1
View File
@@ -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"
}, },
+2640 -5594
View File
File diff suppressed because it is too large Load Diff
+27 -26
View File
@@ -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
+22 -170
View File
@@ -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
View File
@@ -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",
-79
View File
@@ -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>
);
}
File diff suppressed because it is too large Load Diff
+433 -679
View File
File diff suppressed because it is too large Load Diff
+285 -544
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+11 -36
View File
@@ -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();
setHistorique(data);
// ⭐ SÉCURITÉ : Vérifier que c'est un tableau
if (Array.isArray(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) const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) ||
? historique.filter(h => { h.Details?.toLowerCase().includes(recherche.toLowerCase());
const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) || return matchRecherche;
h.realisePar?.toLowerCase().includes(recherche.toLowerCase()) || });
h.Details?.toLowerCase().includes(recherche.toLowerCase());
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)}
+2 -2
View File
@@ -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>
+46 -278
View File
@@ -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,152 +361,36 @@ 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="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4"> <div className="space-y-2">
<h3 className="text-lg font-semibold">Dates</h3> <Label htmlFor="dateDebut">Date de début</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <Input
<div className="space-y-2"> id="dateDebut"
<Label htmlFor="dateDebut">Date de début</Label> type="date"
<Input value={formData.dateDebut}
id="dateDebut" onChange={(e) => setFormData({ ...formData, dateDebut: e.target.value })}
type="date" disabled={!peutModifier}
value={formData.dateDebut} required
onChange={(e) => handleDateChange('dateDebut', e.target.value)} />
disabled={!peutModifier}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="dateFin">Date de fin</Label>
<Input
id="dateFin"
type="date"
value={formData.dateFin}
onChange={(e) => handleDateChange('dateFin', e.target.value)}
disabled={!peutModifier}
required
/>
</div>
</div> </div>
{formData.dateDebut && formData.dateFin && ( <div className="space-y-2">
<div className="p-3 bg-primary/5 rounded-lg"> <Label htmlFor="dateFin">Date de fin</Label>
<p className="text-sm text-muted-foreground"> <Input
Jours ouvrés : <span className="font-bold text-primary">{calculerJoursOuvres(formData.dateDebut, formData.dateFin)}</span> id="dateFin"
</p> type="date"
</div> value={formData.dateFin}
)} onChange={(e) => setFormData({ ...formData, dateFin: e.target.value })}
disabled={!peutModifier}
required
/>
</div>
</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"
+507 -1175
View File
File diff suppressed because it is too large Load Diff
+138 -636
View File
@@ -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) => { // Filtre département
switch (type) { const matchesDepartment = filterDepartment === "all" || team.service === filterDepartment;
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 // Filtre campus
const collaborateursFiltres = collaborateurs.filter(collab => { const matchesCampus = filterCampus === "all" || campusList.includes(filterCampus);
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; return matchesSearch && matchesDepartment && matchesCampus;
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 // Tri par service
const collaborateursTries = [...collaborateursFiltres].sort((a, b) => const sortedTeams = [...filteredTeams].sort((a, b) =>
`${a.nom} ${a.prenom}`.localeCompare(`${b.nom} ${b.prenom}`) (a.service || '').localeCompare(b.service || '')
); );
// Profils incomplets (nouveaux arrivants synchronisés depuis Entra sans société/campus/service/date d'entrée/validateur) // Liste unique des campus
const profilsIncomplets = collaborateurs.filter(estProfilIncomplet); const allCampus = new Set<string>();
teams.forEach(team => {
getCampusList(team.campus).forEach(c => allCampus.add(c));
});
const campusList = Array.from(allCampus).sort();
// Listes uniques pour les filtres // Liste unique des services
const societesUniques = [...new Set(collaborateurs.map(c => c.societe).filter(Boolean))].sort(); const departments = Array.from(new Set(teams.map(t => t.service))).filter(Boolean).sort();
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) => {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('fr-FR');
};
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>
<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>
<h1 className="text-2xl font-bold text-foreground">Gestion des équipes</h1>
</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> </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 é 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"> {loading ? (
<CardHeader> <div className="text-center py-12">
<CardTitle>Liste des collaborateurs</CardTitle> <div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<CardDescription> <p className="text-muted-foreground">Chargement des équipes...</p>
Cliquez sur une ligne pour voir les détails complets </div>
</CardDescription> ) : sortedTeams.length === 0 ? (
</CardHeader> <div className="text-center py-12">
<CardContent> <Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
{loading ? ( <p className="text-muted-foreground text-lg">Aucune équipe trouvée</p>
<div className="text-center py-12"> </div>
<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> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
</div> {sortedTeams.map((team) => {
) : collaborateursTries.length === 0 ? ( const campusArray = getCampusList(team.campus);
<div className="text-center py-12">
<Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" /> return (
<p className="text-muted-foreground text-lg">Aucun collaborateur trouvé</p> <Card
</div> key={team.Id}
) : ( className="shadow-card border-0 bg-gradient-card hover:shadow-elegant transition-smooth cursor-pointer"
<div className="overflow-x-auto"> onClick={() => navigate(`/teams/${team.Id}`)}
<Table> >
<TableHeader> <CardHeader>
<TableRow> <div className="flex items-start justify-between">
<TableHead className="w-8"></TableHead> <div className="flex-1">
<TableHead>Nom / Prénom</TableHead> <CardTitle className="text-lg mb-2">{team.service}</CardTitle>
<TableHead>Email</TableHead> <div className="flex flex-wrap gap-1">
<TableHead>Poste</TableHead> {campusArray.map((campus, idx) => (
<TableHead>Rôle</TableHead> <Badge key={`${team.Id}-${idx}`} variant="outline" className="text-xs">
<TableHead>Société</TableHead> {getCampusFullName(campus)}
<TableHead>Campus</TableHead> </Badge>
<TableHead>Service</TableHead> ))}
<TableHead>Contrat</TableHead> </div>
<TableHead className="text-center">CP N</TableHead> </div>
<TableHead className="text-center">CP N-1</TableHead> <div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center">
<TableHead className="text-center">RTT</TableHead> <Users className="w-6 h-6 text-primary" />
<TableHead className="text-center">Récup</TableHead> </div>
</TableRow> </div>
</TableHeader> </CardHeader>
<TableBody> <CardContent>
{collaborateursTries.map((collab) => ( <div className="space-y-3">
<> <div className="flex items-center justify-between text-sm">
<TableRow <span className="text-muted-foreground">Membres</span>
key={collab.id} <span className="font-medium text-foreground">{team.nombreMembres}</span>
className="cursor-pointer hover:bg-muted/50" </div>
onClick={() => toggleRowExpansion(collab.id)} <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"}
> >
<TableCell> {team.demandesEnAttente}
{expandedRows.has(collab.id) ? ( </Badge>
<ChevronUp className="w-4 h-4 text-muted-foreground" /> </div>
) : ( </div>
<ChevronDown className="w-4 h-4 text-muted-foreground" /> </CardContent>
)} </Card>
</TableCell> );
<TableCell className="font-medium"> })}
{collab.nom} {collab.prenom} </div>
</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>
)}
</CardContent>
</Card>
</main> </main>
</div> </div>
); );
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,10 +1,10 @@
{ {
"files": [], "files": [],
"references": [ { "path": "./tsconfig.app.json" } ], "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": [ "./src/*" ] "@/*": ["./src/*"]
}, },
"noImplicitAny": false, "noImplicitAny": false,
"noUnusedParameters": false, "noUnusedParameters": false,
+8 -14
View File
@@ -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 });
}
} }
} }
}, },