Compare commits

..

1 Commits

Author SHA1 Message Date
oimer bbebf6f44a GTARH_Fonctionnel_V1 2025-12-02 17:57:33 +01:00
21 changed files with 5878 additions and 11705 deletions
+8 -2
View File
@@ -1,4 +1,10 @@
# Configuration Base de données MySQL
DB_SERVER=192.168.0.4
DB_DATABASE=DemandeConge
DB_PASSWORD=-2b/)ru5/Bi8P[7_
DB_ENCRYPT=true
DB_TRUST_SERVER_CERTIFICATE=true
PORT=3000
COLLABORATEURS_URL=http://localhost:3000 COLLABORATEURS_URL=http://localhost:3000
RH_URL=http://localhost:3001 RH_URL=http://localhost:3001
@@ -7,7 +13,7 @@ WEBHOOK_SECRET=secret-rh-2025
# Configuration Serveur # Configuration Serveur
PORT=3000 PORT=3001
NODE_ENV=development NODE_ENV=development
# Configuration Azure AD (Microsoft Graph) # Configuration Azure AD (Microsoft Graph)
+1 -2
View File
@@ -103,8 +103,7 @@
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow- "follow-redirects": "^1.15.6",
s": "^1.15.6",
"form-data": "^4.0.4", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
+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",
"mssql": "^11.0.1", "mysql2": "^3.15.1",
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"pdfkit": "^0.17.2" "pdfkit": "^0.17.2"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
} }
} }
+2672 -5512
View File
File diff suppressed because it is too large Load Diff
+31 -30
View File
@@ -1,45 +1,46 @@
networks: version: '3.8' # ← AJOUTÉ (manquant !)
gtarh: services:
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 - VITE_API_URL=https://mygta-rh.ensup-adm.net:8013
image: ouijdaneim/gtarh-frontend:latest
container_name: gtarh-frontend container_name: gtarh-frontend
environment:
- NODE_ENV=development
restart: always
networks:
- gtarh
ports: ports:
- "3014:3014" - "3014:3014"
environment:
- NODE_ENV=production
depends_on: depends_on:
- backend - backend
networks:
- gtarh-network
restart: unless-stopped
networks:
gtarh-network:
driver: bridge
volumes:
mysql-data:
driver: local
+22 -170
View File
@@ -55,14 +55,12 @@
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"mysql2": "^3.15.1", "mysql2": "^3.15.1",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"pdfkit": "^0.17.2",
"pdfkit-table": "^0.1.99",
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.61.1", "react-hook-form": "^7.61.1",
"react-resizable-panels": "^2.1.9", "react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.2", "react-router-dom": "^6.30.1",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"sonner": "^1.7.4", "sonner": "^1.7.4",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
@@ -75,7 +73,7 @@
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5", "@types/node": "^22.16.5",
"@types/react": "^18.3.27", "@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react-swc": "^3.11.0", "@vitejs/plugin-react-swc": "^3.11.0",
@@ -2664,9 +2662,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.23.1", "version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -3277,15 +3275,6 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@swc/helpers": {
"version": "0.5.18",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@swc/types": { "node_modules/@swc/types": {
"version": "0.1.25", "version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
@@ -3480,15 +3469,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.27", "version": "18.3.25",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "integrity": "sha512-oSVZmGtDPmRZtVDqvdKUi/qgCsWp5IDY29wp8na8Bj4B3cc99hfNzvNhlMkVVxctkAOGUA3Km7MMpBHAnWfcIA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
@@ -4227,15 +4216,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.26.3", "version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
@@ -4495,15 +4475,6 @@
"url": "https://polar.sh/cva" "url": "https://polar.sh/cva"
} }
}, },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -4690,12 +4661,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4709,9 +4674,9 @@
} }
}, },
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
@@ -4906,12 +4871,6 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -5464,6 +5423,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-equals": { "node_modules/fast-equals": {
@@ -5606,23 +5566,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/foreground-child": { "node_modules/foreground-child": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -6191,13 +6134,6 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/jpeg-exif": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6436,25 +6372,6 @@
"url": "https://github.com/sponsors/antonk52" "url": "https://github.com/sponsors/antonk52"
} }
}, },
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -7187,28 +7104,6 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/pdfkit": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz",
"integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==",
"license": "MIT",
"dependencies": {
"crypto-js": "^4.2.0",
"fontkit": "^2.0.4",
"jpeg-exif": "^1.1.4",
"linebreak": "^1.1.0",
"png-js": "^1.0.0"
}
},
"node_modules/pdfkit-table": {
"version": "0.1.99",
"resolved": "https://registry.npmjs.org/pdfkit-table/-/pdfkit-table-0.1.99.tgz",
"integrity": "sha512-BaunFJ7cvKqIPONzX9FXUpeC/3iJeLo0v2nVNjCccAxdMXKvDMBjxyF4wZhKOYlmP+nYZcikf9ByVJeCzjNK6g==",
"license": "MIT",
"dependencies": {
"pdfkit": "latest"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -7245,11 +7140,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -7691,12 +7581,12 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.30.2", "version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.23.1" "@remix-run/router": "1.23.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -7706,13 +7596,13 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.30.2", "version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@remix-run/router": "1.23.1", "@remix-run/router": "1.23.0",
"react-router": "6.30.2" "react-router": "6.30.1"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -7902,12 +7792,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/reusify": { "node_modules/reusify": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -8585,12 +8469,6 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -8756,32 +8634,6 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+2 -4
View File
@@ -58,14 +58,12 @@
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"mysql2": "^3.15.1", "mysql2": "^3.15.1",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"pdfkit": "^0.17.2",
"pdfkit-table": "^0.1.99",
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.61.1", "react-hook-form": "^7.61.1",
"react-resizable-panels": "^2.1.9", "react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.2", "react-router-dom": "^6.30.1",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"sonner": "^1.7.4", "sonner": "^1.7.4",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
@@ -78,7 +76,7 @@
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5", "@types/node": "^22.16.5",
"@types/react": "^18.3.27", "@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react-swc": "^3.11.0", "@vitejs/plugin-react-swc": "^3.11.0",
-3
View File
@@ -54,9 +54,6 @@ 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
+432 -653
View File
File diff suppressed because it is too large Load Diff
+285 -544
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+11 -36
View File
@@ -1,4 +1,6 @@
import { useState, useEffect } from "react"; 
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -15,7 +17,6 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
interface HistoriqueAction { interface HistoriqueAction {
Id: number; Id: number;
collaborateur: string; collaborateur: string;
realisePar: string;
Action: string; Action: string;
Details: string; Details: string;
DateAction: string; DateAction: string;
@@ -50,39 +51,20 @@ const Historique = () => {
`/api/historique?${params.toString()}`, `/api/historique?${params.toString()}`,
{ headers: { 'Authorization': `Bearer ${token}` } } { headers: { 'Authorization': `Bearer ${token}` } }
); );
// ⭐ VÉRIFICATION
if (!response.ok) {
throw new Error(`Erreur ${response.status}`);
}
const data = await response.json(); const data = await response.json();
setHistorique(data);
// ⭐ SÉCURITÉ : Vérifier que c'est un tableau
if (Array.isArray(data)) {
setHistorique(data);
} else {
console.error('❌ Format historique invalide:', data);
setHistorique([]);
}
} catch (error) { } catch (error) {
console.error('❌ Erreur chargement historique:', error);
toast.error("Erreur lors du chargement de l'historique"); toast.error("Erreur lors du chargement de l'historique");
setHistorique([]); // ⭐ Toujours mettre un tableau vide en cas d'erreur
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
// ⭐ SÉCURITÉ : Vérifier que historique est un tableau avant filter const historiqueFiltré = historique.filter(h => {
const historiqueFiltré = Array.isArray(historique) const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) ||
? historique.filter(h => { h.Details?.toLowerCase().includes(recherche.toLowerCase());
const matchRecherche = h.collaborateur?.toLowerCase().includes(recherche.toLowerCase()) || return matchRecherche;
h.realisePar?.toLowerCase().includes(recherche.toLowerCase()) || });
h.Details?.toLowerCase().includes(recherche.toLowerCase());
return matchRecherche;
})
: [];
const getActionIcon = (action: string) => { const getActionIcon = (action: string) => {
if (action.includes('Validation')) return <CheckCircle className="w-4 h-4 text-success" />; if (action.includes('Validation')) return <CheckCircle className="w-4 h-4 text-success" />;
@@ -127,13 +109,12 @@ const Historique = () => {
return; return;
} }
const headers = ['Date et heure', 'Collaborateur concerné', 'Action réalisée par', 'Action', 'Détails', 'Demande ID']; const headers = ['Date et heure', 'Collaborateur', 'Action', 'Détails', 'Demande ID'];
const csvContent = [ const csvContent = [
headers.join(';'), headers.join(';'),
...historiqueFiltré.map(row => [ ...historiqueFiltré.map(row => [
formatDateTime(row.DateAction), formatDateTime(row.DateAction),
row.collaborateur, row.collaborateur,
row.realisePar,
row.Action, row.Action,
row.Details, row.Details,
row.DemandeCongeId || '' row.DemandeCongeId || ''
@@ -302,8 +283,7 @@ const Historique = () => {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Date et heure</TableHead> <TableHead>Date et heure</TableHead>
<TableHead>Collaborateur concerné</TableHead> <TableHead>Collaborateur</TableHead>
<TableHead>Action réalisée par</TableHead>
<TableHead>Action</TableHead> <TableHead>Action</TableHead>
<TableHead>Détails</TableHead> <TableHead>Détails</TableHead>
<TableHead className="text-center">Demande</TableHead> <TableHead className="text-center">Demande</TableHead>
@@ -316,11 +296,6 @@ const Historique = () => {
{formatDateTime(action.DateAction)} {formatDateTime(action.DateAction)}
</TableCell> </TableCell>
<TableCell>{action.collaborateur}</TableCell> <TableCell>{action.collaborateur}</TableCell>
<TableCell>
<span className="text-muted-foreground">
{action.realisePar || '-'}
</span>
</TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getActionIcon(action.Action)} {getActionIcon(action.Action)}
+2 -2
View File
@@ -150,8 +150,8 @@ const LoginDev = () => {
{loading ? 'Connexion...' : 'Se connecter'} {loading ? 'Connexion...' : 'Se connecter'}
</Button> </Button>
<p className="text-xs text-center text-muted-foreground pt-2 border-t"> <p className="text-xs text-center text-muted-foreground">
Version 2.0.0 Mode développement uniquement
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
+46 -278
View File
@@ -5,11 +5,10 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Calendar, ArrowLeft, Save, Trash2, Info, Plus } from "lucide-react"; import { Calendar, ArrowLeft, Save, Trash2, Info } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { useSSE } from "@/hooks/useSSE"; import { useSSE } from "@/hooks/useSSE";
@@ -17,7 +16,6 @@ interface Demande {
Id: number; Id: number;
CollaborateurADId: number; CollaborateurADId: number;
nomEmploye: string; nomEmploye: string;
prenomEmploye: string;
emailEmploye: string; emailEmploye: string;
service: string; service: string;
DateDebut: string; DateDebut: string;
@@ -32,24 +30,12 @@ interface Demande {
}>; }>;
} }
interface TypeConge {
id: number;
nom: string;
couleur: string;
}
interface LigneConge {
typeId: number;
nombreJours: number;
}
const ModificationDemande = () => { const ModificationDemande = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const [demande, setDemande] = useState<Demande | null>(null); const [demande, setDemande] = useState<Demande | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [typesCongeDisponibles, setTypesCongeDisponibles] = useState<TypeConge[]>([]);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
dateDebut: "", dateDebut: "",
@@ -61,7 +47,6 @@ const ModificationDemande = () => {
useEffect(() => { useEffect(() => {
if (id) { if (id) {
chargerDemande(parseInt(id)); chargerDemande(parseInt(id));
chargerTypesConge();
} }
}, [id]); }, [id]);
@@ -138,87 +123,6 @@ const ModificationDemande = () => {
} }
}; };
const chargerTypesConge = async () => {
try {
const token = localStorage.getItem('token');
const response = await fetch('/api/types-conge', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const data = await response.json();
setTypesCongeDisponibles(data);
}
} catch (error) {
console.error('Erreur lors du chargement des types de congé:', error);
}
};
const calculerJoursOuvres = (debut: string, fin: string) => {
if (!debut || !fin) return 0;
const dateDebut = new Date(debut);
const dateFin = new Date(fin);
let jours = 0;
for (let d = new Date(dateDebut); d <= dateFin; d.setDate(d.getDate() + 1)) {
const jour = d.getDay();
if (jour !== 0 && jour !== 6) {
jours++;
}
}
return jours;
};
const ajouterLigne = () => {
const joursOuvres = calculerJoursOuvres(formData.dateDebut, formData.dateFin);
setFormData({
...formData,
typesConge: [...formData.typesConge, { typeId: 0, nombreJours: joursOuvres }]
});
};
const supprimerLigne = (index: number) => {
if (formData.typesConge.length > 1) {
setFormData({
...formData,
typesConge: formData.typesConge.filter((_, i) => i !== index)
});
}
};
const modifierLigne = (index: number, field: 'typeId' | 'nombreJours', value: any) => {
const nouvelles = [...formData.typesConge];
nouvelles[index] = { ...nouvelles[index], [field]: value };
setFormData({
...formData,
typesConge: nouvelles
});
};
const handleDateChange = (field: 'dateDebut' | 'dateFin', value: string) => {
const nouvellesData = { ...formData, [field]: value };
setFormData(nouvellesData);
// Mettre à jour automatiquement le nombre de jours pour toutes les lignes
if (nouvellesData.dateDebut && nouvellesData.dateFin) {
const joursOuvres = calculerJoursOuvres(nouvellesData.dateDebut, nouvellesData.dateFin);
const nouvellesLignes = formData.typesConge.map(ligne => ({
...ligne,
nombreJours: joursOuvres
}));
setFormData({
...nouvellesData,
typesConge: nouvellesLignes
});
}
};
const obtenirNomType = (typeId: number): string => {
const type = typesCongeDisponibles.find(t => t.id === typeId);
return type?.nom || '';
};
// ✅ MODIFICATION AVEC VALIDATION AUTOMATIQUE
const modifierDemande = async (e: React.FormEvent) => { const modifierDemande = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -230,8 +134,6 @@ const ModificationDemande = () => {
setLoading(true); setLoading(true);
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
// 1. Modifier la demande
const response = await fetch(`/api/demandes/${id}`, { const response = await fetch(`/api/demandes/${id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
@@ -248,40 +150,22 @@ const ModificationDemande = () => {
const data = await response.json(); const data = await response.json();
if (!response.ok) { if (response.ok) {
if (data.ancienStatut === 'Validée' && data.nouveauStatut === 'En attente') {
toast.success("Demande modifiée et remise en attente pour re-validation", {
description: "Les compteurs de congé ont été restaurés"
});
} else if (data.ancienStatut === 'Refusée' && data.nouveauStatut === 'En attente') {
toast.success("Demande modifiée et remise en attente");
} else if (data.ancienStatut === 'Annulée' && data.nouveauStatut === 'En attente') {
toast.success("Demande modifiée et remise en attente");
} else {
toast.success("Demande modifiée avec succès");
}
navigate("/dashboard");
} else {
throw new Error(data.error || 'Erreur lors de la modification'); throw new Error(data.error || 'Erreur lors de la modification');
} }
// 2. Valider automatiquement la demande si elle n'est pas déjà validée
if (data.nouveauStatut !== 'Validée') {
const validationResponse = await fetch(`/api/demandes/${id}/valider`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const validationData = await validationResponse.json();
if (validationResponse.ok) {
toast.success("✅ Demande modifiée et validée automatiquement", {
description: "Les compteurs ont été mis à jour"
});
} else {
// Si la validation échoue, on informe l'utilisateur mais on considère que la modification a réussi
toast.warning("⚠️ Demande modifiée mais erreur lors de la validation automatique", {
description: validationData.error || "Veuillez valider manuellement"
});
}
} else {
// La demande était déjà validée, on informe juste de la modification
toast.success("✅ Demande modifiée avec succès", {
description: "Les compteurs ont été mis à jour"
});
}
navigate("/dashboard");
} catch (error: any) { } catch (error: any) {
toast.error(error.message || "Erreur lors de la modification"); toast.error(error.message || "Erreur lors de la modification");
} finally { } finally {
@@ -404,17 +288,17 @@ const ModificationDemande = () => {
</header> </header>
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* ✅ ALERTE MISE À JOUR POUR VALIDATION AUTOMATIQUE */} {/* ✅ ALERTE MISE À JOUR POUR INCLURE LE STATUT "ANNULÉE" */}
{peutModifier && demandeTraitee && ( {peutModifier && demandeTraitee && (
<Alert className="mb-6 border-green-500/50 bg-green-500/5"> <Alert className="mb-6 border-blue-500/50 bg-blue-500/5">
<Info className="h-4 w-4 text-green-500" /> <Info className="h-4 w-4 text-blue-500" />
<AlertDescription className="text-green-700 dark:text-green-400"> <AlertDescription className="text-blue-700 dark:text-blue-400">
{demande.Statut === 'Validée' {demande.Statut === 'Validée'
? " Cette demande a été validée. En la modifiant, elle sera automatiquement re-validée avec les nouveaux paramètres." ? "⚠️ Cette demande a été validée. En la modifiant, elle sera automatiquement remise en attente et les compteurs seront restaurés."
: demande.Statut === 'Refusée' : demande.Statut === 'Refusée'
? " Cette demande a été refusée. En la modifiant, elle sera automatiquement validée." ? " Cette demande a été refusée. En la modifiant, elle sera automatiquement remise en attente pour re-validation."
: demande.Statut === 'Annulée' : demande.Statut === 'Annulée'
? " Cette demande a été annulée. En la modifiant, elle sera automatiquement validée." ? " Cette demande a été annulée. En la modifiant, elle sera automatiquement remise en attente pour re-validation."
: "" : ""
} }
</AlertDescription> </AlertDescription>
@@ -477,152 +361,36 @@ const ModificationDemande = () => {
<Card className="shadow-elegant border-0"> <Card className="shadow-elegant border-0">
<CardHeader> <CardHeader>
<CardTitle>Modifier la demande</CardTitle> <CardTitle>Modifier les dates</CardTitle>
<CardDescription>
La demande sera automatiquement validée après modification
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={modifierDemande} className="space-y-6"> <form onSubmit={modifierDemande} className="space-y-6">
{/* DATES */} <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4"> <div className="space-y-2">
<h3 className="text-lg font-semibold">Dates</h3> <Label htmlFor="dateDebut">Date de début</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <Input
<div className="space-y-2"> id="dateDebut"
<Label htmlFor="dateDebut">Date de début</Label> type="date"
<Input value={formData.dateDebut}
id="dateDebut" onChange={(e) => setFormData({ ...formData, dateDebut: e.target.value })}
type="date" disabled={!peutModifier}
value={formData.dateDebut} required
onChange={(e) => handleDateChange('dateDebut', e.target.value)} />
disabled={!peutModifier}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="dateFin">Date de fin</Label>
<Input
id="dateFin"
type="date"
value={formData.dateFin}
onChange={(e) => handleDateChange('dateFin', e.target.value)}
disabled={!peutModifier}
required
/>
</div>
</div> </div>
{formData.dateDebut && formData.dateFin && ( <div className="space-y-2">
<div className="p-3 bg-primary/5 rounded-lg"> <Label htmlFor="dateFin">Date de fin</Label>
<p className="text-sm text-muted-foreground"> <Input
Jours ouvrés : <span className="font-bold text-primary">{calculerJoursOuvres(formData.dateDebut, formData.dateFin)}</span> id="dateFin"
</p> type="date"
</div> value={formData.dateFin}
)} onChange={(e) => setFormData({ ...formData, dateFin: e.target.value })}
disabled={!peutModifier}
required
/>
</div>
</div> </div>
{/* TYPES DE CONGÉ */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Types de congé</h3>
{peutModifier && (
<Button
type="button"
variant="outline"
size="sm"
onClick={ajouterLigne}
>
<Plus className="w-4 h-4 mr-1" />
Ajouter un type
</Button>
)}
</div>
<div className="space-y-3">
{formData.typesConge.map((ligne, index) => {
const maxJours = calculerJoursOuvres(formData.dateDebut, formData.dateFin);
return (
<div key={index} className="space-y-2">
<div className="flex gap-3 items-end">
<div className="flex-1 space-y-2">
<Label>Nature de congé</Label>
<Select
value={ligne.typeId.toString()}
onValueChange={(value) => modifierLigne(index, 'typeId', parseInt(value))}
disabled={!peutModifier}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner" />
</SelectTrigger>
<SelectContent>
{typesCongeDisponibles.map(type => (
<SelectItem key={type.id} value={type.id.toString()}>
{type.nom}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="w-40 space-y-2">
<Label>
Nombre de jours
{maxJours > 0 && (
<span className="text-xs text-muted-foreground ml-1">
(max: {maxJours})
</span>
)}
</Label>
<Input
type="number"
step="0.5"
min="0"
max={maxJours}
value={ligne.nombreJours || ''}
onChange={(e) => modifierLigne(index, 'nombreJours', parseFloat(e.target.value) || 0)}
placeholder={maxJours > 0 ? maxJours.toString() : "0"}
disabled={!peutModifier}
/>
</div>
{peutModifier && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => supprimerLigne(index)}
disabled={formData.typesConge.length === 1}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
{ligne.typeId > 0 && ligne.nombreJours > 0 && (
<div className="text-xs px-3 py-1.5 rounded-md bg-blue-50 text-blue-700">
{obtenirNomType(ligne.typeId)}: {ligne.nombreJours} jour{ligne.nombreJours > 1 ? 's' : ''}
</div>
)}
</div>
);
})}
</div>
{formData.typesConge.length > 0 && (
<div className="p-3 bg-primary/5 rounded-lg">
<p className="text-sm text-muted-foreground">
Total : <span className="font-bold text-primary">
{formData.typesConge.reduce((sum, ligne) => sum + (ligne.nombreJours || 0), 0)} jour{formData.typesConge.reduce((sum, ligne) => sum + (ligne.nombreJours || 0), 0) > 1 ? 's' : ''}
</span>
</p>
</div>
)}
</div>
{/* COMMENTAIRE */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="commentaire">Commentaire</Label> <Label htmlFor="commentaire">Commentaire</Label>
<Textarea <Textarea
@@ -644,7 +412,7 @@ const ModificationDemande = () => {
className="flex-1 bg-gradient-primary hover:opacity-90 transition-smooth shadow-elegant" className="flex-1 bg-gradient-primary hover:opacity-90 transition-smooth shadow-elegant"
> >
<Save className="w-4 h-4 mr-2" /> <Save className="w-4 h-4 mr-2" />
{loading ? 'Enregistrement...' : 'Modifier et valider automatiquement'} {loading ? 'Enregistrement...' : 'Enregistrer les modifications'}
</Button> </Button>
<Button <Button
type="button" type="button"
+520 -1068
View File
File diff suppressed because it is too large Load Diff
+139 -313
View File
@@ -1,184 +1,104 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Calendar, Search, ArrowLeft, Users, ChevronDown, ChevronUp } from "lucide-react"; import { Calendar, Search, ArrowLeft, Users } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner"; import { toast } from "sonner";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
interface Collaborateur { interface Team {
id: number; Id: number;
nom: string;
prenom: string;
email: string;
societe: string;
societeId: number | null;
campus: string;
campusId: number | null;
service: string; service: string;
serviceId: number | null; nombreMembres: number;
poste: string; demandesEnAttente: number;
typeContrat: string; campus: 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 [filterSociete, setFilterSociete] = useState("all"); const [filterDepartment, setFilterDepartment] = useState("all");
const [filterCampus, setFilterCampus] = useState("all"); const [filterCampus, setFilterCampus] = useState("all");
const [filterService, setFilterService] = useState("all"); const [teams, setTeams] = useState<Team[]>([]);
const [filterContrat, setFilterContrat] = useState("all");
const [collaborateurs, setCollaborateurs] = useState<Collaborateur[]>([]);
const [societes, setSocietes] = useState<Societe[]>([]);
const [campusList, setCampusList] = useState<Campus[]>([]);
const [services, setServices] = useState<Service[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
// 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;
};
useEffect(() => { useEffect(() => {
chargerDonnees(); chargerEquipes();
}, []); }, []);
const chargerDonnees = async () => { const chargerEquipes = async () => {
setLoading(true); setLoading(true);
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
const headers = { 'Authorization': `Bearer ${token}` }; const response = await fetch('/api/equipes', {
headers: { 'Authorization': `Bearer ${token}` }
// Charger les collaborateurs avec toutes leurs infos });
const collabResponse = await fetch('/api/collaborateurs/details', { headers }); const data = await response.json();
if (collabResponse.ok) { setTeams(data || []);
const collabData = await collabResponse.json();
setCollaborateurs(collabData);
}
// Charger les sociétés
const societesResponse = await fetch('/api/societes', { headers });
if (societesResponse.ok) {
const societesData = await societesResponse.json();
setSocietes(societesData);
}
// Charger les campus
const campusResponse = await fetch('/api/campus', { headers });
if (campusResponse.ok) {
const campusData = await campusResponse.json();
setCampusList(campusData);
}
// Charger les services
const servicesResponse = await fetch('/api/services', { headers });
if (servicesResponse.ok) {
const servicesData = await servicesResponse.json();
setServices(servicesData);
}
} catch (error) { } catch (error) {
console.error('Erreur:', error); console.error('Erreur:', error);
toast.error("Erreur lors du chargement des données"); toast.error("Erreur lors du chargement des équipes");
setTeams([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const toggleRowExpansion = (id: number) => { // Obtenir la liste des campus d'une équipe
const newExpanded = new Set(expandedRows); const getCampusList = (campus: string): string[] => {
if (newExpanded.has(id)) { if (!campus) return ['Non assigné'];
newExpanded.delete(id); return campus.split(',').map(c => c.trim());
} else {
newExpanded.add(id);
}
setExpandedRows(newExpanded);
}; };
const getTypeContratLabel = (type: string) => { // Filtrage des équipes
switch (type) { const filteredTeams = teams.filter(team => {
case 'forfait_jour': return 'Forfait jours'; const campusList = getCampusList(team.campus);
case '37h': return '37h';
case 'Apprentissage': return 'Alternant';
case 'tempspartiel': return 'Temps partiel';
default: return type || 'Non défini';
}
};
const getTypeContratBadge = (type: string) => { // Recherche
switch (type) { const matchesSearch = !searchQuery ||
case 'forfait_jour': team.service?.toLowerCase().includes(searchQuery.toLowerCase()) ||
return <Badge variant="outline" className="bg-purple-100 text-purple-700 border-purple-300">Forfait jours</Badge>; campusList.some(c =>
case '37h': c.toLowerCase().includes(searchQuery.toLowerCase()) ||
return <Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300">37h</Badge>; getCampusFullName(c).toLowerCase().includes(searchQuery.toLowerCase())
case 'Apprentissage': );
return <Badge variant="outline" className="bg-orange-100 text-orange-700 border-orange-300">35h</Badge>;
case 'tempspartiel':
return <Badge variant="outline" className="bg-teal-100 text-teal-700 border-teal-300">Temps partiel</Badge>;
default:
return <Badge variant="secondary">{type || 'Non défini'}</Badge>;
}
};
// Filtrage des collaborateurs // Filtre département
const collaborateursFiltres = collaborateurs.filter(collab => { const matchesDepartment = filterDepartment === "all" || team.service === filterDepartment;
const matchSearch = !searchQuery ||
`${collab.prenom} ${collab.nom}`.toLowerCase().includes(searchQuery.toLowerCase()) ||
collab.email?.toLowerCase().includes(searchQuery.toLowerCase()) ||
collab.poste?.toLowerCase().includes(searchQuery.toLowerCase());
const matchSociete = filterSociete === "all" || collab.societe === filterSociete; // Filtre campus
const matchCampus = filterCampus === "all" || collab.campus === filterCampus; const matchesCampus = filterCampus === "all" || campusList.includes(filterCampus);
const matchService = filterService === "all" || collab.service === filterService;
const matchContrat = filterContrat === "all" || collab.typeContrat === filterContrat;
return matchSearch && matchSociete && matchCampus && matchService && matchContrat; return matchesSearch && matchesDepartment && matchesCampus;
}); });
// Tri par nom // Tri par service
const collaborateursTries = [...collaborateursFiltres].sort((a, b) => const sortedTeams = [...filteredTeams].sort((a, b) =>
`${a.nom} ${a.prenom}`.localeCompare(`${b.nom} ${b.prenom}`) (a.service || '').localeCompare(b.service || '')
); );
// Listes uniques pour les filtres // Liste unique des campus
const societesUniques = [...new Set(collaborateurs.map(c => c.societe).filter(Boolean))].sort(); const allCampus = new Set<string>();
const campusUniques = [...new Set(collaborateurs.map(c => c.campus).filter(Boolean))].sort(); teams.forEach(team => {
const servicesUniques = [...new Set(collaborateurs.map(c => c.service).filter(Boolean))].sort(); getCampusList(team.campus).forEach(c => allCampus.add(c));
});
const campusList = Array.from(allCampus).sort();
const formatDate = (dateString: string) => { // Liste unique des services
if (!dateString) return '-'; const departments = Array.from(new Set(teams.map(t => t.service))).filter(Boolean).sort();
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">
@@ -197,12 +117,9 @@ const Teams = () => {
</Button> </Button>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: "#7e5aa2" }}> <div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: "#7e5aa2" }}>
<Users className="w-5 h-5 text-primary-foreground" /> <Calendar className="w-5 h-5 text-primary-foreground" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">Gestion des collaborateurs</h1>
<p className="text-sm text-muted-foreground">{collaborateursTries.length} collaborateur{collaborateursTries.length > 1 ? 's' : ''}</p>
</div> </div>
<h1 className="text-2xl font-bold text-foreground">Gestion des équipes</h1>
</div> </div>
</div> </div>
</div> </div>
@@ -210,207 +127,116 @@ 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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4"> <div className="flex flex-col md:flex-row gap-4">
{/* Recherche collaborateur */} {/* Recherche */}
<div className="relative xl:col-span-1"> <div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
placeholder="Rechercher un collaborateur..." placeholder="Rechercher une équipe ou un campus..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 transition-smooth" className="pl-10 transition-smooth"
/> />
</div> </div>
{/* Filtre Société */}
<Select value={filterSociete} onValueChange={setFilterSociete}>
<SelectTrigger>
<SelectValue placeholder="Toutes les sociétés" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes les sociétés</SelectItem>
{societesUniques.map((soc) => (
<SelectItem key={soc} value={soc}>{soc}</SelectItem>
))}
</SelectContent>
</Select>
{/* Filtre Campus */} {/* Filtre Campus */}
<Select value={filterCampus} onValueChange={setFilterCampus}> <Select value={filterCampus} onValueChange={setFilterCampus}>
<SelectTrigger> <SelectTrigger className="w-full md:w-[250px]">
<SelectValue placeholder="Tous les campus" /> <SelectValue placeholder="Tous les campus" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Tous les campus</SelectItem> <SelectItem value="all">Tous les campus</SelectItem>
{campusUniques.map((campus) => ( {campusList.map((campus) => (
<SelectItem key={campus} value={campus}>{campus}</SelectItem> <SelectItem key={campus} value={campus}>
{getCampusFullName(campus)}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{/* Filtre Service */} {/* Filtre Service */}
<Select value={filterService} onValueChange={setFilterService}> <Select value={filterDepartment} onValueChange={setFilterDepartment}>
<SelectTrigger> <SelectTrigger className="w-full md:w-[200px]">
<SelectValue placeholder="Tous les services" /> <SelectValue placeholder="Tous les services" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Tous les services</SelectItem> <SelectItem value="all">Tous les services</SelectItem>
{servicesUniques.map((service) => ( {departments.map((dept) => (
<SelectItem key={service} value={service}>{service}</SelectItem> <SelectItem key={dept} value={dept}>
{dept}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{/* Filtre Type de contrat */}
<Select value={filterContrat} onValueChange={setFilterContrat}>
<SelectTrigger>
<SelectValue placeholder="Tous les contrats" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les contrats</SelectItem>
<SelectItem value="forfait_jour">Forfait jours</SelectItem>
<SelectItem value="37h">37h</SelectItem>
<SelectItem value="Apprentissage">Alternant</SelectItem>
<SelectItem value="tempspartiel">Temps partiel</SelectItem>
</SelectContent>
</Select>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Liste des collaborateurs */} {/* Contenu */}
<Card className="shadow-card border-0"> {loading ? (
<CardHeader> <div className="text-center py-12">
<CardTitle>Liste des collaborateurs</CardTitle> <div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<CardDescription> <p className="text-muted-foreground">Chargement des équipes...</p>
Cliquez sur une ligne pour voir les détails complets </div>
</CardDescription> ) : sortedTeams.length === 0 ? (
</CardHeader> <div className="text-center py-12">
<CardContent> <Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
{loading ? ( <p className="text-muted-foreground text-lg">Aucune équipe trouvée</p>
<div className="text-center py-12"> </div>
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" /> ) : (
<p className="text-muted-foreground">Chargement des collaborateurs...</p> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
</div> {sortedTeams.map((team) => {
) : collaborateursTries.length === 0 ? ( const campusArray = getCampusList(team.campus);
<div className="text-center py-12">
<Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" /> return (
<p className="text-muted-foreground text-lg">Aucun collaborateur trouvé</p> <Card
</div> key={team.Id}
) : ( className="shadow-card border-0 bg-gradient-card hover:shadow-elegant transition-smooth cursor-pointer"
<div className="overflow-x-auto"> onClick={() => navigate(`/teams/${team.Id}`)}
<Table> >
<TableHeader> <CardHeader>
<TableRow> <div className="flex items-start justify-between">
<TableHead className="w-8"></TableHead> <div className="flex-1">
<TableHead>Nom / Prénom</TableHead> <CardTitle className="text-lg mb-2">{team.service}</CardTitle>
<TableHead>Email</TableHead> <div className="flex flex-wrap gap-1">
<TableHead>Société</TableHead> {campusArray.map((campus, idx) => (
<TableHead>Campus</TableHead> <Badge key={`${team.Id}-${idx}`} variant="outline" className="text-xs">
<TableHead>Service</TableHead> {getCampusFullName(campus)}
<TableHead>Contrat</TableHead> </Badge>
<TableHead className="text-center">CP N</TableHead> ))}
<TableHead className="text-center">CP N-1</TableHead> </div>
<TableHead className="text-center">RTT</TableHead> </div>
<TableHead className="text-center">Récup</TableHead> <div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center">
</TableRow> <Users className="w-6 h-6 text-primary" />
</TableHeader> </div>
<TableBody> </div>
{collaborateursTries.map((collab) => ( </CardHeader>
<> <CardContent>
<TableRow <div className="space-y-3">
key={collab.id} <div className="flex items-center justify-between text-sm">
className="cursor-pointer hover:bg-muted/50" <span className="text-muted-foreground">Membres</span>
onClick={() => toggleRowExpansion(collab.id)} <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"}
> >
<TableCell> {team.demandesEnAttente}
{expandedRows.has(collab.id) ? ( </Badge>
<ChevronUp className="w-4 h-4 text-muted-foreground" /> </div>
) : ( </div>
<ChevronDown className="w-4 h-4 text-muted-foreground" /> </CardContent>
)} </Card>
</TableCell> );
<TableCell className="font-medium"> })}
{collab.nom} {collab.prenom} </div>
</TableCell> )}
<TableCell className="text-muted-foreground">
{collab.email}
</TableCell>
<TableCell>{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" } ], "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": [ "./src/*" ] "@/*": ["./src/*"]
}, },
"noImplicitAny": false, "noImplicitAny": false,
"noUnusedParameters": false, "noUnusedParameters": false,
@@ -13,4 +13,4 @@
"noUnusedLocals": false, "noUnusedLocals": false,
"strictNullChecks": false "strictNullChecks": false
} }
} }
+9 -15
View File
@@ -1,11 +1,10 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import path from 'path' import path from 'path'
import http from 'http'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [react()],
react()
],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
@@ -14,22 +13,17 @@ export default defineConfig({
server: { server: {
host: true, host: true,
port: 3014, port: 3014,
allowedHosts: [ allowedHosts: ['mygta-rh.ensup-adm.net', 'localhost'],
'mygta-rh.ensup-adm.net',
'localhost'
],
hmr: {
protocol: 'ws',
host: 'mygta-rh.ensup-adm.net',
clientPort: 80,
},
proxy: { proxy: {
'/api': { '/api': {
target: 'http://172.20.0.2:1433', // ← backend Node, pas SQL Server target: 'http://gtarh-backend:3000',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,
rewrite: (path) => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(/^\/api/, ''), // ✅ RETIRE /api
configure: (proxy, options) => {
options.agent = new http.Agent({ family: 4 });
}
} }
} }
}, },
@@ -37,4 +31,4 @@ export default defineConfig({
outDir: 'dist', outDir: 'dist',
sourcemap: false, sourcemap: false,
} }
}) })