Compare commits

..

1 Commits

Author SHA1 Message Date
oimer 762f9cfd4a GTARH_Modif_anno27 2026-05-28 11:11:52 +02:00
21 changed files with 11753 additions and 5926 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 COLLABORATEURS_URL=http://localhost:3000
RH_URL=http://localhost:3001 RH_URL=http://localhost:3001
@@ -13,7 +7,7 @@ WEBHOOK_SECRET=secret-rh-2025
# Configuration Serveur # Configuration Serveur
PORT=3001 PORT=3000
NODE_ENV=development NODE_ENV=development
# Configuration Azure AD (Microsoft Graph) # Configuration Azure AD (Microsoft Graph)
+2 -1
View File
@@ -103,7 +103,8 @@
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-
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"
} }
+2 -2
View File
@@ -14,11 +14,11 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mysql2": "^3.15.1", "mssql": "^11.0.1",
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"pdfkit": "^0.17.2" "pdfkit": "^0.17.2"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
} }
} }
+5510 -2670
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 !) networks:
services: gtarh:
external: false
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:8013 - VITE_API_URL=https://mygta-rh.ensup-adm.net
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
+170 -22
View File
@@ -55,12 +55,14 @@
"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.1", "react-router-dom": "^6.30.2",
"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",
@@ -73,7 +75,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.23", "@types/react": "^18.3.27",
"@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",
@@ -2662,9 +2664,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.23.0", "version": "1.23.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -3275,6 +3277,15 @@
"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",
@@ -3469,15 +3480,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.25", "version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-oSVZmGtDPmRZtVDqvdKUi/qgCsWp5IDY29wp8na8Bj4B3cc99hfNzvNhlMkVVxctkAOGUA3Km7MMpBHAnWfcIA==", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.2.2"
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
@@ -4216,6 +4227,15 @@
"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",
@@ -4475,6 +4495,15 @@
"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",
@@ -4661,6 +4690,12 @@
"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",
@@ -4674,9 +4709,9 @@
} }
}, },
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
@@ -4871,6 +4906,12 @@
"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",
@@ -5423,7 +5464,6 @@
"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": {
@@ -5566,6 +5606,23 @@
"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",
@@ -6134,6 +6191,13 @@
"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",
@@ -6372,6 +6436,25 @@
"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",
@@ -7104,6 +7187,28 @@
"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",
@@ -7140,6 +7245,11 @@
"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",
@@ -7581,12 +7691,12 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.30.1", "version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.23.0" "@remix-run/router": "1.23.1"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -7596,13 +7706,13 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.30.1", "version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.23.0", "@remix-run/router": "1.23.1",
"react-router": "6.30.1" "react-router": "6.30.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -7792,6 +7902,12 @@
"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",
@@ -8469,6 +8585,12 @@
"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",
@@ -8634,6 +8756,32 @@
"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",
+4 -2
View File
@@ -58,12 +58,14 @@
"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.1", "react-router-dom": "^6.30.2",
"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",
@@ -76,7 +78,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.23", "@types/react": "^18.3.27",
"@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",
+3
View File
@@ -54,6 +54,9 @@ const AuthMicrosoft = () => {
// 🔑 Envoie accessToken au backend // 🔑 Envoie accessToken au backend
const authenticateWithBackend = async (accessToken: string) => { const authenticateWithBackend = async (accessToken: string) => {
try { try {
console.log('🔑 Token complet:', accessToken);
console.log('🔑 Token (50 premiers):', accessToken.substring(0, 50));
console.log('🔑 Longueur:', accessToken.length);
const response = await fetch('/api/login-dev', { const response = await fetch('/api/login-dev', {
method: 'POST', method: 'POST',
headers: { headers: {
File diff suppressed because it is too large Load Diff
+640 -419
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 { 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";
@@ -17,6 +15,7 @@ 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;
@@ -51,20 +50,39 @@ 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);
} }
}; };
const historiqueFiltré = historique.filter(h => { // ⭐ SÉCURITÉ : Vérifier que historique est un tableau avant filter
const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) || const historiqueFiltré = Array.isArray(historique)
h.Details?.toLowerCase().includes(recherche.toLowerCase()); ? historique.filter(h => {
return matchRecherche; 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) => { 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" />;
@@ -109,12 +127,13 @@ const Historique = () => {
return; 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 = [ 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 || ''
@@ -283,7 +302,8 @@ const Historique = () => {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Date et heure</TableHead> <TableHead>Date et heure</TableHead>
<TableHead>Collaborateur</TableHead> <TableHead>Collaborateur concerné</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>
@@ -296,6 +316,11 @@ 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"> <p className="text-xs text-center text-muted-foreground pt-2 border-t">
Mode développement uniquement Version 2.0.0
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
+278 -46
View File
@@ -5,10 +5,11 @@ 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 } from "lucide-react"; import { Calendar, ArrowLeft, Save, Trash2, Info, Plus } 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";
@@ -16,6 +17,7 @@ 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;
@@ -30,12 +32,24 @@ 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: "",
@@ -47,6 +61,7 @@ const ModificationDemande = () => {
useEffect(() => { useEffect(() => {
if (id) { if (id) {
chargerDemande(parseInt(id)); chargerDemande(parseInt(id));
chargerTypesConge();
} }
}, [id]); }, [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) => { const modifierDemande = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -134,6 +230,8 @@ 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: {
@@ -150,22 +248,40 @@ 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 {
@@ -288,17 +404,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 INCLURE LE STATUT "ANNULÉE" */} {/* ✅ ALERTE MISE À JOUR POUR VALIDATION AUTOMATIQUE */}
{peutModifier && demandeTraitee && ( {peutModifier && demandeTraitee && (
<Alert className="mb-6 border-blue-500/50 bg-blue-500/5"> <Alert className="mb-6 border-green-500/50 bg-green-500/5">
<Info className="h-4 w-4 text-blue-500" /> <Info className="h-4 w-4 text-green-500" />
<AlertDescription className="text-blue-700 dark:text-blue-400"> <AlertDescription className="text-green-700 dark:text-green-400">
{demande.Statut === 'Validée' {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' : 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' : 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> </AlertDescription>
@@ -361,36 +477,152 @@ const ModificationDemande = () => {
<Card className="shadow-elegant border-0"> <Card className="shadow-elegant border-0">
<CardHeader> <CardHeader>
<CardTitle>Modifier les dates</CardTitle> <CardTitle>Modifier la demande</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">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {/* DATES */}
<div className="space-y-2"> <div className="space-y-4">
<Label htmlFor="dateDebut">Date de début</Label> <h3 className="text-lg font-semibold">Dates</h3>
<Input <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
id="dateDebut" <div className="space-y-2">
type="date" <Label htmlFor="dateDebut">Date de début</Label>
value={formData.dateDebut} <Input
onChange={(e) => setFormData({ ...formData, dateDebut: e.target.value })} id="dateDebut"
disabled={!peutModifier} type="date"
required 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>
<div className="space-y-2"> {formData.dateDebut && formData.dateFin && (
<Label htmlFor="dateFin">Date de fin</Label> <div className="p-3 bg-primary/5 rounded-lg">
<Input <p className="text-sm text-muted-foreground">
id="dateFin" Jours ouvrés : <span className="font-bold text-primary">{calculerJoursOuvres(formData.dateDebut, formData.dateFin)}</span>
type="date" </p>
value={formData.dateFin} </div>
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
@@ -412,7 +644,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...' : 'Enregistrer les modifications'} {loading ? 'Enregistrement...' : 'Modifier et valider automatiquement'}
</Button> </Button>
<Button <Button
type="button" type="button"
+1099 -551
View File
File diff suppressed because it is too large Load Diff
+313 -139
View File
@@ -1,104 +1,184 @@
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, 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";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Calendar, Search, ArrowLeft, Users } from "lucide-react"; import { Calendar, Search, ArrowLeft, Users, ChevronDown, ChevronUp } 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";
interface Team { interface Collaborateur {
Id: number; id: number;
service: string; nom: string;
nombreMembres: number; prenom: string;
demandesEnAttente: number; email: string;
societe: string;
societeId: number | null;
campus: string; campus: string;
campusId: number | null;
service: string;
serviceId: number | null;
poste: string;
typeContrat: string;
dateEntree: string;
description: 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;
} }
const Teams = () => { const Teams = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [filterDepartment, setFilterDepartment] = useState("all"); const [filterSociete, setFilterSociete] = useState("all");
const [filterCampus, setFilterCampus] = useState("all"); const [filterCampus, setFilterCampus] = useState("all");
const [teams, setTeams] = useState<Team[]>([]); const [filterService, setFilterService] = useState("all");
const [loading, setLoading] = useState(true); const [filterContrat, setFilterContrat] = useState("all");
// Mapping des campus const [collaborateurs, setCollaborateurs] = useState<Collaborateur[]>([]);
const getCampusFullName = (code: string): string => { const [societes, setSocietes] = useState<Societe[]>([]);
const mapping: Record<string, string> = { const [campusList, setCampusList] = useState<Campus[]>([]);
'CGY': 'Cergy', const [services, setServices] = useState<Service[]>([]);
'Marseille': 'Marseille',
'Nantes': 'Nantes', const [loading, setLoading] = useState(true);
'SQY': 'Saint-Quentin-en-Yvelines', const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
'Non assigné': 'Non assigné'
};
return mapping[code] || code;
};
useEffect(() => { useEffect(() => {
chargerEquipes(); chargerDonnees();
}, []); }, []);
const chargerEquipes = async () => { const chargerDonnees = async () => {
setLoading(true); setLoading(true);
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
const response = await fetch('/api/equipes', { const headers = { 'Authorization': `Bearer ${token}` };
headers: { 'Authorization': `Bearer ${token}` }
}); // Charger les collaborateurs avec toutes leurs infos
const data = await response.json(); const collabResponse = await fetch('/api/collaborateurs/details', { headers });
setTeams(data || []); 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) { } catch (error) {
console.error('Erreur:', error); console.error('Erreur:', error);
toast.error("Erreur lors du chargement des équipes"); toast.error("Erreur lors du chargement des données");
setTeams([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
// Obtenir la liste des campus d'une équipe const toggleRowExpansion = (id: number) => {
const getCampusList = (campus: string): string[] => { const newExpanded = new Set(expandedRows);
if (!campus) return ['Non assigné']; if (newExpanded.has(id)) {
return campus.split(',').map(c => c.trim()); newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedRows(newExpanded);
}; };
// Filtrage des équipes const getTypeContratLabel = (type: string) => {
const filteredTeams = teams.filter(team => { switch (type) {
const campusList = getCampusList(team.campus); 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 getTypeContratBadge = (type: string) => {
const matchesSearch = !searchQuery || switch (type) {
team.service?.toLowerCase().includes(searchQuery.toLowerCase()) || case 'forfait_jour':
campusList.some(c => return <Badge variant="outline" className="bg-purple-100 text-purple-700 border-purple-300">Forfait jours</Badge>;
c.toLowerCase().includes(searchQuery.toLowerCase()) || case '37h':
getCampusFullName(c).toLowerCase().includes(searchQuery.toLowerCase()) 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 département // Filtrage des collaborateurs
const matchesDepartment = filterDepartment === "all" || team.service === filterDepartment; 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());
// Filtre campus const matchSociete = filterSociete === "all" || collab.societe === filterSociete;
const matchesCampus = filterCampus === "all" || campusList.includes(filterCampus); const matchCampus = filterCampus === "all" || collab.campus === filterCampus;
const matchService = filterService === "all" || collab.service === filterService;
const matchContrat = filterContrat === "all" || collab.typeContrat === filterContrat;
return matchesSearch && matchesDepartment && matchesCampus; return matchSearch && matchSociete && matchCampus && matchService && matchContrat;
}); });
// Tri par service // Tri par nom
const sortedTeams = [...filteredTeams].sort((a, b) => const collaborateursTries = [...collaborateursFiltres].sort((a, b) =>
(a.service || '').localeCompare(b.service || '') `${a.nom} ${a.prenom}`.localeCompare(`${b.nom} ${b.prenom}`)
); );
// Liste unique des campus // Listes uniques pour les filtres
const allCampus = new Set<string>(); const societesUniques = [...new Set(collaborateurs.map(c => c.societe).filter(Boolean))].sort();
teams.forEach(team => { const campusUniques = [...new Set(collaborateurs.map(c => c.campus).filter(Boolean))].sort();
getCampusList(team.campus).forEach(c => allCampus.add(c)); const servicesUniques = [...new Set(collaborateurs.map(c => c.service).filter(Boolean))].sort();
});
const campusList = Array.from(allCampus).sort();
// Liste unique des services const formatDate = (dateString: string) => {
const departments = Array.from(new Set(teams.map(t => t.service))).filter(Boolean).sort(); 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">
@@ -117,9 +197,12 @@ 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" }}>
<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> </div>
<h1 className="text-2xl font-bold text-foreground">Gestion des équipes</h1>
</div> </div>
</div> </div>
</div> </div>
@@ -127,116 +210,207 @@ const Teams = () => {
</header> </header>
<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">
{/* 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="flex flex-col md:flex-row gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
{/* Recherche */} {/* Recherche collaborateur */}
<div className="flex-1 relative"> <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" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
placeholder="Rechercher une équipe ou un campus..." placeholder="Rechercher un collaborateur..."
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 className="w-full md:w-[250px]"> <SelectTrigger>
<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>
{campusList.map((campus) => ( {campusUniques.map((campus) => (
<SelectItem key={campus} value={campus}> <SelectItem key={campus} value={campus}>{campus}</SelectItem>
{getCampusFullName(campus)}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{/* Filtre Service */} {/* Filtre Service */}
<Select value={filterDepartment} onValueChange={setFilterDepartment}> <Select value={filterService} onValueChange={setFilterService}>
<SelectTrigger className="w-full md:w-[200px]"> <SelectTrigger>
<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>
{departments.map((dept) => ( {servicesUniques.map((service) => (
<SelectItem key={dept} value={dept}> <SelectItem key={service} value={service}>{service}</SelectItem>
{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>
{/* Contenu */} {/* Liste des collaborateurs */}
{loading ? ( <Card className="shadow-card border-0">
<div className="text-center py-12"> <CardHeader>
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" /> <CardTitle>Liste des collaborateurs</CardTitle>
<p className="text-muted-foreground">Chargement des équipes...</p> <CardDescription>
</div> Cliquez sur une ligne pour voir les détails complets
) : sortedTeams.length === 0 ? ( </CardDescription>
<div className="text-center py-12"> </CardHeader>
<Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" /> <CardContent>
<p className="text-muted-foreground text-lg">Aucune équipe trouvée</p> {loading ? (
</div> <div className="text-center py-12">
) : ( <div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <p className="text-muted-foreground">Chargement des collaborateurs...</p>
{sortedTeams.map((team) => { </div>
const campusArray = getCampusList(team.campus); ) : collaborateursTries.length === 0 ? (
<div className="text-center py-12">
return ( <Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
<Card <p className="text-muted-foreground text-lg">Aucun collaborateur trouvé</p>
key={team.Id} </div>
className="shadow-card border-0 bg-gradient-card hover:shadow-elegant transition-smooth cursor-pointer" ) : (
onClick={() => navigate(`/teams/${team.Id}`)} <div className="overflow-x-auto">
> <Table>
<CardHeader> <TableHeader>
<div className="flex items-start justify-between"> <TableRow>
<div className="flex-1"> <TableHead className="w-8"></TableHead>
<CardTitle className="text-lg mb-2">{team.service}</CardTitle> <TableHead>Nom / Prénom</TableHead>
<div className="flex flex-wrap gap-1"> <TableHead>Email</TableHead>
{campusArray.map((campus, idx) => ( <TableHead>Société</TableHead>
<Badge key={`${team.Id}-${idx}`} variant="outline" className="text-xs"> <TableHead>Campus</TableHead>
{getCampusFullName(campus)} <TableHead>Service</TableHead>
</Badge> <TableHead>Contrat</TableHead>
))} <TableHead className="text-center">CP N</TableHead>
</div> <TableHead className="text-center">CP N-1</TableHead>
</div> <TableHead className="text-center">RTT</TableHead>
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center"> <TableHead className="text-center">Récup</TableHead>
<Users className="w-6 h-6 text-primary" /> </TableRow>
</div> </TableHeader>
</div> <TableBody>
</CardHeader> {collaborateursTries.map((collab) => (
<CardContent> <>
<div className="space-y-3"> <TableRow
<div className="flex items-center justify-between text-sm"> key={collab.id}
<span className="text-muted-foreground">Membres</span> className="cursor-pointer hover:bg-muted/50"
<span className="font-medium text-foreground">{team.nombreMembres}</span> onClick={() => toggleRowExpansion(collab.id)}
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Demandes en attente</span>
<Badge
variant="outline"
className={team.demandesEnAttente > 0
? "bg-warning/10 text-warning border-warning/20"
: "bg-success/10 text-success border-success/20"}
> >
{team.demandesEnAttente} <TableCell>
</Badge> {expandedRows.has(collab.id) ? (
</div> <ChevronUp className="w-4 h-4 text-muted-foreground" />
</div> ) : (
</CardContent> <ChevronDown className="w-4 h-4 text-muted-foreground" />
</Card> )}
); </TableCell>
})} <TableCell className="font-medium">
</div> {collab.nom} {collab.prenom}
)} </TableCell>
<TableCell className="text-muted-foreground">
{collab.email}
</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={11}>
<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
+3 -3
View File
@@ -1,10 +1,10 @@
{ {
"files": [], "files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], "references": [ { "path": "./tsconfig.app.json" } ],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [ "./src/*" ]
}, },
"noImplicitAny": false, "noImplicitAny": false,
"noUnusedParameters": false, "noUnusedParameters": false,
@@ -13,4 +13,4 @@
"noUnusedLocals": false, "noUnusedLocals": false,
"strictNullChecks": false "strictNullChecks": false
} }
} }
+15 -9
View File
@@ -1,10 +1,11 @@
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: [react()], plugins: [
react()
],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
@@ -13,17 +14,22 @@ export default defineConfig({
server: { server: {
host: true, host: true,
port: 3014, 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: { proxy: {
'/api': { '/api': {
target: 'http://gtarh-backend:3000', target: 'http://172.20.0.2:1433', // ← backend Node, pas SQL Server
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,
rewrite: (path) => path.replace(/^\/api/, ''), // ✅ RETIRE /api rewrite: (path) => path.replace(/^\/api/, ''),
configure: (proxy, options) => {
options.agent = new http.Agent({ family: 4 });
}
} }
} }
}, },
@@ -31,4 +37,4 @@ export default defineConfig({
outDir: 'dist', outDir: 'dist',
sourcemap: false, sourcemap: false,
} }
}) })