Compare commits

..

2 Commits

Author SHA1 Message Date
oimer 314a98c1ad Ajouter completion des profils nouveaux arrivants et corrections diverses
- Détection des profils incomplets (société/campus/service/date d'entrée/validateur) avec alerte Dashboard et section dédiée dans Équipe
- Formulaire d'édition des collaborateurs (date d'entrée, rôle, validateur N+1, société, campus, service, type de contrat) avec combobox de recherche pour le validateur
- Correction du mapping poste (colonne description au lieu de fonction) et du filtre des validateurs (variantes de rôle: Validateur/Validatrice, Directeur/Directrice, President)
- Fix du proxy Vite vers le backend Docker

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:37:03 +02:00
oimer a7a0e51c7a Modif_GestionDoublonsGTARH_Fonctionnel_V1 2025-12-11 16:47:21 +01:00
21 changed files with 12343 additions and 5863 deletions
+2 -8
View File
@@ -1,10 +1,4 @@
# 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
RH_URL=http://localhost:3001
@@ -13,7 +7,7 @@ WEBHOOK_SECRET=secret-rh-2025
# Configuration Serveur
PORT=3001
PORT=3000
NODE_ENV=development
# Configuration Azure AD (Microsoft Graph)
+2 -1
View File
@@ -103,7 +103,8 @@
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"follow-
s": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
+2 -2
View File
@@ -14,11 +14,11 @@
"dotenv": "^17.2.3",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.15.1",
"mssql": "^11.0.1",
"node-fetch": "^2.7.0",
"pdfkit": "^0.17.2"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}
}
+5615 -2661
View File
File diff suppressed because it is too large Load Diff
+30 -31
View File
@@ -1,46 +1,45 @@
version: '3.8' # ← AJOUTÉ (manquant !)
services:
networks:
gtarh:
external: false
services:
backend:
build:
build:
context: ./Backend
dockerfile: DockerFileGTARH.backend
image: ouijdaneim/gtarh-backend:latest
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:
- "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:
build:
context: .
dockerfile: ./src/DockerFileGTARH.Frontend
args:
- VITE_API_URL=https://mygta-rh.ensup-adm.net:8013
- VITE_API_URL=https://mygta-rh.ensup-adm.net
image: ouijdaneim/gtarh-frontend:latest
container_name: gtarh-frontend
environment:
- NODE_ENV=development
restart: always
networks:
- gtarh
ports:
- "3014:3014"
environment:
- NODE_ENV=production
depends_on:
- backend
networks:
- gtarh-network
restart: unless-stopped
networks:
gtarh-network:
driver: bridge
volumes:
mysql-data:
driver: local
- backend
+170 -22
View File
@@ -55,12 +55,14 @@
"lucide-react": "^0.462.0",
"mysql2": "^3.15.1",
"next-themes": "^0.3.0",
"pdfkit": "^0.17.2",
"pdfkit-table": "^0.1.99",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.61.1",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.1",
"react-router-dom": "^6.30.2",
"recharts": "^2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
@@ -73,7 +75,7 @@
"@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react-swc": "^3.11.0",
@@ -2662,9 +2664,9 @@
"license": "MIT"
},
"node_modules/@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"version": "1.23.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -3275,6 +3277,15 @@
"dev": true,
"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": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
@@ -3469,15 +3480,15 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.25",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz",
"integrity": "sha512-oSVZmGtDPmRZtVDqvdKUi/qgCsWp5IDY29wp8na8Bj4B3cc99hfNzvNhlMkVVxctkAOGUA3Km7MMpBHAnWfcIA==",
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
@@ -4216,6 +4227,15 @@
"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": {
"version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
@@ -4475,6 +4495,15 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -4661,6 +4690,12 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4674,9 +4709,9 @@
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/d3-array": {
@@ -4871,6 +4906,12 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"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": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -5423,7 +5464,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-equals": {
@@ -5566,6 +5606,23 @@
"dev": true,
"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": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -6134,6 +6191,13 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6372,6 +6436,25 @@
"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": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -7104,6 +7187,28 @@
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -7140,6 +7245,11 @@
"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": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -7581,12 +7691,12 @@
}
},
"node_modules/react-router": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0"
"@remix-run/router": "1.23.1"
},
"engines": {
"node": ">=14.0.0"
@@ -7596,13 +7706,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0",
"react-router": "6.30.1"
"@remix-run/router": "1.23.1",
"react-router": "6.30.2"
},
"engines": {
"node": ">=14.0.0"
@@ -7792,6 +7902,12 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -8469,6 +8585,12 @@
"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": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -8634,6 +8756,32 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+4 -2
View File
@@ -58,12 +58,14 @@
"lucide-react": "^0.462.0",
"mysql2": "^3.15.1",
"next-themes": "^0.3.0",
"pdfkit": "^0.17.2",
"pdfkit-table": "^0.1.99",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.61.1",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.1",
"react-router-dom": "^6.30.2",
"recharts": "^2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
@@ -76,7 +78,7 @@
"@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react-swc": "^3.11.0",
+79
View File
@@ -0,0 +1,79 @@
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
+666 -420
View File
File diff suppressed because it is too large Load Diff
+547 -288
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
+36 -11
View File
@@ -1,6 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
@@ -17,6 +15,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
interface HistoriqueAction {
Id: number;
collaborateur: string;
realisePar: string;
Action: string;
Details: string;
DateAction: string;
@@ -51,20 +50,39 @@ const Historique = () => {
`/api/historique?${params.toString()}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
// ⭐ VÉRIFICATION
if (!response.ok) {
throw new Error(`Erreur ${response.status}`);
}
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) {
console.error('❌ Erreur chargement historique:', error);
toast.error("Erreur lors du chargement de l'historique");
setHistorique([]); // ⭐ Toujours mettre un tableau vide en cas d'erreur
} finally {
setLoading(false);
}
};
const historiqueFiltré = historique.filter(h => {
const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) ||
h.Details?.toLowerCase().includes(recherche.toLowerCase());
return matchRecherche;
});
// ⭐ SÉCURITÉ : Vérifier que historique est un tableau avant filter
const historiqueFiltré = Array.isArray(historique)
? historique.filter(h => {
const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) ||
h.realisePar?.toLowerCase().includes(recherche.toLowerCase()) ||
h.Details?.toLowerCase().includes(recherche.toLowerCase());
return matchRecherche;
})
: [];
const getActionIcon = (action: string) => {
if (action.includes('Validation')) return <CheckCircle className="w-4 h-4 text-success" />;
@@ -109,12 +127,13 @@ const Historique = () => {
return;
}
const headers = ['Date et heure', 'Collaborateur', 'Action', 'Détails', 'Demande ID'];
const headers = ['Date et heure', 'Collaborateur concerné', 'Action réalisée par', 'Action', 'Détails', 'Demande ID'];
const csvContent = [
headers.join(';'),
...historiqueFiltré.map(row => [
formatDateTime(row.DateAction),
row.collaborateur,
row.realisePar,
row.Action,
row.Details,
row.DemandeCongeId || ''
@@ -283,7 +302,8 @@ const Historique = () => {
<TableHeader>
<TableRow>
<TableHead>Date et heure</TableHead>
<TableHead>Collaborateur</TableHead>
<TableHead>Collaborateur concerné</TableHead>
<TableHead>Action réalisée par</TableHead>
<TableHead>Action</TableHead>
<TableHead>Détails</TableHead>
<TableHead className="text-center">Demande</TableHead>
@@ -296,6 +316,11 @@ const Historique = () => {
{formatDateTime(action.DateAction)}
</TableCell>
<TableCell>{action.collaborateur}</TableCell>
<TableCell>
<span className="text-muted-foreground">
{action.realisePar || '-'}
</span>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getActionIcon(action.Action)}
+2 -2
View File
@@ -150,8 +150,8 @@ const LoginDev = () => {
{loading ? 'Connexion...' : 'Se connecter'}
</Button>
<p className="text-xs text-center text-muted-foreground">
Mode développement uniquement
<p className="text-xs text-center text-muted-foreground pt-2 border-t">
Version 2.0.0
</p>
</CardContent>
</Card>
+278 -46
View File
@@ -5,10 +5,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Calendar, ArrowLeft, Save, Trash2, Info } from "lucide-react";
import { Calendar, ArrowLeft, Save, Trash2, Info, Plus } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "sonner";
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 { useSSE } from "@/hooks/useSSE";
@@ -16,6 +17,7 @@ interface Demande {
Id: number;
CollaborateurADId: number;
nomEmploye: string;
prenomEmploye: string;
emailEmploye: string;
service: string;
DateDebut: string;
@@ -30,12 +32,24 @@ interface Demande {
}>;
}
interface TypeConge {
id: number;
nom: string;
couleur: string;
}
interface LigneConge {
typeId: number;
nombreJours: number;
}
const ModificationDemande = () => {
const navigate = useNavigate();
const { id } = useParams();
const [demande, setDemande] = useState<Demande | null>(null);
const [loading, setLoading] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [typesCongeDisponibles, setTypesCongeDisponibles] = useState<TypeConge[]>([]);
const [formData, setFormData] = useState({
dateDebut: "",
@@ -47,6 +61,7 @@ const ModificationDemande = () => {
useEffect(() => {
if (id) {
chargerDemande(parseInt(id));
chargerTypesConge();
}
}, [id]);
@@ -123,6 +138,87 @@ 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) => {
e.preventDefault();
@@ -134,6 +230,8 @@ const ModificationDemande = () => {
setLoading(true);
try {
const token = localStorage.getItem('token');
// 1. Modifier la demande
const response = await fetch(`/api/demandes/${id}`, {
method: 'PUT',
headers: {
@@ -150,22 +248,40 @@ const ModificationDemande = () => {
const data = await response.json();
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 {
if (!response.ok) {
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) {
toast.error(error.message || "Erreur lors de la modification");
} finally {
@@ -288,17 +404,17 @@ const ModificationDemande = () => {
</header>
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* ✅ ALERTE MISE À JOUR POUR INCLURE LE STATUT "ANNULÉE" */}
{/* ✅ ALERTE MISE À JOUR POUR VALIDATION AUTOMATIQUE */}
{peutModifier && demandeTraitee && (
<Alert className="mb-6 border-blue-500/50 bg-blue-500/5">
<Info className="h-4 w-4 text-blue-500" />
<AlertDescription className="text-blue-700 dark:text-blue-400">
<Alert className="mb-6 border-green-500/50 bg-green-500/5">
<Info className="h-4 w-4 text-green-500" />
<AlertDescription className="text-green-700 dark:text-green-400">
{demande.Statut === 'Validée'
? "⚠️ Cette demande a été validée. En la modifiant, elle sera automatiquement remise en attente et les compteurs seront restaurés."
? " Cette demande a été validée. En la modifiant, elle sera automatiquement re-validée avec les nouveaux paramètres."
: demande.Statut === 'Refusée'
? " Cette demande a été refusée. En la modifiant, elle sera automatiquement remise en attente pour re-validation."
? " Cette demande a été refusée. En la modifiant, elle sera automatiquement validée."
: demande.Statut === 'Annulée'
? " Cette demande a été annulée. En la modifiant, elle sera automatiquement remise en attente pour re-validation."
? " Cette demande a été annulée. En la modifiant, elle sera automatiquement validée."
: ""
}
</AlertDescription>
@@ -361,36 +477,152 @@ const ModificationDemande = () => {
<Card className="shadow-elegant border-0">
<CardHeader>
<CardTitle>Modifier les dates</CardTitle>
<CardTitle>Modifier la demande</CardTitle>
<CardDescription>
La demande sera automatiquement validée après modification
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={modifierDemande} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="dateDebut">Date de début</Label>
<Input
id="dateDebut"
type="date"
value={formData.dateDebut}
onChange={(e) => setFormData({ ...formData, dateDebut: e.target.value })}
disabled={!peutModifier}
required
/>
{/* 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="space-y-2">
<Label htmlFor="dateDebut">Date de début</Label>
<Input
id="dateDebut"
type="date"
value={formData.dateDebut}
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 className="space-y-2">
<Label htmlFor="dateFin">Date de fin</Label>
<Input
id="dateFin"
type="date"
value={formData.dateFin}
onChange={(e) => setFormData({ ...formData, dateFin: e.target.value })}
disabled={!peutModifier}
required
/>
</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">
<Label htmlFor="commentaire">Commentaire</Label>
<Textarea
@@ -412,7 +644,7 @@ const ModificationDemande = () => {
className="flex-1 bg-gradient-primary hover:opacity-90 transition-smooth shadow-elegant"
>
<Save className="w-4 h-4 mr-2" />
{loading ? 'Enregistrement...' : 'Enregistrer les modifications'}
{loading ? 'Enregistrement...' : 'Modifier et valider automatiquement'}
</Button>
<Button
type="button"
+1152 -484
View File
File diff suppressed because it is too large Load Diff
+637 -139
View File
@@ -1,104 +1,333 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Calendar, Search, ArrowLeft, Users } from "lucide-react";
import { Calendar, Search, ArrowLeft, Users, ChevronDown, ChevronUp, UserCog, AlertTriangle } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
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 Team {
Id: number;
service: string;
nombreMembres: number;
demandesEnAttente: number;
interface Collaborateur {
id: number;
nom: string;
prenom: string;
email: string;
societe: string;
societeId: number | null;
campus: string;
campusId: number | null;
service: string;
serviceId: number | null;
poste: string;
role: 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 navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState("");
const [filterDepartment, setFilterDepartment] = useState("all");
const [filterSociete, setFilterSociete] = useState("all");
const [filterCampus, setFilterCampus] = useState("all");
const [teams, setTeams] = useState<Team[]>([]);
const [loading, setLoading] = useState(true);
const [filterService, setFilterService] = useState("all");
const [filterContrat, setFilterContrat] = useState("all");
// Mapping des campus
const getCampusFullName = (code: string): string => {
const mapping: Record<string, string> = {
'CGY': 'Cergy',
'Marseille': 'Marseille',
'Nantes': 'Nantes',
'SQY': 'Saint-Quentin-en-Yvelines',
'Non assigné': 'Non assigné'
};
return mapping[code] || code;
};
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 [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
const [validateurs, setValidateurs] = useState<Validateur[]>([]);
const [editionOuverte, setEditionOuverte] = useState(false);
const [edition, setEdition] = useState(EDITION_INITIALE);
const [enregistrement, setEnregistrement] = useState(false);
useEffect(() => {
chargerEquipes();
chargerDonnees();
}, []);
const chargerEquipes = async () => {
const chargerValidateurs = 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);
try {
const token = localStorage.getItem('token');
const response = await fetch('/api/equipes', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
setTeams(data || []);
const headers = { 'Authorization': `Bearer ${token}` };
// Charger les collaborateurs avec toutes leurs infos
const collabResponse = await fetch('/api/collaborateurs/details', { headers });
if (collabResponse.ok) {
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) {
console.error('Erreur:', error);
toast.error("Erreur lors du chargement des équipes");
setTeams([]);
toast.error("Erreur lors du chargement des données");
} finally {
setLoading(false);
}
};
// Obtenir la liste des campus d'une équipe
const getCampusList = (campus: string): string[] => {
if (!campus) return ['Non assigné'];
return campus.split(',').map(c => c.trim());
const toggleRowExpansion = (id: number) => {
const newExpanded = new Set(expandedRows);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedRows(newExpanded);
};
// Filtrage des équipes
const filteredTeams = teams.filter(team => {
const campusList = getCampusList(team.campus);
const getTypeContratLabel = (type: string) => {
switch (type) {
case 'forfait_jour': return 'Forfait jours';
case '37h': return '37h';
case 'Apprentissage': return 'Alternant';
case 'tempspartiel': return 'Temps partiel';
default: return type || 'Non défini';
}
};
// Recherche
const matchesSearch = !searchQuery ||
team.service?.toLowerCase().includes(searchQuery.toLowerCase()) ||
campusList.some(c =>
c.toLowerCase().includes(searchQuery.toLowerCase()) ||
getCampusFullName(c).toLowerCase().includes(searchQuery.toLowerCase())
);
const getRoleBadge = (role: string) => {
switch (role) {
case 'Admin':
return <Badge variant="destructive">{role}</Badge>;
case 'RH':
return <Badge variant="default">{role}</Badge>;
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>;
}
};
// Filtre département
const matchesDepartment = filterDepartment === "all" || team.service === filterDepartment;
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>;
}
};
// Filtre campus
const matchesCampus = filterCampus === "all" || campusList.includes(filterCampus);
// 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());
return matchesSearch && matchesDepartment && matchesCampus;
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 service
const sortedTeams = [...filteredTeams].sort((a, b) =>
(a.service || '').localeCompare(b.service || '')
// Tri par nom
const collaborateursTries = [...collaborateursFiltres].sort((a, b) =>
`${a.nom} ${a.prenom}`.localeCompare(`${b.nom} ${b.prenom}`)
);
// 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();
// Profils incomplets (nouveaux arrivants synchronisés depuis Entra sans société/campus/service/date d'entrée/validateur)
const profilsIncomplets = collaborateurs.filter(estProfilIncomplet);
// Liste unique des services
const departments = Array.from(new Set(teams.map(t => t.service))).filter(Boolean).sort();
// Listes uniques pour les filtres
const societesUniques = [...new Set(collaborateurs.map(c => c.societe).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 (
<div className="min-h-screen bg-gradient-subtle">
@@ -117,126 +346,395 @@ const Teams = () => {
</Button>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: "#7e5aa2" }}>
<Calendar className="w-5 h-5 text-primary-foreground" />
<Users 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>
<h1 className="text-2xl font-bold text-foreground">Gestion des équipes</h1>
</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>
</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">
{/* 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">
<CardContent className="p-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Recherche */}
<div className="flex-1 relative">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
{/* Recherche collaborateur */}
<div className="relative xl:col-span-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Rechercher une équipe ou un campus..."
placeholder="Rechercher un collaborateur..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 transition-smooth"
/>
</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 */}
<Select value={filterCampus} onValueChange={setFilterCampus}>
<SelectTrigger className="w-full md:w-[250px]">
<SelectTrigger>
<SelectValue placeholder="Tous les campus" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les campus</SelectItem>
{campusList.map((campus) => (
<SelectItem key={campus} value={campus}>
{getCampusFullName(campus)}
</SelectItem>
{campusUniques.map((campus) => (
<SelectItem key={campus} value={campus}>{campus}</SelectItem>
))}
</SelectContent>
</Select>
{/* Filtre Service */}
<Select value={filterDepartment} onValueChange={setFilterDepartment}>
<SelectTrigger className="w-full md:w-[200px]">
<Select value={filterService} onValueChange={setFilterService}>
<SelectTrigger>
<SelectValue placeholder="Tous les services" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les services</SelectItem>
{departments.map((dept) => (
<SelectItem key={dept} value={dept}>
{dept}
</SelectItem>
{servicesUniques.map((service) => (
<SelectItem key={service} value={service}>{service}</SelectItem>
))}
</SelectContent>
</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>
</CardContent>
</Card>
{/* Contenu */}
{loading ? (
<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" />
<p className="text-muted-foreground">Chargement des équipes...</p>
</div>
) : sortedTeams.length === 0 ? (
<div className="text-center py-12">
<Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
<p className="text-muted-foreground text-lg">Aucune équipe trouvée</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sortedTeams.map((team) => {
const campusArray = getCampusList(team.campus);
return (
<Card
key={team.Id}
className="shadow-card border-0 bg-gradient-card hover:shadow-elegant transition-smooth cursor-pointer"
onClick={() => navigate(`/teams/${team.Id}`)}
>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-lg mb-2">{team.service}</CardTitle>
<div className="flex flex-wrap gap-1">
{campusArray.map((campus, idx) => (
<Badge key={`${team.Id}-${idx}`} variant="outline" className="text-xs">
{getCampusFullName(campus)}
</Badge>
))}
</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"}
{/* Liste des collaborateurs */}
<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 ? (
<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" />
<p className="text-muted-foreground">Chargement des collaborateurs...</p>
</div>
) : collaborateursTries.length === 0 ? (
<div className="text-center py-12">
<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>
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-8"></TableHead>
<TableHead>Nom / Prénom</TableHead>
<TableHead>Email</TableHead>
<TableHead>Poste</TableHead>
<TableHead>Rôle</TableHead>
<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)}
>
{team.demandesEnAttente}
</Badge>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
<TableCell>
{expandedRows.has(collab.id) ? (
<ChevronUp className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
)}
</TableCell>
<TableCell className="font-medium">
{collab.nom} {collab.prenom}
</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>
</div>
);
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,10 +1,10 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"references": [ { "path": "./tsconfig.app.json" } ],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@/*": [ "./src/*" ]
},
"noImplicitAny": false,
"noUnusedParameters": false,
@@ -13,4 +13,4 @@
"noUnusedLocals": false,
"strictNullChecks": false
}
}
}
+15 -9
View File
@@ -1,10 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import http from 'http'
export default defineConfig({
plugins: [react()],
plugins: [
react()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
@@ -13,17 +14,22 @@ export default defineConfig({
server: {
host: true,
port: 3014,
allowedHosts: ['mygta-rh.ensup-adm.net', 'localhost'],
allowedHosts: [
'mygta-rh.ensup-adm.net',
'localhost'
],
hmr: {
protocol: 'ws',
host: 'mygta-rh.ensup-adm.net',
clientPort: 80,
},
proxy: {
'/api': {
target: 'http://gtarh-backend:3000',
target: 'http://172.20.0.2:1433', // ← backend Node, pas SQL Server
changeOrigin: true,
secure: false,
ws: true,
rewrite: (path) => path.replace(/^\/api/, ''), // ✅ RETIRE /api
configure: (proxy, options) => {
options.agent = new http.Agent({ family: 4 });
}
rewrite: (path) => path.replace(/^\/api/, ''),
}
}
},
@@ -31,4 +37,4 @@ export default defineConfig({
outDir: 'dist',
sourcemap: false,
}
})
})