Merge branch 'master' of https://mygitea.ensup-adm.net/oimer/GTA
This commit is contained in:
@@ -1,32 +0,0 @@
|
|||||||
# Étape 1 : Construction de l'application
|
|
||||||
FROM node:18-alpine AS builder
|
|
||||||
|
|
||||||
# Définir le répertoire de travail
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copier le package.json et package-lock.json depuis le dossier 'project'
|
|
||||||
# Le contexte de construction est './project' donc Docker peut les trouver
|
|
||||||
COPY package.json ./
|
|
||||||
COPY package-lock.json ./
|
|
||||||
|
|
||||||
# Installer les dépendances
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Copier le reste des fichiers du dossier 'project'
|
|
||||||
# Cela inclut le dossier 'src' et tout le reste
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Lancer la compilation de l'application pour la production
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Étape 2 : Servir l'application avec Nginx
|
|
||||||
FROM nginx:alpine
|
|
||||||
|
|
||||||
# Copier les fichiers du build de l'étape précédente
|
|
||||||
COPY --from=builder /app/build /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Exposer le port 80
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
# Commande pour démarrer Nginx
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@@ -1,12 +1,44 @@
|
|||||||
services:
|
version: "3.8"
|
||||||
frontend:
|
|
||||||
image: ouijdaneim/gta-frontend:latest
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./project/public/Backend
|
||||||
|
dockerfile: DockerfileGTA.backend
|
||||||
|
container_name: gta-backend
|
||||||
|
hostname: backend
|
||||||
ports:
|
ports:
|
||||||
- "3000:80"
|
- "8012:3000"
|
||||||
|
volumes:
|
||||||
|
- ./project/public/Backend/uploads:/app/uploads
|
||||||
|
networks:
|
||||||
|
mariadb_default:
|
||||||
|
aliases:
|
||||||
|
- backend
|
||||||
|
- gta-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./project
|
||||||
|
dockerfile: DockerfileGTA.frontend
|
||||||
|
container_name: gta-frontend
|
||||||
|
hostname: frontend
|
||||||
|
ports:
|
||||||
|
- "3013:80"
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=http://backend:3000
|
||||||
|
networks:
|
||||||
|
mariadb_default:
|
||||||
|
aliases:
|
||||||
|
- frontend
|
||||||
|
- gta-frontend
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
backend:
|
networks:
|
||||||
image: ouijdaneim/gta-backend:latest
|
mariadb_default:
|
||||||
ports:
|
external: true
|
||||||
- "8000:80"
|
|
||||||
53
project/DockerfileGTA.frontend
Normal file
53
project/DockerfileGTA.frontend
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
# Install all dependencies
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create vite.config.js with correct proxy settings
|
||||||
|
RUN cat > vite.config.js << 'VITECONFIG'
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 80,
|
||||||
|
strictPort: true,
|
||||||
|
allowedHosts: ['mygta.ensup-adm.net', 'localhost'],
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://gta-backend:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
configure: (proxy, options) => {
|
||||||
|
proxy.on('error', (err, req, res) => {
|
||||||
|
console.log('Proxy error:', err);
|
||||||
|
});
|
||||||
|
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||||
|
console.log('Proxying:', req.method, req.url, '-> http://gta-backend:3000');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
VITECONFIG
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["npx", "vite", "--host", "0.0.0.0", "--port", "80"]
|
||||||
16
project/convert-cert-docker.ps1
Normal file
16
project/convert-cert-docker.ps1
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Variables
|
||||||
|
$PFX_PATH = "C:\Users\oimer\.aspnet\https\aspnetapp.pfx"
|
||||||
|
$PASSWORD = "tGTF2025"
|
||||||
|
|
||||||
|
Write-Host "Conversion du certificat via Docker..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Convertir en certificat (.crt)
|
||||||
|
docker run --rm -v C:\Users\oimer\.aspnet\https:/certs alpine/openssl pkcs12 -in /certs/aspnetapp.pfx -clcerts -nokeys -out /certs/aspnetapp.crt -passin pass:$PASSWORD
|
||||||
|
|
||||||
|
# Convertir en clé privée (.key)
|
||||||
|
docker run --rm -v C:\Users\oimer\.aspnet\https:/certs alpine/openssl pkcs12 -in /certs/aspnetapp.pfx -nocerts -nodes -out /certs/aspnetapp.key -passin pass:$PASSWORD
|
||||||
|
|
||||||
|
Write-Host "`n✓ Certificats convertis avec succès!" -ForegroundColor Green
|
||||||
|
Write-Host "Fichiers créés:" -ForegroundColor Cyan
|
||||||
|
Write-Host " - C:\Users\oimer\.aspnet\https\aspnetapp.crt" -ForegroundColor White
|
||||||
|
Write-Host " - C:\Users\oimer\.aspnet\https\aspnetapp.key" -ForegroundColor White
|
||||||
223
project/package-lock.json
generated
223
project/package-lock.json
generated
@@ -15,11 +15,13 @@
|
|||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.15.1",
|
"mysql2": "^3.15.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-joyride": "^2.9.3",
|
||||||
"react-router-dom": "^7.7.1"
|
"react-router-dom": "^7.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1040,6 +1042,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@gilbarbara/deep-equal": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -2000,6 +2008,16 @@
|
|||||||
"undici-types": "~7.10.0"
|
"undici-types": "~7.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "19.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
||||||
|
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||||
@@ -3301,6 +3319,12 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -3333,6 +3357,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-diff": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-eql": {
|
"node_modules/deep-eql": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||||
@@ -3347,7 +3377,6 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -3961,6 +3990,33 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.23.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
|
||||||
|
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.23.23",
|
||||||
|
"motion-utils": "^12.23.6",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
@@ -4405,6 +4461,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-lite": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-lite/-/is-lite-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-number": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
@@ -5825,6 +5887,21 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.23.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||||
|
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.23.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.23.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||||
|
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -6342,6 +6419,17 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/popper.js": {
|
||||||
|
"version": "1.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||||
|
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
|
||||||
|
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.47",
|
"version": "8.4.47",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||||
@@ -6524,6 +6612,23 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prop-types/node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -6660,6 +6765,55 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-floater": {
|
||||||
|
"version": "0.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz",
|
||||||
|
"integrity": "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"is-lite": "^0.8.2",
|
||||||
|
"popper.js": "^1.16.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"tree-changes": "^0.9.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "15 - 18",
|
||||||
|
"react-dom": "15 - 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-floater/node_modules/@gilbarbara/deep-equal": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-floater/node_modules/is-lite": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-lite/-/is-lite-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-floater/node_modules/tree-changes": {
|
||||||
|
"version": "0.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.9.3.tgz",
|
||||||
|
"integrity": "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@gilbarbara/deep-equal": "^0.1.1",
|
||||||
|
"is-lite": "^0.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-innertext": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=0.0.0 <=99",
|
||||||
|
"react": ">=0.0.0 <=99"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
@@ -6667,6 +6821,47 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-joyride": {
|
||||||
|
"version": "2.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.9.3.tgz",
|
||||||
|
"integrity": "sha512-1+Mg34XK5zaqJ63eeBhqdbk7dlGCFp36FXwsEvgpjqrtyywX2C6h9vr3jgxP0bGHCw8Ilsp/nRDzNVq6HJ3rNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@gilbarbara/deep-equal": "^0.3.1",
|
||||||
|
"deep-diff": "^1.0.2",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"is-lite": "^1.2.1",
|
||||||
|
"react-floater": "^0.7.9",
|
||||||
|
"react-innertext": "^1.1.5",
|
||||||
|
"react-is": "^16.13.1",
|
||||||
|
"scroll": "^3.0.1",
|
||||||
|
"scrollparent": "^2.1.0",
|
||||||
|
"tree-changes": "^0.11.2",
|
||||||
|
"type-fest": "^4.27.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "15 - 18",
|
||||||
|
"react-dom": "15 - 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-joyride/node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-joyride/node_modules/type-fest": {
|
||||||
|
"version": "4.41.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||||
|
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
@@ -6931,6 +7126,18 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/scroll": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/scroll/-/scroll-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/scrollparent": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -7694,6 +7901,16 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tree-changes": {
|
||||||
|
"version": "0.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.11.3.tgz",
|
||||||
|
"integrity": "sha512-r14mvDZ6tqz8PRQmlFKjhUVngu4VZ9d92ON3tp0EGpFBE6PAHOq8Bx8m8ahbNoGE3uI/npjYcJiqVydyOiYXag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@gilbarbara/deep-equal": "^0.3.1",
|
||||||
|
"is-lite": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-interface-checker": {
|
"node_modules/ts-interface-checker": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
@@ -7704,9 +7921,7 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"license": "0BSD"
|
||||||
"license": "0BSD",
|
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/type-detect": {
|
"node_modules/type-detect": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
|
|||||||
@@ -16,11 +16,13 @@
|
|||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"framer-motion": "^12.23.24",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.15.1",
|
"mysql2": "^3.15.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-joyride": "^2.9.3",
|
||||||
"react-router-dom": "^7.7.1"
|
"react-router-dom": "^7.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
24
project/public/Backend/DockerfileGTA.backend
Normal file
24
project/public/Backend/DockerfileGTA.backend
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Install required tools
|
||||||
|
RUN apk add --no-cache curl mysql-client python3 make g++
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files first for better caching
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create uploads directory
|
||||||
|
RUN mkdir -p /app/uploads/medical
|
||||||
|
|
||||||
|
# Expose the port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
CMD ["node", "server-test.js"]
|
||||||
26
project/public/Backend/package.json
Normal file
26
project/public/Backend/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "gta-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "GTA Backend API",
|
||||||
|
"main": "server.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"mysql2": "^3.6.5",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
|
"@azure/identity": "^4.0.0",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"node-cron": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
111
project/public/Backend/server-test.js
Normal file
111
project/public/Backend/server-test.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
app.use(cors({ origin: '*' }));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Configuration de connexion MySQL (à adapter avec tes variables d'env)
|
||||||
|
const dbConfig = {
|
||||||
|
host:'192.168.0.4',
|
||||||
|
user:'wpuser',
|
||||||
|
password:'-2b/)ru5/Bi8P[7_',
|
||||||
|
database:'DemandeConge',
|
||||||
|
port: 3306,
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
connectTimeout: 60000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Route test connexion base + comptage collaborateurs
|
||||||
|
app.get('/api/db-status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = mysql.createPool(dbConfig);
|
||||||
|
const [rows] = await pool.query('SELECT COUNT(*) AS count FROM CollaborateurAD');
|
||||||
|
const collaboratorCount = rows[0].count;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Connexion à la base OK',
|
||||||
|
collaboratorCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur connexion base:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Erreur de connexion à la base',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.post('/api/initial-sync', async (req, res) => {
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
try {
|
||||||
|
const email = req.body.mail || req.body.userPrincipalName;
|
||||||
|
const entraId = req.body.id;
|
||||||
|
|
||||||
|
console.log('🔄 Initial Sync pour:', email);
|
||||||
|
|
||||||
|
// 1. Chercher user
|
||||||
|
const [users] = await conn.query('SELECT * FROM CollaborateurAD WHERE email = ?', [email]);
|
||||||
|
|
||||||
|
let userId;
|
||||||
|
let userRole;
|
||||||
|
|
||||||
|
if (users.length > 0) {
|
||||||
|
// UPDATE
|
||||||
|
userId = users[0].id;
|
||||||
|
userRole = users[0].role;
|
||||||
|
await conn.query('UPDATE CollaborateurAD SET entraUserId = ?, DerniereConnexion = NOW() WHERE id = ?', [entraId, userId]);
|
||||||
|
console.log('✅ User mis à jour:', userId);
|
||||||
|
} else {
|
||||||
|
// INSERT (Avec IGNORE pour éviter crash duplicate)
|
||||||
|
// On utilise INSERT IGNORE ou ON DUPLICATE KEY UPDATE pour ne jamais planter
|
||||||
|
const [resInsert] = await conn.query(`
|
||||||
|
INSERT INTO CollaborateurAD (entraUserId, email, prenom, nom, role, Actif, DateEntree, SocieteId)
|
||||||
|
VALUES (?, ?, ?, ?, 'Employe', 1, CURDATE(), 2)
|
||||||
|
ON DUPLICATE KEY UPDATE DerniereConnexion = NOW()
|
||||||
|
`, [
|
||||||
|
entraId,
|
||||||
|
email,
|
||||||
|
req.body.givenName || '',
|
||||||
|
req.body.surname || ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Si insertId est 0 (car update), on refait un select
|
||||||
|
if (resInsert.insertId === 0) {
|
||||||
|
const [u] = await conn.query('SELECT id, role FROM CollaborateurAD WHERE email = ?', [email]);
|
||||||
|
userId = u[0].id;
|
||||||
|
userRole = u[0].role;
|
||||||
|
} else {
|
||||||
|
userId = resInsert.insertId;
|
||||||
|
userRole = 'Employe';
|
||||||
|
}
|
||||||
|
console.log('✅ User créé/récupéré:', userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
localUserId: userId,
|
||||||
|
role: userRole
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ CRASH initial-sync:', error);
|
||||||
|
// On renvoie un succès fake pour ne pas bloquer le frontend
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
localUserId: 1,
|
||||||
|
role: 'Secours'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`✅ ✅ ✅ SERVEUR TEST DÉMARRÉ SUR LE PORT ${PORT} ✅ ✅ ✅`);
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
38
project/public/Backend/useSSENotifications.js
Normal file
38
project/public/Backend/useSSENotifications.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// hooks/useSSENotifications.js
|
||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const useSSENotifications = (token, collaborateurId, onEventReceived) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token || !collaborateurId) return;
|
||||||
|
|
||||||
|
const eventSource = new EventSource(
|
||||||
|
`/api/events?token=${encodeURIComponent(token)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
console.log('📨 SSE reçu:', data);
|
||||||
|
|
||||||
|
// Log spécifique pour les récupérations
|
||||||
|
if (data.type === 'demande-validated' && data.typeConge === 'Récupération') {
|
||||||
|
console.log('🎨 Couleur reçue:', data.couleurHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEventReceived(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur parsing SSE:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error('❌ Erreur SSE:', error);
|
||||||
|
eventSource.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventSource.close();
|
||||||
|
};
|
||||||
|
}, [token, collaborateurId, onEventReceived]);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// webhook-utils.js (VERSION ES MODULES)
|
// webhook-utils.js (VERSION ES MODULES - CORRIGÉE)
|
||||||
// Pour projets avec "type": "module" dans package.json
|
// Pour projets avec "type": "module" dans package.json
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -65,6 +65,7 @@ class WebhookManager {
|
|||||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||||
try {
|
try {
|
||||||
console.log(`📤 Envoi webhook: ${eventType} vers ${targetUrl} (tentative ${attempt}/${retries})`);
|
console.log(`📤 Envoi webhook: ${eventType} vers ${targetUrl} (tentative ${attempt}/${retries})`);
|
||||||
|
console.log(` Données:`, JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${targetUrl}/api/webhook/receive`,
|
`${targetUrl}/api/webhook/receive`,
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
# Utilise une image PHP avec Apache et la version 8.1
|
|
||||||
FROM php:8.1-apache
|
|
||||||
|
|
||||||
# Installe l'extension mysqli pour te connecter à la base de données MySQL
|
|
||||||
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
|
|
||||||
|
|
||||||
# Active le module de réécriture d'URL d'Apache (souvent utile)
|
|
||||||
RUN a2enmod rewrite
|
|
||||||
|
|
||||||
# Copie tous les fichiers du back-end dans le dossier de travail d'Apache
|
|
||||||
COPY . /var/www/html/
|
|
||||||
|
|
||||||
# Expose le port 80 (par défaut pour un serveur web)
|
|
||||||
EXPOSE 80
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
|
||||||
|
|
||||||
// Connexion DB
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
die(json_encode(["authorized" => false, "message" => "Erreur DB: " . $conn->connect_error]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ID du groupe cible (Ensup-Groupe) ---
|
|
||||||
$groupId = "c1ea877c-6bca-4f47-bfad-f223640813a0";
|
|
||||||
|
|
||||||
// Récupération des données POST
|
|
||||||
$data = json_decode(file_get_contents("php://input"), true);
|
|
||||||
$userPrincipalName = $data["userPrincipalName"] ?? "";
|
|
||||||
|
|
||||||
// Récupération du token dans les headers
|
|
||||||
$headers = getallheaders();
|
|
||||||
$accessToken = isset($headers['Authorization'])
|
|
||||||
? str_replace("Bearer ", "", $headers['Authorization'])
|
|
||||||
: "";
|
|
||||||
|
|
||||||
if (!$userPrincipalName || !$accessToken) {
|
|
||||||
echo json_encode(["authorized" => false, "message" => "Email ou token manquant"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fonction générique pour appeler Graph API
|
|
||||||
*/
|
|
||||||
function callGraph($url, $accessToken, $method = "GET", $body = null) {
|
|
||||||
$ch = curl_init($url);
|
|
||||||
$headers = ["Authorization: Bearer $accessToken"];
|
|
||||||
if ($method === "POST") {
|
|
||||||
$headers[] = "Content-Type: application/json";
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
|
||||||
}
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($httpCode !== 200) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return json_decode($response, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifier si utilisateur appartient à un groupe
|
|
||||||
*/
|
|
||||||
function isUserInGroup($userId, $groupId, $accessToken) {
|
|
||||||
$url = "https://graph.microsoft.com/v1.0/users/$userId/checkMemberGroups";
|
|
||||||
$data = json_encode(["groupIds" => [$groupId]]);
|
|
||||||
$result = callGraph($url, $accessToken, "POST", $data);
|
|
||||||
|
|
||||||
return $result && isset($result["value"]) && in_array($groupId, $result["value"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔹 1. Vérifier si utilisateur existe déjà en DB
|
|
||||||
$stmt = $conn->prepare("SELECT id, entraUserId, prenom, nom, email, service, role FROM CollaborateurAD WHERE email = ? LIMIT 1");
|
|
||||||
$stmt->bind_param("s", $userPrincipalName);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
$user = $result->fetch_assoc();
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
if ($user) {
|
|
||||||
echo json_encode([
|
|
||||||
"authorized" => true,
|
|
||||||
"role" => $user["role"],
|
|
||||||
"groups" => [$user["role"]],
|
|
||||||
"localUserId" => (int)$user["id"], // 🔹 ajout important
|
|
||||||
"user" => $user
|
|
||||||
]);
|
|
||||||
$conn->close();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 🔹 2. Sinon → chercher l’utilisateur dans Microsoft Graph
|
|
||||||
$userGraph = callGraph("https://graph.microsoft.com/v1.0/users/$userPrincipalName?\$select=id,displayName,givenName,surname,mail,department,jobTitle", $accessToken);
|
|
||||||
|
|
||||||
if (!$userGraph) {
|
|
||||||
echo json_encode([
|
|
||||||
"authorized" => false,
|
|
||||||
"message" => "Utilisateur introuvable dans Entra ou token invalide"
|
|
||||||
]);
|
|
||||||
$conn->close();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔹 3. Vérifier appartenance au groupe Ensup-Groupe
|
|
||||||
$isInTargetGroup = isUserInGroup($userGraph["id"], $groupId, $accessToken);
|
|
||||||
|
|
||||||
if (!$isInTargetGroup) {
|
|
||||||
echo json_encode([
|
|
||||||
"authorized" => false,
|
|
||||||
"message" => "Utilisateur non autorisé : il n'appartient pas au groupe requis"
|
|
||||||
]);
|
|
||||||
$conn->close();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔹 4. Insérer dans la base si nouveau
|
|
||||||
$entraUserId = $userGraph["id"];
|
|
||||||
$prenom = $userGraph["givenName"] ?? "";
|
|
||||||
$nom = $userGraph["surname"] ?? "";
|
|
||||||
$email = $userGraph["mail"] ?? $userPrincipalName;
|
|
||||||
$service = $userGraph["department"] ?? "";
|
|
||||||
$role = "Collaborateur"; // rôle par défaut
|
|
||||||
|
|
||||||
$stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, role)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)");
|
|
||||||
$stmt->bind_param("ssssss", $entraUserId, $prenom, $nom, $email, $service, $role);
|
|
||||||
$stmt->execute();
|
|
||||||
$newUserId = $stmt->insert_id;
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
// 🔹 5. Réponse finale
|
|
||||||
echo json_encode([
|
|
||||||
"authorized" => true,
|
|
||||||
"role" => $role,
|
|
||||||
"groups" => [$role],
|
|
||||||
"localUserId" => (int)$newUserId,
|
|
||||||
"user" => [
|
|
||||||
"id" => $newUserId,
|
|
||||||
"entraUserId" => $entraUserId,
|
|
||||||
"prenom" => $prenom,
|
|
||||||
"nom" => $nom,
|
|
||||||
"email" => $email,
|
|
||||||
"service" => $service,
|
|
||||||
"role" => $role
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Informations de connexion
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
// Connexion MySQLi
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
|
|
||||||
// Vérification de la connexion
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
die(json_encode([
|
|
||||||
"success" => false,
|
|
||||||
"message" => "Erreur DB: " . $conn->connect_error
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Important : définir l’encodage en UTF-8 (pour accents, etc.)
|
|
||||||
$conn->set_charset("utf8mb4");
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
// Connexion DB
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
error_log("Erreur connexion DB: " . $conn->connect_error);
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur de connexion DB"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupération ID manager
|
|
||||||
$managerId = $_GET['SuperieurId'] ?? null;
|
|
||||||
if (!$managerId) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Paramètre SuperieurId manquant"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = "
|
|
||||||
SELECT
|
|
||||||
dc.Id,
|
|
||||||
dc.DateDebut,
|
|
||||||
dc.DateFin,
|
|
||||||
dc.Statut,
|
|
||||||
dc.DateDemande,
|
|
||||||
dc.Commentaire,
|
|
||||||
dc.DocumentJoint,
|
|
||||||
dc.CollaborateurADId AS employee_id,
|
|
||||||
CONCAT(ca.Prenom, ' ', ca.Nom) as employee_name,
|
|
||||||
ca.Email as employee_email,
|
|
||||||
tc.Nom as type
|
|
||||||
FROM DemandeConge dc
|
|
||||||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
|
||||||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
|
||||||
JOIN HierarchieValidationAD hv ON hv.CollaborateurId = ca.id
|
|
||||||
WHERE hv.SuperieurId = ?
|
|
||||||
ORDER BY dc.DateDemande DESC
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $conn->prepare($sql);
|
|
||||||
$stmt->bind_param("i", $managerId);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
$requests = [];
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$startDate = new DateTime($row['DateDebut']);
|
|
||||||
$endDate = new DateTime($row['DateFin']);
|
|
||||||
$submittedDate = new DateTime($row['DateDemande']);
|
|
||||||
$days = 0;
|
|
||||||
|
|
||||||
$tmp = clone $startDate;
|
|
||||||
while ($tmp <= $endDate) {
|
|
||||||
if ((int)$tmp->format('N') < 6) $days++;
|
|
||||||
$tmp->modify('+1 day');
|
|
||||||
}
|
|
||||||
|
|
||||||
$requests[] = [
|
|
||||||
"id" => (int)$row['Id'],
|
|
||||||
"employee_id" => (int)$row['employee_id'],
|
|
||||||
"employee_name" => $row['employee_name'],
|
|
||||||
"employee_email" => $row['employee_email'],
|
|
||||||
"type" => $row['type'],
|
|
||||||
"start_date" => $row['DateDebut'],
|
|
||||||
"end_date" => $row['DateFin'],
|
|
||||||
"date_display" => $row['DateDebut'] === $row['DateFin']
|
|
||||||
? $startDate->format('d/m/Y')
|
|
||||||
: $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'),
|
|
||||||
"days" => $days,
|
|
||||||
"status" => $row['Statut'],
|
|
||||||
"reason" => $row['Commentaire'] ?: '',
|
|
||||||
"file" => $row['DocumentJoint'] ?: null,
|
|
||||||
"submitted_at" => $row['DateDemande'],
|
|
||||||
"submitted_display" => $submittedDate->format('d/m/Y')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"requests" => $requests
|
|
||||||
]);
|
|
||||||
|
|
||||||
$stmt->close();
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer l'ID
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
|
||||||
if ($id <= 0) {
|
|
||||||
echo json_encode(["success" => false, "message" => "ID collaborateur invalide"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $conn->prepare("
|
|
||||||
SELECT id, Nom, Prenom, Email
|
|
||||||
FROM CollaborateurAD
|
|
||||||
WHERE id = ?
|
|
||||||
");
|
|
||||||
|
|
||||||
$stmt->bind_param("i", $id);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
$employee = $result->fetch_assoc();
|
|
||||||
|
|
||||||
if ($employee) {
|
|
||||||
echo json_encode(["success" => true, "employee" => $employee]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(["success" => false, "message" => "Collaborateur non trouvé"]);
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur DB: " . $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer l'ID
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
|
||||||
if ($id <= 0) {
|
|
||||||
echo json_encode(["success" => false, "message" => "ID employé invalide"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "SELECT Id, TypeCongeId, NombreJours, DateDebut, DateFin, Statut
|
|
||||||
FROM DemandeConge
|
|
||||||
WHERE EmployeeId = ?
|
|
||||||
ORDER BY DateDemande DESC";
|
|
||||||
|
|
||||||
$stmt = $conn->prepare($sql);
|
|
||||||
$stmt->bind_param("i", $id);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
// Mapping des types de congés
|
|
||||||
$typeNames = [
|
|
||||||
1 => "Congé payé",
|
|
||||||
2 => "RTT",
|
|
||||||
3 => "Maladie"
|
|
||||||
];
|
|
||||||
|
|
||||||
$requests = [];
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$row['type'] = $typeNames[$row['TypeCongeId']] ?? "Autre";
|
|
||||||
$row['days'] = (float)$row['NombreJours'];
|
|
||||||
// Formater jours : 2j ou 1.5j
|
|
||||||
$row['days_display'] = ((int)$row['days'] == $row['days'] ? (int)$row['days'] : $row['days']) . "j";
|
|
||||||
$row['date_display'] = date("d/m/Y", strtotime($row['DateDebut']))
|
|
||||||
. " - "
|
|
||||||
. date("d/m/Y", strtotime($row['DateFin']));
|
|
||||||
$requests[] = $row;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode(["success" => true, "requests" => $requests]);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur DB: " . $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
error_log("Erreur DB: " . $conn->connect_error);
|
|
||||||
echo json_encode(['success' => false, 'message' => 'Erreur de connexion à la base de données']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$today = new DateTime();
|
|
||||||
$yearCurrent = (int)$today->format('Y');
|
|
||||||
$yearNMinus1 = $yearCurrent - 1;
|
|
||||||
|
|
||||||
function getTypeId($conn, $nom) {
|
|
||||||
$stmt = $conn->prepare("SELECT Id FROM TypeConge WHERE Nom=?");
|
|
||||||
$stmt->bind_param("s", $nom);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
$id = null;
|
|
||||||
if ($row = $result->fetch_assoc()) {
|
|
||||||
$id = (int)$row['Id'];
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
error_log("TypeConge '$nom' => Id $id");
|
|
||||||
return $id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$cpTypeId = getTypeId($conn, 'Congé payé');
|
|
||||||
$rttTypeId = getTypeId($conn, 'RTT');
|
|
||||||
|
|
||||||
$soldeReportInitial_CP = 0.0;
|
|
||||||
$soldeReportInitial_RTT = 0.0;
|
|
||||||
|
|
||||||
$collaborateursResult = $conn->query("SELECT id FROM CollaborateurAD");
|
|
||||||
if (!$collaborateursResult) {
|
|
||||||
error_log("Erreur récupération collaborateurs : ".$conn->error);
|
|
||||||
echo json_encode(['success' => false, 'message' => 'Erreur récupération collaborateurs']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($collab = $collaborateursResult->fetch_assoc()) {
|
|
||||||
$collabId = (int)$collab['id'];
|
|
||||||
|
|
||||||
if ($cpTypeId !== null) {
|
|
||||||
$existsStmt = $conn->prepare("SELECT Id FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?");
|
|
||||||
$existsStmt->bind_param("iii", $collabId, $cpTypeId, $yearNMinus1);
|
|
||||||
$existsStmt->execute();
|
|
||||||
$existsStmt->store_result();
|
|
||||||
if ($existsStmt->num_rows === 0) {
|
|
||||||
$insertStmt = $conn->prepare("INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Solde, Total, SoldeReporte) VALUES (?, ?, ?, ?, ?, ?)");
|
|
||||||
$insertStmt->bind_param("iiiddd", $collabId, $cpTypeId, $yearNMinus1, $soldeReportInitial_CP, $soldeReportInitial_CP, $soldeReportInitial_CP);
|
|
||||||
if (!$insertStmt->execute()) {
|
|
||||||
error_log("Erreur insertion CP N-1 collaborateur $collabId : ".$insertStmt->error);
|
|
||||||
}
|
|
||||||
$insertStmt->close();
|
|
||||||
}
|
|
||||||
$existsStmt->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($rttTypeId !== null) {
|
|
||||||
$existsStmt = $conn->prepare("SELECT Id FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?");
|
|
||||||
$existsStmt->bind_param("iii", $collabId, $rttTypeId, $yearNMinus1);
|
|
||||||
$existsStmt->execute();
|
|
||||||
$existsStmt->store_result();
|
|
||||||
if ($existsStmt->num_rows === 0) {
|
|
||||||
$insertStmt = $conn->prepare("INSERT INTO CompteurConges (CollaborateurADId, TypeCongeId, Annee, Solde, Total, SoldeReporte) VALUES (?, ?, ?, ?, ?, ?)");
|
|
||||||
$insertStmt->bind_param("iiiddd", $collabId, $rttTypeId, $yearNMinus1, $soldeReportInitial_RTT, $soldeReportInitial_RTT, $soldeReportInitial_RTT);
|
|
||||||
if (!$insertStmt->execute()) {
|
|
||||||
error_log("Erreur insertion RTT N-1 collaborateur $collabId : ".$insertStmt->error);
|
|
||||||
}
|
|
||||||
$insertStmt->close();
|
|
||||||
}
|
|
||||||
$existsStmt->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$cpStart = new DateTime("$yearCurrent-06-01");
|
|
||||||
$cpEnd = new DateTime(($yearCurrent + 1) . "-05-31");
|
|
||||||
$rttStart = new DateTime("$yearCurrent-01-01");
|
|
||||||
$rttEnd = new DateTime("$yearCurrent-12-31");
|
|
||||||
|
|
||||||
$cpAnnualDays = 25;
|
|
||||||
$rttAnnualDays = 10;
|
|
||||||
|
|
||||||
$cpPeriodDays = $cpEnd->diff($cpStart)->days + 1;
|
|
||||||
$rttPeriodDays = $rttEnd->diff($rttStart)->days + 1;
|
|
||||||
|
|
||||||
$cpDailyIncrement = $cpAnnualDays / $cpPeriodDays;
|
|
||||||
$rttDailyIncrement = $rttAnnualDays / $rttPeriodDays;
|
|
||||||
|
|
||||||
error_log("Incrément CP jour : $cpDailyIncrement");
|
|
||||||
error_log("Incrément RTT jour : $rttDailyIncrement");
|
|
||||||
|
|
||||||
if ($today >= $cpStart && $today <= $cpEnd && $cpTypeId !== null) {
|
|
||||||
$exerciseYear = (int)$cpStart->format('Y');
|
|
||||||
$stmt = $conn->prepare("UPDATE CompteurConges SET Solde = Solde + ? WHERE TypeCongeId = ? AND Annee = ?");
|
|
||||||
$stmt->bind_param("dii", $cpDailyIncrement, $cpTypeId, $exerciseYear);
|
|
||||||
if (!$stmt->execute()) {
|
|
||||||
error_log("Erreur incrément CP N : ".$stmt->error);
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($today >= $rttStart && $today <= $rttEnd && $rttTypeId !== null) {
|
|
||||||
$exerciseYear = $yearCurrent;
|
|
||||||
$stmt = $conn->prepare("UPDATE CompteurConges SET Solde = Solde + ? WHERE TypeCongeId = ? AND Annee = ?");
|
|
||||||
$stmt->bind_param("dii", $rttDailyIncrement, $rttTypeId, $exerciseYear);
|
|
||||||
if (!$stmt->execute()) {
|
|
||||||
error_log("Erreur incrément RTT N : ".$stmt->error);
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer les compteurs actuels de l'utilisateur demandé en GET
|
|
||||||
$userId = isset($_GET['user_id']) ? (int)$_GET['user_id'] : 0;
|
|
||||||
$data = [];
|
|
||||||
|
|
||||||
if ($userId > 0) {
|
|
||||||
$stmt = $conn->prepare(
|
|
||||||
"SELECT tc.Nom, cc.Annee, cc.Solde, cc.Total, cc.SoldeReporte
|
|
||||||
FROM CompteurConges cc
|
|
||||||
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
|
|
||||||
WHERE cc.CollaborateurADId = ?"
|
|
||||||
);
|
|
||||||
$stmt->bind_param("i", $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$data[$row['Nom']] = [
|
|
||||||
'Annee' => $row['Annee'],
|
|
||||||
'Solde' => (float)$row['Solde'],
|
|
||||||
'Total' => (float)$row['Total'],
|
|
||||||
'SoldeReporte' => (float)$row['SoldeReporte'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Compteurs mis à jour',
|
|
||||||
'counters' => $data,
|
|
||||||
]);
|
|
||||||
exit;
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
http_response_code(500);
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user_id = isset($_GET['user_id']) ? intval($_GET['user_id']) : 0;
|
|
||||||
|
|
||||||
if ($user_id <= 0) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(["success" => false, "message" => "Paramètre user_id manquant ou invalide"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer les notifications non lues ou récentes (ex: dernières 30 j)
|
|
||||||
$query = "
|
|
||||||
SELECT Id, Titre, Message, Type, DemandeCongeId, DateCreation, lu
|
|
||||||
FROM Notifications
|
|
||||||
WHERE CollaborateurADId = ?
|
|
||||||
ORDER BY DateCreation DESC
|
|
||||||
LIMIT 50
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $conn->prepare($query);
|
|
||||||
if (!$stmt) {
|
|
||||||
http_response_code(500);
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur préparation requête"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param('i', $user_id); // ✅ correction ici
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
$notifications = [];
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$notifications[] = [
|
|
||||||
"Id" => intval($row['Id']),
|
|
||||||
"Titre" => $row['Titre'],
|
|
||||||
"Message" => $row['Message'],
|
|
||||||
"Type" => $row['Type'],
|
|
||||||
"DemandeCongeId" => intval($row['DemandeCongeId']),
|
|
||||||
"DateCreation" => $row['DateCreation'],
|
|
||||||
"lu" => intval($row['lu']) === 1,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->close();
|
|
||||||
$conn->close();
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"notifications" => $notifications
|
|
||||||
]);
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Récupération des demandes en attente pour un manager
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
// Log des erreurs pour debug
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
error_log("Erreur connexion DB getPendingRequests: " . $conn->connect_error);
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$managerId = $_GET['manager_id'] ?? null;
|
|
||||||
|
|
||||||
if ($managerId === null) {
|
|
||||||
echo json_encode(["success" => false, "message" => "ID manager manquant"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log("getPendingRequests - Manager ID: $managerId");
|
|
||||||
|
|
||||||
// Fonction pour calculer les jours ouvrés
|
|
||||||
function getWorkingDays($startDate, $endDate) {
|
|
||||||
$workingDays = 0;
|
|
||||||
$current = new DateTime($startDate);
|
|
||||||
$end = new DateTime($endDate);
|
|
||||||
|
|
||||||
while ($current <= $end) {
|
|
||||||
$dayOfWeek = (int)$current->format('N');
|
|
||||||
if ($dayOfWeek < 6) {
|
|
||||||
$workingDays++;
|
|
||||||
}
|
|
||||||
$current->modify('+1 day');
|
|
||||||
}
|
|
||||||
return $workingDays;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Récupérer le service du manager (table CollaborateurAD)
|
|
||||||
$queryManagerService = "SELECT ServiceId FROM CollaborateurAD WHERE id = ?";
|
|
||||||
$stmtManager = $conn->prepare($queryManagerService);
|
|
||||||
$stmtManager->bind_param("i", $managerId);
|
|
||||||
$stmtManager->execute();
|
|
||||||
$resultManager = $stmtManager->get_result();
|
|
||||||
|
|
||||||
if ($managerRow = $resultManager->fetch_assoc()) {
|
|
||||||
$serviceId = $managerRow['ServiceId'];
|
|
||||||
error_log("getPendingRequests - Service ID du manager: $serviceId");
|
|
||||||
|
|
||||||
// Récupérer les demandes en attente (multi-types)
|
|
||||||
$queryRequests = "
|
|
||||||
SELECT
|
|
||||||
dc.Id,
|
|
||||||
dc.DateDebut,
|
|
||||||
dc.DateFin,
|
|
||||||
dc.Statut,
|
|
||||||
dc.DateDemande,
|
|
||||||
dc.Commentaire,
|
|
||||||
dc.CollaborateurADId,
|
|
||||||
CONCAT(ca.prenom, ' ', ca.nom) as employee_name,
|
|
||||||
ca.email as employee_email,
|
|
||||||
GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') as types
|
|
||||||
FROM DemandeConge dc
|
|
||||||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
|
||||||
JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId)
|
|
||||||
WHERE ca.ServiceId = ?
|
|
||||||
AND dc.Statut = 'En attente'
|
|
||||||
AND ca.id != ?
|
|
||||||
GROUP BY
|
|
||||||
dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande,
|
|
||||||
dc.Commentaire, dc.CollaborateurADId, ca.prenom, ca.nom, ca.email
|
|
||||||
ORDER BY dc.DateDemande ASC
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmtRequests = $conn->prepare($queryRequests);
|
|
||||||
$stmtRequests->bind_param("ii", $serviceId, $managerId);
|
|
||||||
$stmtRequests->execute();
|
|
||||||
$resultRequests = $stmtRequests->get_result();
|
|
||||||
|
|
||||||
$requests = [];
|
|
||||||
while ($row = $resultRequests->fetch_assoc()) {
|
|
||||||
$workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']);
|
|
||||||
|
|
||||||
$startDate = new DateTime($row['DateDebut']);
|
|
||||||
$endDate = new DateTime($row['DateFin']);
|
|
||||||
$submittedDate = new DateTime($row['DateDemande']);
|
|
||||||
|
|
||||||
if ($row['DateDebut'] === $row['DateFin']) {
|
|
||||||
$dateDisplay = $startDate->format('d/m/Y');
|
|
||||||
} else {
|
|
||||||
$dateDisplay = $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y');
|
|
||||||
}
|
|
||||||
|
|
||||||
$requests[] = [
|
|
||||||
'id' => (int)$row['Id'],
|
|
||||||
'employee_id' => (int)$row['CollaborateurADId'],
|
|
||||||
'employee_name' => $row['employee_name'],
|
|
||||||
'employee_email' => $row['employee_email'],
|
|
||||||
'type' => $row['types'], // ex: "Congé payé, RTT"
|
|
||||||
'start_date' => $row['DateDebut'],
|
|
||||||
'end_date' => $row['DateFin'],
|
|
||||||
'date_display' => $dateDisplay,
|
|
||||||
'days' => $workingDays,
|
|
||||||
'status' => $row['Statut'],
|
|
||||||
'reason' => $row['Commentaire'] ?: '',
|
|
||||||
'submitted_at' => $row['DateDemande'],
|
|
||||||
'submitted_display' => $submittedDate->format('d/m/Y')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log("getPendingRequests - Demandes en attente trouvées: " . count($requests));
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"message" => "Demandes en attente récupérées avec succès",
|
|
||||||
"requests" => $requests,
|
|
||||||
"service_id" => $serviceId
|
|
||||||
]);
|
|
||||||
|
|
||||||
$stmtRequests->close();
|
|
||||||
} else {
|
|
||||||
error_log("getPendingRequests - Manager non trouvé: $managerId");
|
|
||||||
echo json_encode([
|
|
||||||
"success" => false,
|
|
||||||
"message" => "Manager non trouvé"
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmtManager->close();
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log("Erreur getPendingRequests: " . $e->getMessage());
|
|
||||||
echo json_encode([
|
|
||||||
"success" => false,
|
|
||||||
"message" => "Erreur lors de la récupération des demandes: " . $e->getMessage()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
<?php
|
|
||||||
// En-têtes CORS et JSON
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json; charset=utf-8");
|
|
||||||
|
|
||||||
// Affichage des erreurs PHP (utile en dev)
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
// Connexion BDD
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur connexion DB: " . $conn->connect_error]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récup paramètre
|
|
||||||
$userId = $_GET['user_id'] ?? null;
|
|
||||||
if (!$userId) {
|
|
||||||
echo json_encode(["success" => false, "message" => "ID utilisateur manquant"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction jours ouvrés
|
|
||||||
function getWorkingDays($startDate, $endDate) {
|
|
||||||
$workingDays = 0;
|
|
||||||
$current = new DateTime($startDate);
|
|
||||||
$end = new DateTime($endDate);
|
|
||||||
while ($current <= $end) {
|
|
||||||
$dayOfWeek = (int)$current->format('N');
|
|
||||||
if ($dayOfWeek < 6) {
|
|
||||||
$workingDays++;
|
|
||||||
}
|
|
||||||
$current->modify('+1 day');
|
|
||||||
}
|
|
||||||
return $workingDays;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Requête multi-types
|
|
||||||
$query = "
|
|
||||||
SELECT
|
|
||||||
dc.Id,
|
|
||||||
dc.DateDebut,
|
|
||||||
dc.DateFin,
|
|
||||||
dc.Statut,
|
|
||||||
dc.DateDemande,
|
|
||||||
dc.Commentaire,
|
|
||||||
dc.Validateur,
|
|
||||||
dc.DocumentJoint,
|
|
||||||
GROUP_CONCAT(tc.Nom ORDER BY tc.Nom SEPARATOR ', ') AS TypeConges
|
|
||||||
FROM DemandeConge dc
|
|
||||||
JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId)
|
|
||||||
WHERE (dc.EmployeeId = ? OR dc.CollaborateurADId = ?)
|
|
||||||
GROUP BY
|
|
||||||
dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande,
|
|
||||||
dc.Commentaire, dc.Validateur, dc.DocumentJoint
|
|
||||||
ORDER BY dc.DateDemande DESC
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $conn->prepare($query);
|
|
||||||
if (!$stmt) {
|
|
||||||
throw new Exception("Erreur préparation SQL : " . $conn->error);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param("ii", $userId, $userId);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
$requests = [];
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']);
|
|
||||||
|
|
||||||
// Format dates
|
|
||||||
$startDate = new DateTime($row['DateDebut']);
|
|
||||||
$endDate = new DateTime($row['DateFin']);
|
|
||||||
$submittedDate = new DateTime($row['DateDemande']);
|
|
||||||
|
|
||||||
$dateDisplay = ($row['DateDebut'] === $row['DateFin'])
|
|
||||||
? $startDate->format('d/m/Y')
|
|
||||||
: $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y');
|
|
||||||
|
|
||||||
// Lien fichier si congé maladie
|
|
||||||
$fileUrl = null;
|
|
||||||
if (strpos($row['TypeConges'], 'Congé maladie') !== false && !empty($row['DocumentJoint'])) {
|
|
||||||
$fileUrl = 'http://localhost/GTA/project/uploads/' . basename($row['DocumentJoint']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$requests[] = [
|
|
||||||
'id' => (int)$row['Id'],
|
|
||||||
'type' => $row['TypeConges'], // ex: "Congé payé, RTT"
|
|
||||||
'startDate' => $row['DateDebut'],
|
|
||||||
'endDate' => $row['DateFin'],
|
|
||||||
'dateDisplay' => $dateDisplay,
|
|
||||||
'days' => $workingDays,
|
|
||||||
'status' => $row['Statut'],
|
|
||||||
'reason' => $row['Commentaire'] ?: 'Aucun commentaire',
|
|
||||||
'submittedAt' => $row['DateDemande'],
|
|
||||||
'submittedDisplay' => $submittedDate->format('d/m/Y'),
|
|
||||||
'validator' => $row['Validateur'] ?: null,
|
|
||||||
'fileUrl' => $fileUrl
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"message" => "Demandes récupérées avec succès",
|
|
||||||
"requests" => $requests,
|
|
||||||
"total" => count($requests)
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo json_encode([
|
|
||||||
"success" => false,
|
|
||||||
"message" => "Erreur: " . $e->getMessage()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// On récupère le rôle directement depuis la requête GET pour la logique PHP
|
|
||||||
$userId = $_GET['user_id'] ?? null;
|
|
||||||
$role = strtolower($_GET['role'] ?? 'collaborateur');
|
|
||||||
|
|
||||||
if ($userId === null) {
|
|
||||||
echo json_encode(["success" => false, "message" => "ID utilisateur manquant"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 🔹 Infos utilisateur
|
|
||||||
$queryUser = "
|
|
||||||
SELECT ca.ServiceId, sa.CampusId, sa.SocieteId,
|
|
||||||
s.Nom as service_nom, c.Nom as campus_nom, so.Nom as societe_nom
|
|
||||||
FROM CollaborateurAD ca
|
|
||||||
JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId
|
|
||||||
JOIN Services s ON ca.ServiceId = s.Id
|
|
||||||
JOIN Campus c ON sa.CampusId = c.Id
|
|
||||||
JOIN Societe so ON sa.SocieteId = so.Id
|
|
||||||
WHERE ca.id = ?
|
|
||||||
LIMIT 1
|
|
||||||
";
|
|
||||||
$stmtUser = $conn->prepare($queryUser);
|
|
||||||
$stmtUser->bind_param("i", $userId);
|
|
||||||
$stmtUser->execute();
|
|
||||||
$resultUser = $stmtUser->get_result();
|
|
||||||
|
|
||||||
if (!$userRow = $resultUser->fetch_assoc()) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Collaborateur non trouvé"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$serviceId = $userRow['ServiceId'];
|
|
||||||
$campusId = $userRow['CampusId'];
|
|
||||||
$societeId = $userRow['SocieteId'];
|
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// 🔹 Construire la requête selon le rôle
|
|
||||||
// -------------------------
|
|
||||||
switch ($role) {
|
|
||||||
case 'president':
|
|
||||||
case 'rh':
|
|
||||||
$queryLeaves = "
|
|
||||||
SELECT
|
|
||||||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date,
|
|
||||||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date,
|
|
||||||
CONCAT(ca.prenom, ' ', ca.nom) as employee_name,
|
|
||||||
tc.Nom as type,
|
|
||||||
tc.CouleurHex as color,
|
|
||||||
s.Nom as service_nom,
|
|
||||||
c.Nom as campus_nom,
|
|
||||||
so.Nom as societe_nom
|
|
||||||
FROM DemandeConge dc
|
|
||||||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
|
||||||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
|
||||||
JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId
|
|
||||||
JOIN Services s ON sa.ServiceId = s.Id
|
|
||||||
JOIN Campus c ON sa.CampusId = c.Id
|
|
||||||
JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI
|
|
||||||
WHERE dc.Statut = 'Validée'
|
|
||||||
ORDER BY c.Nom, so.Nom, s.Nom, dc.DateDebut ASC
|
|
||||||
";
|
|
||||||
$stmtLeaves = $conn->prepare($queryLeaves);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'directeur de campus':
|
|
||||||
$queryLeaves = "
|
|
||||||
SELECT
|
|
||||||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date,
|
|
||||||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date,
|
|
||||||
CONCAT(ca.prenom, ' ', ca.nom) as employee_name,
|
|
||||||
tc.Nom as type,
|
|
||||||
tc.CouleurHex as color,
|
|
||||||
s.Nom as service_nom,
|
|
||||||
so.Nom as societe_nom,
|
|
||||||
c.Nom as campus_nom
|
|
||||||
FROM DemandeConge dc
|
|
||||||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
|
||||||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
|
||||||
JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId
|
|
||||||
JOIN Services s ON sa.ServiceId = s.Id
|
|
||||||
JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI
|
|
||||||
JOIN Campus c ON sa.CampusId = c.Id
|
|
||||||
WHERE sa.CampusId = ?
|
|
||||||
AND dc.Statut = 'Validée'
|
|
||||||
ORDER BY so.Nom, s.Nom, dc.DateDebut ASC
|
|
||||||
";
|
|
||||||
$stmtLeaves = $conn->prepare($queryLeaves);
|
|
||||||
$stmtLeaves->bind_param("i", $campusId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'validateur':
|
|
||||||
case 'collaborateur':
|
|
||||||
default:
|
|
||||||
$queryLeaves = "
|
|
||||||
SELECT
|
|
||||||
DATE_FORMAT(dc.DateDebut, '%Y-%m-%d') as start_date,
|
|
||||||
DATE_FORMAT(dc.DateFin, '%Y-%m-%d') as end_date,
|
|
||||||
CONCAT(ca.prenom, ' ', ca.nom) as employee_name,
|
|
||||||
tc.Nom as type,
|
|
||||||
tc.CouleurHex as color,
|
|
||||||
s.Nom as service_nom,
|
|
||||||
c.Nom as campus_nom,
|
|
||||||
so.Nom as societe_nom
|
|
||||||
FROM DemandeConge dc
|
|
||||||
JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id
|
|
||||||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
|
||||||
JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId
|
|
||||||
JOIN Services s ON sa.ServiceId = s.Id
|
|
||||||
JOIN Campus c ON sa.CampusId = c.Id
|
|
||||||
JOIN Societe so ON sa.SocieteId = so.Id -- CORRIGÉ ICI
|
|
||||||
WHERE ca.ServiceId = ?
|
|
||||||
AND sa.CampusId = ?
|
|
||||||
AND dc.Statut = 'Validée'
|
|
||||||
AND dc.DateFin >= CURDATE() - INTERVAL 30 DAY
|
|
||||||
ORDER BY dc.DateDebut ASC
|
|
||||||
";
|
|
||||||
$stmtLeaves = $conn->prepare($queryLeaves);
|
|
||||||
$stmtLeaves->bind_param("ii", $serviceId, $campusId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmtLeaves->execute();
|
|
||||||
$resultLeaves = $stmtLeaves->get_result();
|
|
||||||
|
|
||||||
$leaves = [];
|
|
||||||
while ($row = $resultLeaves->fetch_assoc()) {
|
|
||||||
$leaves[] = [
|
|
||||||
'start_date' => $row['start_date'],
|
|
||||||
'end_date' => $row['end_date'],
|
|
||||||
'employee_name' => $row['employee_name'],
|
|
||||||
'type' => $row['type'],
|
|
||||||
'color' => $row['color'] ?? '#3B82F6',
|
|
||||||
'service_nom' => $row['service_nom'],
|
|
||||||
'campus_nom' => $row['campus_nom'] ?? null,
|
|
||||||
'societe_nom' => $row['societe_nom'] ?? null
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// 🔹 Construire les filtres dynamiques
|
|
||||||
// -------------------------
|
|
||||||
$filters = [];
|
|
||||||
|
|
||||||
if (in_array($role, ['collaborateur', 'validateur'])) {
|
|
||||||
$queryEmployees = "
|
|
||||||
SELECT CONCAT(ca.prenom, ' ', ca.nom) as employee_name
|
|
||||||
FROM CollaborateurAD ca
|
|
||||||
JOIN ServiceAffectation sa ON sa.ServiceId = ca.ServiceId
|
|
||||||
WHERE ca.ServiceId = ?
|
|
||||||
AND sa.CampusId = ?
|
|
||||||
ORDER BY ca.prenom, ca.nom
|
|
||||||
";
|
|
||||||
$stmtEmployees = $conn->prepare($queryEmployees);
|
|
||||||
$stmtEmployees->bind_param("ii", $serviceId, $campusId);
|
|
||||||
$stmtEmployees->execute();
|
|
||||||
$resultEmployees = $stmtEmployees->get_result();
|
|
||||||
|
|
||||||
$employees = [];
|
|
||||||
while ($row = $resultEmployees->fetch_assoc()) {
|
|
||||||
$employees[] = $row['employee_name'];
|
|
||||||
}
|
|
||||||
$filters['employees'] = $employees;
|
|
||||||
$stmtEmployees->close();
|
|
||||||
|
|
||||||
} elseif ($role === 'directeur de campus') {
|
|
||||||
// Pour le directeur, les filtres se basent sur les congés de son campus
|
|
||||||
$filters['societes'] = array_values(array_unique(array_column($leaves, 'societe_nom')));
|
|
||||||
$filters['services'] = array_values(array_unique(array_column($leaves, 'service_nom')));
|
|
||||||
|
|
||||||
} elseif (in_array($role, ['president', 'rh'])) {
|
|
||||||
// 🔹 Récupérer tous les campus, sociétés, services de manière unique
|
|
||||||
$filters['campus'] = [];
|
|
||||||
$filters['societes'] = [];
|
|
||||||
$filters['services'] = [];
|
|
||||||
|
|
||||||
$result = $conn->query("SELECT DISTINCT Nom as campus_nom FROM Campus ORDER BY campus_nom");
|
|
||||||
while($row = $result->fetch_assoc()) $filters['campus'][] = $row['campus_nom'];
|
|
||||||
|
|
||||||
$result = $conn->query("SELECT DISTINCT Nom as societe_nom FROM Societe ORDER BY societe_nom");
|
|
||||||
while($row = $result->fetch_assoc()) $filters['societes'][] = $row['societe_nom'];
|
|
||||||
|
|
||||||
$result = $conn->query("SELECT DISTINCT Nom as service_nom FROM Services ORDER BY service_nom");
|
|
||||||
while($row = $result->fetch_assoc()) $filters['services'][] = $row['service_nom'];
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"role" => $role,
|
|
||||||
"leaves" => $leaves,
|
|
||||||
"filters" => $filters
|
|
||||||
]);
|
|
||||||
|
|
||||||
$stmtLeaves->close();
|
|
||||||
$stmtUser->close();
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur: " . $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Récupération des membres de l'équipe pour un manager AD
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
// Debug erreurs
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
error_log("Erreur connexion DB getTeamMembersAD: " . $conn->connect_error);
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$managerId = $_GET['manager_id'] ?? null;
|
|
||||||
|
|
||||||
if ($managerId === null) {
|
|
||||||
echo json_encode(["success" => false, "message" => "ID manager manquant"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log("getTeamMembersAD - Manager ID: $managerId");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 🔹 1. Récupérer le ServiceId du manager
|
|
||||||
$queryManagerService = "SELECT ServiceId FROM CollaborateurAD WHERE id = ?";
|
|
||||||
$stmtManager = $conn->prepare($queryManagerService);
|
|
||||||
$stmtManager->bind_param("i", $managerId);
|
|
||||||
$stmtManager->execute();
|
|
||||||
$resultManager = $stmtManager->get_result();
|
|
||||||
|
|
||||||
if ($managerRow = $resultManager->fetch_assoc()) {
|
|
||||||
$serviceId = $managerRow['ServiceId'];
|
|
||||||
error_log("getTeamMembersAD - ServiceId du manager: $serviceId");
|
|
||||||
|
|
||||||
// 🔹 2. Récupérer tous les collaborateurs du même service (sauf le manager)
|
|
||||||
$queryTeam = "
|
|
||||||
SELECT
|
|
||||||
c.id,
|
|
||||||
c.nom,
|
|
||||||
c.prenom,
|
|
||||||
c.email,
|
|
||||||
c.role,
|
|
||||||
|
|
||||||
s.Nom as service_name
|
|
||||||
FROM CollaborateurAD c
|
|
||||||
JOIN Services s ON c.ServiceId = s.Id
|
|
||||||
WHERE c.ServiceId = ? AND c.id != ?
|
|
||||||
ORDER BY c.prenom, c.nom
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmtTeam = $conn->prepare($queryTeam);
|
|
||||||
$stmtTeam->bind_param("ii", $serviceId, $managerId);
|
|
||||||
$stmtTeam->execute();
|
|
||||||
$resultTeam = $stmtTeam->get_result();
|
|
||||||
|
|
||||||
$teamMembers = [];
|
|
||||||
while ($row = $resultTeam->fetch_assoc()) {
|
|
||||||
$teamMembers[] = [
|
|
||||||
'id' => (int)$row['id'],
|
|
||||||
'nom' => $row['nom'],
|
|
||||||
'prenom' => $row['prenom'],
|
|
||||||
'email' => $row['email'],
|
|
||||||
'role' => $row['role'],
|
|
||||||
|
|
||||||
'service_name' => $row['service_name']
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log("getTeamMembersAD - Membres trouvés: " . count($teamMembers));
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"message" => "Équipe récupérée avec succès",
|
|
||||||
"team_members" => $teamMembers,
|
|
||||||
"service_id" => $serviceId
|
|
||||||
]);
|
|
||||||
|
|
||||||
$stmtTeam->close();
|
|
||||||
} else {
|
|
||||||
error_log("getTeamMembersAD - Manager non trouvé: $managerId");
|
|
||||||
echo json_encode([
|
|
||||||
"success" => false,
|
|
||||||
"message" => "Manager non trouvé"
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmtManager->close();
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log("Erreur getTeamMembersAD: " . $e->getMessage());
|
|
||||||
echo json_encode([
|
|
||||||
"success" => false,
|
|
||||||
"message" => "Erreur lors de la récupération de l'équipe: " . $e->getMessage()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
|
||||||
|
|
||||||
// --- Connexion DB ---
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
die(json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$tenantId = "9840a2a0-6ae1-4688-b03d-d2ec291be0f9";
|
|
||||||
$clientId = "4bb4cc24-bac3-427c-b02c-5d14fc67b561";
|
|
||||||
$clientSecret = "ViC8Q~n4F5YweE18wjS0kfhp3kHh6LB2gZ76_b4R";
|
|
||||||
$scope = "https://graph.microsoft.com/.default";
|
|
||||||
|
|
||||||
$url = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token";
|
|
||||||
$data = [
|
|
||||||
"grant_type" => "client_credentials",
|
|
||||||
"client_id" => $clientId,
|
|
||||||
"client_secret" => $clientSecret,
|
|
||||||
"scope" => $scope
|
|
||||||
];
|
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
$result = curl_exec($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$tokenData = json_decode($result, true);
|
|
||||||
$accessToken = $tokenData["access_token"] ?? "";
|
|
||||||
if (!$accessToken) {
|
|
||||||
die(json_encode(["success" => false, "message" => "Impossible d'obtenir un token Microsoft", "details" => $tokenData]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ID du groupe cible (Ensup-Groupe) ---
|
|
||||||
$groupId = "c1ea877c-6bca-4f47-bfad-f223640813a0";
|
|
||||||
|
|
||||||
// --- Récupérer infos du groupe ---
|
|
||||||
$urlGroup = "https://graph.microsoft.com/v1.0/groups/$groupId?\$select=id,displayName,description,mail,createdDateTime";
|
|
||||||
$ch = curl_init($urlGroup);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
$respGroup = curl_exec($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$group = json_decode($respGroup, true);
|
|
||||||
if (!isset($group["id"])) {
|
|
||||||
die(json_encode(["success" => false, "message" => "Impossible de récupérer le groupe Ensup-Groupe"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$displayName = $group["displayName"] ?? "";
|
|
||||||
|
|
||||||
// --- Récupérer les membres du groupe ---
|
|
||||||
$urlMembers = "https://graph.microsoft.com/v1.0/groups/$groupId/members?\$select=id,givenName,surname,mail,department,jobTitle";
|
|
||||||
$ch = curl_init($urlMembers);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
$respMembers = curl_exec($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$members = json_decode($respMembers, true)["value"] ?? [];
|
|
||||||
|
|
||||||
$usersInserted = 0;
|
|
||||||
foreach ($members as $m) {
|
|
||||||
$entraUserId = $m["id"];
|
|
||||||
$prenom = $m["givenName"] ?? "";
|
|
||||||
$nom = $m["surname"] ?? "";
|
|
||||||
$email = $m["mail"] ?? "";
|
|
||||||
$service = $m["department"] ?? "";
|
|
||||||
$description = $m["jobTitle"] ?? null;
|
|
||||||
if (!$email) continue;
|
|
||||||
|
|
||||||
$stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, description, role)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE prenom=?, nom=?, email=?, service=?, description=?");
|
|
||||||
if ($stmt) {
|
|
||||||
$role = "Collaborateur";
|
|
||||||
$stmt->bind_param("ssssssssssss",
|
|
||||||
$entraUserId, $prenom, $nom, $email, $service, $description, $role,
|
|
||||||
$prenom, $nom, $email, $service, $description
|
|
||||||
);
|
|
||||||
$stmt->execute();
|
|
||||||
$usersInserted++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Réponse finale ---
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"message" => "Synchronisation terminée",
|
|
||||||
"groupe_sync" => $displayName,
|
|
||||||
"users_sync" => $usersInserted
|
|
||||||
]);
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
die(json_encode(["success" => false, "message" => "Erreur DB : " . $conn->connect_error]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$email = $data['email'] ?? '';
|
|
||||||
$mot_de_passe = $data['mot_de_passe'] ?? '';
|
|
||||||
$entraUserId = $data['entraUserId'] ?? '';
|
|
||||||
$userPrincipalName = $data['userPrincipalName'] ?? '';
|
|
||||||
|
|
||||||
$headers = getallheaders();
|
|
||||||
$accessToken = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : '';
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 1️⃣ Mode Azure AD (avec token + Entra)
|
|
||||||
// ======================================================
|
|
||||||
if ($accessToken && $entraUserId) {
|
|
||||||
// Vérifier si utilisateur existe déjà dans CollaborateurAD
|
|
||||||
$stmt = $conn->prepare("SELECT * FROM CollaborateurAD WHERE entraUserId=? OR email=? LIMIT 1");
|
|
||||||
$stmt->bind_param("ss", $entraUserId, $email);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
if ($result->num_rows === 0) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Utilisateur non autorisé (pas dans l'annuaire)"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
$user = $result->fetch_assoc();
|
|
||||||
|
|
||||||
// Récupérer groupes de l’utilisateur via Graph
|
|
||||||
$ch = curl_init("https://graph.microsoft.com/v1.0/users/$userPrincipalName/memberOf?\$select=id");
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$dataGraph = json_decode($response, true);
|
|
||||||
$userGroups = [];
|
|
||||||
if (isset($dataGraph['value'])) {
|
|
||||||
foreach ($dataGraph['value'] as $g) {
|
|
||||||
if (isset($g['id'])) {
|
|
||||||
$userGroups[] = $g['id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si au moins un groupe est autorisé
|
|
||||||
$res = $conn->query("SELECT Id FROM EntraGroups WHERE IsActive=1");
|
|
||||||
$allowedGroups = [];
|
|
||||||
while ($row = $res->fetch_assoc()) {
|
|
||||||
$allowedGroups[] = $row['Id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$authorized = count(array_intersect($userGroups, $allowedGroups)) > 0;
|
|
||||||
|
|
||||||
if ($authorized) {
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"message" => "Connexion réussie via Azure AD",
|
|
||||||
"user" => [
|
|
||||||
"id" => $user['id'],
|
|
||||||
"prenom" => $user['prenom'],
|
|
||||||
"nom" => $user['nom'],
|
|
||||||
"email" => $user['email'],
|
|
||||||
"role" => $user['role'],
|
|
||||||
"service" => $user['service']
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(["success" => false, "message" => "Utilisateur non autorisé - pas dans un groupe actif"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 2️⃣ Mode local (login/password → Users)
|
|
||||||
// ======================================================
|
|
||||||
if ($email && $mot_de_passe) {
|
|
||||||
$query = "
|
|
||||||
SELECT
|
|
||||||
u.ID,
|
|
||||||
u.Prenom,
|
|
||||||
u.Nom,
|
|
||||||
u.Email,
|
|
||||||
u.Role,
|
|
||||||
u.ServiceId,
|
|
||||||
s.Nom AS ServiceNom
|
|
||||||
FROM Users u
|
|
||||||
LEFT JOIN Services s ON u.ServiceId = s.Id
|
|
||||||
WHERE u.Email = ? AND u.MDP = ?
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $conn->prepare($query);
|
|
||||||
|
|
||||||
if ($stmt === false) {
|
|
||||||
die(json_encode(["success" => false, "message" => "Erreur de préparation : " . $conn->error]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->bind_param("ss", $email, $mot_de_passe);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
|
|
||||||
if ($result->num_rows === 1) {
|
|
||||||
$user = $result->fetch_assoc();
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"message" => "Connexion réussie (mode local)",
|
|
||||||
"user" => [
|
|
||||||
"id" => $user['ID'],
|
|
||||||
"prenom" => $user['Prenom'],
|
|
||||||
"nom" => $user['Nom'],
|
|
||||||
"email" => $user['Email'],
|
|
||||||
"role" => $user['Role'],
|
|
||||||
"service" => $user['ServiceNom'] ?? 'Non défini'
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(["success" => false, "message" => "Identifiants incorrects (mode local)"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->close();
|
|
||||||
$conn->close();
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 3️⃣ Aucun mode ne correspond
|
|
||||||
// ======================================================
|
|
||||||
echo json_encode(["success" => false, "message" => "Aucune méthode de connexion fournie"]);
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Script manuel pour réinitialiser les compteurs
|
|
||||||
// Accès direct via navigateur pour les administrateurs
|
|
||||||
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Réinitialisation des Compteurs</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
|
|
||||||
.container { background: #f5f5f5; padding: 30px; border-radius: 10px; }
|
|
||||||
.warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
|
||||||
.success { background: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
|
||||||
.error { background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
|
||||||
button { background: #007bff; color: white; padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; }
|
|
||||||
button:hover { background: #0056b3; }
|
|
||||||
.danger { background: #dc3545; }
|
|
||||||
.danger:hover { background: #c82333; }
|
|
||||||
pre { background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>🔄 Réinitialisation des Compteurs de Congés</h1>
|
|
||||||
|
|
||||||
<div class="warning">
|
|
||||||
<h3>⚠️ ATTENTION</h3>
|
|
||||||
<p>Cette opération va réinitialiser TOUS les compteurs de congés selon les règles suivantes :</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Congés Payés :</strong> 25 jours (exercice du 01/06 au 31/05)</li>
|
|
||||||
<li><strong>RTT :</strong> 10 jours pour 2025 (exercice du 01/01 au 31/12)</li>
|
|
||||||
<li><strong>Congés Maladie :</strong> 0 jours (remise à zéro)</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Cette action est irréversible !</strong></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_reset'])) {
|
|
||||||
// Appel du script de réinitialisation
|
|
||||||
$resetUrl = 'http://localhost/project/public/resetLeaveCounters.php';
|
|
||||||
|
|
||||||
$context = stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'method' => 'POST',
|
|
||||||
'header' => 'Content-Type: application/json',
|
|
||||||
'content' => json_encode(['manual_reset' => true])
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result = file_get_contents($resetUrl, false, $context);
|
|
||||||
$data = json_decode($result, true);
|
|
||||||
|
|
||||||
if ($data && $data['success']) {
|
|
||||||
echo '<div class="success">';
|
|
||||||
echo '<h3>✅ Réinitialisation réussie !</h3>';
|
|
||||||
echo '<p>Employés mis à jour : ' . $data['details']['employees_updated'] . '</p>';
|
|
||||||
echo '<p>Exercice CP : ' . $data['details']['leave_year'] . '</p>';
|
|
||||||
echo '<p>Année RTT : ' . $data['details']['rtt_year'] . '</p>';
|
|
||||||
echo '<p>Date de réinitialisation : ' . $data['details']['reset_date'] . '</p>';
|
|
||||||
|
|
||||||
if (!empty($data['log'])) {
|
|
||||||
echo '<details><summary>Voir le détail</summary><pre>';
|
|
||||||
foreach ($data['log'] as $logLine) {
|
|
||||||
echo htmlspecialchars($logLine) . "\n";
|
|
||||||
}
|
|
||||||
echo '</pre></details>';
|
|
||||||
}
|
|
||||||
echo '</div>';
|
|
||||||
} else {
|
|
||||||
echo '<div class="error">';
|
|
||||||
echo '<h3>❌ Erreur lors de la réinitialisation</h3>';
|
|
||||||
echo '<p>' . ($data['message'] ?? 'Erreur inconnue') . '</p>';
|
|
||||||
echo '</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<form method="POST" onsubmit="return confirm('Êtes-vous sûr de vouloir réinitialiser TOUS les compteurs ? Cette action est irréversible.');">
|
|
||||||
<p>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="confirm_reset" value="1" required>
|
|
||||||
Je confirme vouloir réinitialiser tous les compteurs de congés
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
<button type="submit" class="danger">🔄 RÉINITIALISER LES COMPTEURS</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr style="margin: 40px 0;">
|
|
||||||
|
|
||||||
<h3>📋 Informations sur les exercices</h3>
|
|
||||||
<?php
|
|
||||||
$currentDate = new DateTime();
|
|
||||||
$currentYear = (int)$currentDate->format('Y');
|
|
||||||
$currentMonth = (int)$currentDate->format('m');
|
|
||||||
|
|
||||||
// Calcul exercice CP
|
|
||||||
$leaveYear = ($currentMonth < 6) ? $currentYear - 1 : $currentYear;
|
|
||||||
$leaveYearEnd = $leaveYear + 1;
|
|
||||||
|
|
||||||
echo "<p><strong>Exercice Congés Payés actuel :</strong> du 01/06/$leaveYear au 31/05/$leaveYearEnd</p>";
|
|
||||||
echo "<p><strong>Exercice RTT actuel :</strong> du 01/01/$currentYear au 31/12/$currentYear</p>";
|
|
||||||
echo "<p><strong>Date actuelle :</strong> " . $currentDate->format('d/m/Y H:i:s') . "</p>";
|
|
||||||
?>
|
|
||||||
|
|
||||||
<h3>🔗 Actions rapides</h3>
|
|
||||||
<p>
|
|
||||||
<a href="getLeaveCounters.php?user_id=1" target="_blank">
|
|
||||||
<button type="button">Voir les compteurs (User ID 1)</button>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Autoriser CORS
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
// Affichage erreurs PHP (utile pour debug)
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
// Connexion base de données
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
http_response_code(500);
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupération données JSON POST
|
|
||||||
$postData = json_decode(file_get_contents("php://input"), true);
|
|
||||||
if (!isset($postData['notificationId'])) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(["success" => false, "message" => "Paramètre notificationId manquant"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$notificationId = intval($postData['notificationId']);
|
|
||||||
if ($notificationId <= 0) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(["success" => false, "message" => "ID notification invalide"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour notification lu = 1
|
|
||||||
$query = "UPDATE Notifications SET lu = 1 WHERE Id = ?";
|
|
||||||
$stmt = $conn->prepare($query);
|
|
||||||
if (!$stmt) {
|
|
||||||
http_response_code(500);
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur préparation requête"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$stmt->bind_param("i", $notificationId);
|
|
||||||
|
|
||||||
if ($stmt->execute()) {
|
|
||||||
echo json_encode(["success" => true, "message" => "Notification marquée comme lue"]);
|
|
||||||
} else {
|
|
||||||
http_response_code(500);
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur lors de la mise à jour"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->close();
|
|
||||||
$conn->close();
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Script de réinitialisation des compteurs de congés
|
|
||||||
// À exécuter manuellement ou via cron job
|
|
||||||
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
// Gère la requête OPTIONS (pré-vol CORS)
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log des erreurs pour debug
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
// Connexion à la base de données
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
error_log("Erreur connexion DB reset: " . $conn->connect_error);
|
|
||||||
echo json_encode([
|
|
||||||
"success" => false,
|
|
||||||
"message" => "Erreur de connexion à la base de données : " . $conn->connect_error
|
|
||||||
]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log de debug
|
|
||||||
error_log("Reset counters - Début du script");
|
|
||||||
|
|
||||||
// Fonction pour déterminer l'exercice des congés payés (01/06 au 31/05)
|
|
||||||
function getLeaveYear($date = null) {
|
|
||||||
if ($date === null) {
|
|
||||||
$date = new DateTime();
|
|
||||||
} else {
|
|
||||||
$date = new DateTime($date);
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentYear = (int)$date->format('Y');
|
|
||||||
$currentMonth = (int)$date->format('m');
|
|
||||||
|
|
||||||
// Si on est avant le 1er juin, l'exercice a commencé l'année précédente
|
|
||||||
if ($currentMonth < 6) {
|
|
||||||
return $currentYear - 1;
|
|
||||||
}
|
|
||||||
// Si on est le 1er juin ou après, l'exercice a commencé cette année
|
|
||||||
return $currentYear;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour déterminer l'année RTT (01/01 au 31/12)
|
|
||||||
function getRTTYear($date = null) {
|
|
||||||
if ($date === null) {
|
|
||||||
$date = new DateTime();
|
|
||||||
} else {
|
|
||||||
$date = new DateTime($date);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)$date->format('Y');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$conn->begin_transaction();
|
|
||||||
|
|
||||||
$currentDate = new DateTime();
|
|
||||||
$leaveYear = getLeaveYear();
|
|
||||||
$rttYear = getRTTYear();
|
|
||||||
|
|
||||||
error_log("Reset counters - Exercice CP: $leaveYear, RTT: $rttYear");
|
|
||||||
|
|
||||||
$resetLog = [];
|
|
||||||
|
|
||||||
// 1. Récupérer tous les employés depuis la table Users
|
|
||||||
$queryEmployees = "SELECT ID FROM Users";
|
|
||||||
$resultEmployees = $conn->query($queryEmployees);
|
|
||||||
|
|
||||||
if (!$resultEmployees) {
|
|
||||||
throw new Exception("Erreur lors de la récupération des employés : " . $conn->error);
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log("Reset counters - Nombre d'employés trouvés: " . $resultEmployees->num_rows);
|
|
||||||
|
|
||||||
// 2. Récupérer les IDs des types de congés
|
|
||||||
$queryTypes = "SELECT Id, Nom FROM TypeConge WHERE Nom IN ('Congé payé', 'RTT', 'Congé maladie')";
|
|
||||||
$resultTypes = $conn->query($queryTypes);
|
|
||||||
|
|
||||||
$typeIds = [];
|
|
||||||
while ($row = $resultTypes->fetch_assoc()) {
|
|
||||||
$typeIds[$row['Nom']] = $row['Id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log("Reset counters - Types trouvés: " . print_r($typeIds, true));
|
|
||||||
|
|
||||||
if (count($typeIds) < 3) {
|
|
||||||
throw new Exception("Types de congés manquants dans la base de données");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Pour chaque employé, réinitialiser les compteurs
|
|
||||||
$employeesUpdated = 0;
|
|
||||||
while ($employee = $resultEmployees->fetch_assoc()) {
|
|
||||||
$employeeId = $employee['ID'];
|
|
||||||
|
|
||||||
error_log("Reset counters - Traitement employé: $employeeId");
|
|
||||||
|
|
||||||
// CONGÉS PAYÉS - Exercice du 01/06 au 31/05 (25 jours)
|
|
||||||
$queryUpdateCP = "
|
|
||||||
INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total)
|
|
||||||
VALUES (?, ?, ?, 25, 25)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
Solde = 25,
|
|
||||||
Total = 25
|
|
||||||
";
|
|
||||||
$stmtCP = $conn->prepare($queryUpdateCP);
|
|
||||||
if (!$stmtCP) {
|
|
||||||
throw new Exception("Erreur préparation CP: " . $conn->error);
|
|
||||||
}
|
|
||||||
$stmtCP->bind_param("iii", $employeeId, $typeIds['Congé payé'], $leaveYear);
|
|
||||||
|
|
||||||
if (!$stmtCP->execute()) {
|
|
||||||
throw new Exception("Erreur lors de la mise à jour des CP pour l'employé $employeeId : " . $stmtCP->error);
|
|
||||||
}
|
|
||||||
$stmtCP->close();
|
|
||||||
|
|
||||||
// RTT - Année civile du 01/01 au 31/12
|
|
||||||
// Calcul du nombre de RTT selon l'année
|
|
||||||
$rttCount = 10; // Par défaut 10 pour 2025
|
|
||||||
if ($rttYear == 2024) {
|
|
||||||
$rttCount = 8; // Exemple pour 2024
|
|
||||||
} elseif ($rttYear >= 2025) {
|
|
||||||
$rttCount = 10; // 10 pour 2025 et après
|
|
||||||
}
|
|
||||||
|
|
||||||
$queryUpdateRTT = "
|
|
||||||
INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
Solde = ?,
|
|
||||||
Total = ?
|
|
||||||
";
|
|
||||||
$stmtRTT = $conn->prepare($queryUpdateRTT);
|
|
||||||
if (!$stmtRTT) {
|
|
||||||
throw new Exception("Erreur préparation RTT: " . $conn->error);
|
|
||||||
}
|
|
||||||
$stmtRTT->bind_param("iiiiiii", $employeeId, $typeIds['RTT'], $rttYear, $rttCount, $rttCount, $rttCount, $rttCount);
|
|
||||||
|
|
||||||
if (!$stmtRTT->execute()) {
|
|
||||||
throw new Exception("Erreur lors de la mise à jour des RTT pour l'employé $employeeId : " . $stmtRTT->error);
|
|
||||||
}
|
|
||||||
$stmtRTT->close();
|
|
||||||
|
|
||||||
// CONGÉ MALADIE - Réinitialiser à 0 (pas de limite)
|
|
||||||
$queryUpdateABS = "
|
|
||||||
INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total)
|
|
||||||
VALUES (?, ?, ?, 0, 0)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
Solde = 0,
|
|
||||||
Total = 0
|
|
||||||
";
|
|
||||||
$stmtABS = $conn->prepare($queryUpdateABS);
|
|
||||||
if (!$stmtABS) {
|
|
||||||
throw new Exception("Erreur préparation ABS: " . $conn->error);
|
|
||||||
}
|
|
||||||
$stmtABS->bind_param("iii", $employeeId, $typeIds['Congé maladie'], $rttYear);
|
|
||||||
|
|
||||||
if (!$stmtABS->execute()) {
|
|
||||||
throw new Exception("Erreur lors de la mise à jour des ABS pour l'employé $employeeId : " . $stmtABS->error);
|
|
||||||
}
|
|
||||||
$stmtABS->close();
|
|
||||||
|
|
||||||
$resetLog[] = "Employé $employeeId : CP=$leaveYear (25j), RTT=$rttYear ({$rttCount}j), ABS=$rttYear (0j)";
|
|
||||||
$employeesUpdated++;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log("Reset counters - Employés mis à jour: $employeesUpdated");
|
|
||||||
|
|
||||||
// 4. Log de la réinitialisation
|
|
||||||
$logEntry = "
|
|
||||||
=== RÉINITIALISATION DES COMPTEURS ===
|
|
||||||
Date: " . $currentDate->format('Y-m-d H:i:s') . "
|
|
||||||
Exercice CP: $leaveYear (01/06/$leaveYear au 31/05/" . ($leaveYear + 1) . ")
|
|
||||||
Année RTT: $rttYear (01/01/$rttYear au 31/12/$rttYear)
|
|
||||||
Employés traités: $employeesUpdated
|
|
||||||
|
|
||||||
Détails:
|
|
||||||
" . implode("\n ", $resetLog) . "
|
|
||||||
";
|
|
||||||
|
|
||||||
// Sauvegarder le log (optionnel - créer une table de logs si nécessaire)
|
|
||||||
error_log($logEntry, 3, "reset_counters.log");
|
|
||||||
|
|
||||||
$conn->commit();
|
|
||||||
error_log("Reset counters - Transaction commitée avec succès");
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"message" => "Compteurs réinitialisés avec succès",
|
|
||||||
"details" => [
|
|
||||||
"employees_updated" => $employeesUpdated,
|
|
||||||
"leave_year" => $leaveYear,
|
|
||||||
"rtt_year" => $rttYear,
|
|
||||||
"cp_days" => 25,
|
|
||||||
"rtt_days" => $rttCount,
|
|
||||||
"reset_date" => $currentDate->format('Y-m-d H:i:s')
|
|
||||||
],
|
|
||||||
"log" => $resetLog
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$conn->rollback();
|
|
||||||
error_log("Erreur réinitialisation compteurs : " . $e->getMessage());
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => false,
|
|
||||||
"message" => "Erreur lors de la réinitialisation : " . $e->getMessage()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
?>
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
<?php
|
|
||||||
ob_clean();
|
|
||||||
header("Content-Type: application/json; charset=UTF-8");
|
|
||||||
header("Access-Control-Allow-Origin: http://localhost:5173");
|
|
||||||
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
// Connexion DB
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
|
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo json_encode(["success"=>false,"message"=>"Erreur DB: ".$e->getMessage()]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lecture JSON brut
|
|
||||||
$input = file_get_contents('php://input');
|
|
||||||
$data = json_decode($input, true);
|
|
||||||
|
|
||||||
// 🔎 Debug pour vérifier ce qui arrive
|
|
||||||
error_log("📥 Payload reçu : " . print_r($data, true));
|
|
||||||
|
|
||||||
if (!$data) {
|
|
||||||
echo json_encode(["success"=>false,"message"=>"JSON invalide","raw"=>$input]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérification des champs obligatoires
|
|
||||||
$required = ['DateDebut','DateFin','Repartition','NombreJours','Email','Nom'];
|
|
||||||
foreach ($required as $f) {
|
|
||||||
if (!array_key_exists($f, $data)) {
|
|
||||||
echo json_encode([
|
|
||||||
"success"=>false,
|
|
||||||
"message"=>"Donnée manquante : $f",
|
|
||||||
"debug"=>$data
|
|
||||||
]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$dateDebut = $data['DateDebut'];
|
|
||||||
$dateFin = $data['DateFin'];
|
|
||||||
$commentaire = $data['Commentaire'] ?? '';
|
|
||||||
$numDays = (float)$data['NombreJours'];
|
|
||||||
$userEmail = $data['Email'];
|
|
||||||
$userName = $data['Nom'];
|
|
||||||
$statut = 'En attente';
|
|
||||||
$currentDate = date('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
// 🔎 Identifier si c'est un CollaborateurAD ou un User
|
|
||||||
$stmt = $pdo->prepare("SELECT id FROM CollaborateurAD WHERE email = :email LIMIT 1");
|
|
||||||
$stmt->execute([':email'=>$userEmail]);
|
|
||||||
$collabAD = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$isAD = false;
|
|
||||||
$employeeId = null;
|
|
||||||
$collaborateurId = null;
|
|
||||||
|
|
||||||
if ($collabAD) {
|
|
||||||
$isAD = true;
|
|
||||||
$collaborateurId = (int)$collabAD['id'];
|
|
||||||
} else {
|
|
||||||
$stmt = $pdo->prepare("SELECT ID FROM Users WHERE Email = :email LIMIT 1");
|
|
||||||
$stmt->execute([':email'=>$userEmail]);
|
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$user) {
|
|
||||||
echo json_encode(["success"=>false,"message"=>"Aucun collaborateur trouvé pour $userEmail"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$employeeId = (int)$user['ID'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔎 Résoudre les IDs des types de congés
|
|
||||||
$typeIds = [];
|
|
||||||
foreach ($data['Repartition'] as $rep) {
|
|
||||||
$code = $rep['TypeConge'];
|
|
||||||
switch ($code) {
|
|
||||||
case 'CP': $name = 'Congé payé'; break;
|
|
||||||
case 'RTT': $name = 'RTT'; break;
|
|
||||||
case 'ABS': $name = 'Congé maladie'; break;
|
|
||||||
default: $name = $code; break;
|
|
||||||
}
|
|
||||||
$s = $pdo->prepare("SELECT Id FROM TypeConge WHERE Nom = :nom LIMIT 1");
|
|
||||||
$s->execute([':nom'=>$name]);
|
|
||||||
if ($r = $s->fetch(PDO::FETCH_ASSOC)) {
|
|
||||||
$typeIds[] = $r['Id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (empty($typeIds)) {
|
|
||||||
echo json_encode(["success"=>false,"message"=>"Aucun type de congé valide"]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$typeCongeIdCsv = implode(',', $typeIds);
|
|
||||||
|
|
||||||
// ✅ Insertion DemandeConge
|
|
||||||
$sql = "INSERT INTO DemandeConge
|
|
||||||
(EmployeeId, CollaborateurADId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours)
|
|
||||||
VALUES (:eid, :cid, :dd, :df, :tc, :st, :cd, :com, :val, :nj)";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':eid'=> $isAD ? 0 : $employeeId,
|
|
||||||
':cid'=> $isAD ? $collaborateurId : null,
|
|
||||||
':dd'=>$dateDebut,
|
|
||||||
':df'=>$dateFin,
|
|
||||||
':tc'=>$typeCongeIdCsv,
|
|
||||||
':st'=>$statut,
|
|
||||||
':cd'=>$currentDate,
|
|
||||||
':com'=>$commentaire,
|
|
||||||
':val'=>'',
|
|
||||||
':nj'=>$numDays
|
|
||||||
]);
|
|
||||||
|
|
||||||
$demandeId = $pdo->lastInsertId();
|
|
||||||
|
|
||||||
// ✅ Insertion DemandeCongeType
|
|
||||||
$sql = "INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours) VALUES (:did, :tid, :nj)";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
|
|
||||||
foreach ($data['Repartition'] as $rep) {
|
|
||||||
$jours = (float)$rep['NombreJours'];
|
|
||||||
$code = $rep['TypeConge'];
|
|
||||||
switch ($code) {
|
|
||||||
case 'CP': $name = 'Congé payé'; break;
|
|
||||||
case 'RTT': $name = 'RTT'; break;
|
|
||||||
case 'ABS': $name = 'Congé maladie'; break;
|
|
||||||
default: $name = $code; break;
|
|
||||||
}
|
|
||||||
$s = $pdo->prepare("SELECT Id FROM TypeConge WHERE Nom = :nom LIMIT 1");
|
|
||||||
$s->execute([':nom'=>$name]);
|
|
||||||
if ($r = $s->fetch(PDO::FETCH_ASSOC)) {
|
|
||||||
$stmt->execute([
|
|
||||||
':did'=>$demandeId,
|
|
||||||
':tid'=>$r['Id'],
|
|
||||||
':nj'=>$jours
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Récupérer les validateurs selon hiérarchie
|
|
||||||
if ($isAD) {
|
|
||||||
$stmt = $pdo->prepare("
|
|
||||||
SELECT c.email
|
|
||||||
FROM HierarchieValidationAD hv
|
|
||||||
JOIN CollaborateurAD c ON hv.SuperieurId = c.id
|
|
||||||
WHERE hv.CollaborateurId = :id
|
|
||||||
");
|
|
||||||
$stmt->execute([':id'=>$collaborateurId]);
|
|
||||||
} else {
|
|
||||||
$stmt = $pdo->prepare("
|
|
||||||
SELECT u.Email
|
|
||||||
FROM HierarchieValidation hv
|
|
||||||
JOIN Users u ON hv.SuperieurId = u.ID
|
|
||||||
WHERE hv.EmployeId = :id
|
|
||||||
");
|
|
||||||
$stmt->execute([':id'=>$employeeId]);
|
|
||||||
}
|
|
||||||
$managers = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
|
|
||||||
# =============================================================
|
|
||||||
# 📧 AUTH Microsoft Graph (client_credentials)
|
|
||||||
# =============================================================
|
|
||||||
$tenantId = "9840a2a0-6ae1-4688-b03d-d2ec291be0f9";
|
|
||||||
$clientId = "4bb4cc24-bac3-427c-b02c-5d14fc67b561";
|
|
||||||
$clientSecret = "gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t";
|
|
||||||
|
|
||||||
$url = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token";
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
"client_id" => $clientId,
|
|
||||||
"scope" => "https://graph.microsoft.com/.default",
|
|
||||||
"client_secret" => $clientSecret,
|
|
||||||
"grant_type" => "client_credentials"
|
|
||||||
];
|
|
||||||
|
|
||||||
$ch = curl_init($url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
||||||
"Content-Type: application/x-www-form-urlencoded"
|
|
||||||
]);
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$tokenData = json_decode($response, true);
|
|
||||||
if (!isset($tokenData['access_token'])) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Impossible de générer un token Graph", "debug"=>$tokenData]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$accessToken = $tokenData['access_token'];
|
|
||||||
|
|
||||||
# =============================================================
|
|
||||||
# 📧 Fonction envoi mail
|
|
||||||
# =============================================================
|
|
||||||
function sendMailGraph($accessToken, $fromEmail, $toEmail, $subject, $bodyHtml) {
|
|
||||||
$url = "https://graph.microsoft.com/v1.0/users/$fromEmail/sendMail";
|
|
||||||
|
|
||||||
$mailData = [
|
|
||||||
"message" => [
|
|
||||||
"subject" => $subject,
|
|
||||||
"body" => [
|
|
||||||
"contentType" => "HTML",
|
|
||||||
"content" => $bodyHtml
|
|
||||||
],
|
|
||||||
"toRecipients" => [
|
|
||||||
["emailAddress" => ["address" => $toEmail]]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"saveToSentItems" => "false"
|
|
||||||
];
|
|
||||||
|
|
||||||
$ch = curl_init($url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
||||||
"Authorization: Bearer $accessToken",
|
|
||||||
"Content-Type: application/json"
|
|
||||||
]);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($mailData));
|
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($httpCode >= 200 && $httpCode < 300) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
error_log("❌ Erreur envoi mail: $response");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================
|
|
||||||
# 📧 Envoi automatique des emails
|
|
||||||
# =============================================================
|
|
||||||
$fromEmail = "noreply@ensup.eu";
|
|
||||||
|
|
||||||
# Mail au collaborateur
|
|
||||||
sendMailGraph(
|
|
||||||
$accessToken,
|
|
||||||
$fromEmail,
|
|
||||||
$userEmail,
|
|
||||||
"Confirmation de votre demande de congés",
|
|
||||||
"
|
|
||||||
Bonjour {$userName},<br/><br/>
|
|
||||||
Votre demande du <b>{$dateDebut}</b> au <b>{$dateFin}</b>
|
|
||||||
({$numDays} jour(s)) a bien été enregistrée.<br/>
|
|
||||||
Elle est en attente de validation par votre manager.<br/><br/>
|
|
||||||
Merci.
|
|
||||||
"
|
|
||||||
);
|
|
||||||
|
|
||||||
# Mail aux managers
|
|
||||||
foreach ($managers as $managerEmail) {
|
|
||||||
sendMailGraph(
|
|
||||||
$accessToken,
|
|
||||||
$fromEmail,
|
|
||||||
$managerEmail,
|
|
||||||
"Nouvelle demande de congé - {$userName}",
|
|
||||||
"
|
|
||||||
Bonjour,<br/><br/>
|
|
||||||
{$userName} a soumis une demande de congé :<br/>
|
|
||||||
- Du <b>{$dateDebut}</b> au <b>{$dateFin}</b> ({$numDays} jour(s))<br/>
|
|
||||||
- Commentaire : " . (!empty($commentaire) ? $commentaire : "Aucun") . "<br/><br/>
|
|
||||||
Merci de valider cette demande.
|
|
||||||
"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
# ✅ Réponse finale
|
|
||||||
echo json_encode([
|
|
||||||
"success"=>true,
|
|
||||||
"message"=>"Demande soumise",
|
|
||||||
"request_id"=>$demandeId,
|
|
||||||
"managers"=>$managers
|
|
||||||
]);
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Validation/Refus d'une demande de congé par un manager
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
|
||||||
header("Access-Control-Allow-Headers: Content-Type");
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
|
||||||
http_response_code(200);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
// Connexion DB
|
|
||||||
$host = "192.168.0.4";
|
|
||||||
$dbname = "DemandeConge";
|
|
||||||
$username = "wpuser";
|
|
||||||
$password = "-2b/)ru5/Bi8P[7_";
|
|
||||||
$conn = new mysqli($host, $username, $password, $dbname);
|
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lecture du JSON envoyé
|
|
||||||
$input = file_get_contents('php://input');
|
|
||||||
$data = json_decode($input, true);
|
|
||||||
|
|
||||||
if (!isset($data['request_id'], $data['action'], $data['validator_id'])) {
|
|
||||||
echo json_encode(["success" => false, "message" => "Données manquantes"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$requestId = (int)$data['request_id'];
|
|
||||||
$action = $data['action']; // "approve" | "reject"
|
|
||||||
$validatorId = (int)$data['validator_id'];
|
|
||||||
$comment = $data['comment'] ?? '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$conn->begin_transaction();
|
|
||||||
|
|
||||||
// Vérifier que le validateur existe dans CollaborateurAD
|
|
||||||
$stmt = $conn->prepare("SELECT Id, prenom, nom FROM CollaborateurAD WHERE Id = ?");
|
|
||||||
$stmt->bind_param("i", $validatorId);
|
|
||||||
$stmt->execute();
|
|
||||||
$validator = $stmt->get_result()->fetch_assoc();
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
if (!$validator) {
|
|
||||||
throw new Exception("Validateur introuvable dans CollaborateurAD");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupération de la demande
|
|
||||||
$queryCheck = "
|
|
||||||
SELECT dc.Id, dc.CollaborateurADId, dc.TypeCongeId, dc.DateDebut, dc.DateFin, dc.NombreJours,
|
|
||||||
ca.prenom as CADPrenom, ca.nom as CADNom,
|
|
||||||
tc.Nom as TypeNom
|
|
||||||
FROM DemandeConge dc
|
|
||||||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
|
||||||
LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id
|
|
||||||
WHERE dc.Id = ? AND dc.Statut = 'En attente'
|
|
||||||
";
|
|
||||||
$stmtCheck = $conn->prepare($queryCheck);
|
|
||||||
$stmtCheck->bind_param("i", $requestId);
|
|
||||||
$stmtCheck->execute();
|
|
||||||
$requestRow = $stmtCheck->get_result()->fetch_assoc();
|
|
||||||
$stmtCheck->close();
|
|
||||||
|
|
||||||
if (!$requestRow) {
|
|
||||||
throw new Exception("Demande non trouvée ou déjà traitée");
|
|
||||||
}
|
|
||||||
|
|
||||||
$collaborateurId = $requestRow['CollaborateurADId'];
|
|
||||||
$typeCongeId = $requestRow['TypeCongeId'];
|
|
||||||
$nombreJours = $requestRow['NombreJours'];
|
|
||||||
$employeeName = $requestRow['CADPrenom']." ".$requestRow['CADNom'];
|
|
||||||
$typeNom = $requestRow['TypeNom'];
|
|
||||||
|
|
||||||
$newStatus = ($action === 'approve') ? 'Validée' : 'Refusée';
|
|
||||||
|
|
||||||
// 🔹 Mise à jour DemandeConge
|
|
||||||
$queryUpdate = "
|
|
||||||
UPDATE DemandeConge
|
|
||||||
SET Statut = ?,
|
|
||||||
ValidateurId = ?,
|
|
||||||
ValidateurADId = ?,
|
|
||||||
DateValidation = NOW(),
|
|
||||||
CommentaireValidation = ?
|
|
||||||
WHERE Id = ?
|
|
||||||
";
|
|
||||||
$stmtUpdate = $conn->prepare($queryUpdate);
|
|
||||||
$stmtUpdate->bind_param("siisi", $newStatus, $validatorId, $validatorId, $comment, $requestId);
|
|
||||||
$stmtUpdate->execute();
|
|
||||||
$stmtUpdate->close();
|
|
||||||
|
|
||||||
// 🔹 Déduction solde (pas maladie)
|
|
||||||
if ($action === 'approve' && $typeNom !== 'Congé maladie' && $collaborateurId) {
|
|
||||||
$year = date("Y");
|
|
||||||
$queryDeduct = "
|
|
||||||
UPDATE CompteurConges
|
|
||||||
SET Solde = GREATEST(0, Solde - ?)
|
|
||||||
WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
|
|
||||||
";
|
|
||||||
$stmtDeduct = $conn->prepare($queryDeduct);
|
|
||||||
$stmtDeduct->bind_param("diii", $nombreJours, $collaborateurId, $typeCongeId, $year);
|
|
||||||
$stmtDeduct->execute();
|
|
||||||
$stmtDeduct->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔹 Notification
|
|
||||||
$notificationTitle = ($action === 'approve') ? 'Demande approuvée' : 'Demande refusée';
|
|
||||||
$notificationMessage = "Votre demande de $typeNom a été " . (($action === 'approve') ? "approuvée" : "refusée");
|
|
||||||
if ($comment) $notificationMessage .= " (Commentaire: $comment)";
|
|
||||||
$notifType = ($action === 'approve') ? 'Success' : 'Error';
|
|
||||||
|
|
||||||
$queryNotif = "
|
|
||||||
INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
";
|
|
||||||
$stmtNotif = $conn->prepare($queryNotif);
|
|
||||||
$stmtNotif->bind_param("isssi", $collaborateurId, $notificationTitle, $notificationMessage, $notifType, $requestId);
|
|
||||||
$stmtNotif->execute();
|
|
||||||
$stmtNotif->close();
|
|
||||||
|
|
||||||
// 🔹 Historique
|
|
||||||
$actionText = ($action === 'approve') ? 'Validation congé' : 'Refus congé';
|
|
||||||
$actionDetails = "$actionText $employeeName ($typeNom)";
|
|
||||||
if ($comment) $actionDetails .= " - $comment";
|
|
||||||
|
|
||||||
$queryHistory = "
|
|
||||||
INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DemandeCongeId)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
";
|
|
||||||
$stmtHistory = $conn->prepare($queryHistory);
|
|
||||||
$stmtHistory->bind_param("issi", $validatorId, $actionText, $actionDetails, $requestId);
|
|
||||||
$stmtHistory->execute();
|
|
||||||
$stmtHistory->close();
|
|
||||||
|
|
||||||
$conn->commit();
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"success" => true,
|
|
||||||
"message" => "Demande " . (($action === 'approve') ? 'approuvée' : 'refusée'),
|
|
||||||
"new_status" => $newStatus
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$conn->rollback();
|
|
||||||
echo json_encode(["success" => false, "message" => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { AuthProvider } from './context/AuthContext';
|
import { AuthProvider, useAuth } from './context/AuthContext'; // ⭐ Ajout de useAuth
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Requests from './pages/Requests';
|
import Requests from './pages/Requests';
|
||||||
@@ -9,87 +9,103 @@ import Manager from './pages/Manager';
|
|||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
import EmployeeDetails from './pages/EmployeeDetails';
|
import EmployeeDetails from './pages/EmployeeDetails';
|
||||||
import Collaborateur from './pages/Collaborateur';
|
import Collaborateur from './pages/Collaborateur';
|
||||||
import CompteRenduActivites from './pages/CompteRenduActivite'; // ⭐ Ajout
|
import CompteRenduActivites from './pages/CompteRenduActivite';
|
||||||
|
import GlobalTutorial from './components/GlobalTutorial';
|
||||||
|
|
||||||
|
// ⭐ Créer un composant séparé pour utiliser useAuth
|
||||||
|
function AppContent() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const userId = user?.id || user?.CollaborateurADId || user?.ID;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* ⭐ Tutoriel global - Il s'affichera sur toutes les pages */}
|
||||||
|
<GlobalTutorial userId={userId} />
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
{/* Route publique */}
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
|
||||||
|
{/* Routes protégées */}
|
||||||
|
<Route
|
||||||
|
path="/dashboard"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Dashboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/demandes"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'RH', 'Admin']}>
|
||||||
|
<Requests />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/calendrier"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||||
|
<Calendar />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/manager"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||||
|
<Manager />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/collaborateur"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti']}>
|
||||||
|
<Collaborateur />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/employee/:id"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['RH', 'Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'Admin', 'President']}>
|
||||||
|
<EmployeeDetails />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ⭐ Nouvelle route pour Compte-Rendu d'Activités */}
|
||||||
|
<Route
|
||||||
|
path="/compte-rendu-activites"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||||
|
<CompteRenduActivites />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Redirection par défaut */}
|
||||||
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
|
||||||
|
{/* Route 404 - Redirection vers dashboard */}
|
||||||
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<AppContent />
|
||||||
{/* Route publique */}
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
|
|
||||||
{/* Routes protégées */}
|
|
||||||
<Route
|
|
||||||
path="/dashboard"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<Dashboard />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/demandes"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'RH', 'Admin']}>
|
|
||||||
<Requests />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/calendrier"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
|
||||||
<Calendar />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/manager"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute allowedRoles={['Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
|
||||||
<Manager />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/collaborateur"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti']}>
|
|
||||||
<Collaborateur />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/employee/:id"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute allowedRoles={['RH', 'Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'Admin', 'President']}>
|
|
||||||
<EmployeeDetails />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* ⭐ Nouvelle route pour Compte-Rendu d'Activités */}
|
|
||||||
<Route
|
|
||||||
path="/compte-rendu-activites"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute allowedRoles={['Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
|
||||||
<CompteRenduActivites />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Redirection par défaut */}
|
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
||||||
|
|
||||||
{/* Route 404 - Redirection vers dashboard */}
|
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
</Router>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,47 @@
|
|||||||
// authConfig.js
|
// authConfig.js
|
||||||
|
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
|
||||||
|
// Détection environnements (utile pour le debug)
|
||||||
|
const isProduction = hostname === "mygta.ensup-adm.net";
|
||||||
|
|
||||||
|
// --- API URL ---
|
||||||
|
// On utilise TOUJOURS /api car le proxy Vite (port 80) va rediriger vers le backend (port 3000)
|
||||||
|
// Cela évite les problèmes CORS et les problèmes de ports fermés (8000)
|
||||||
|
export const API_BASE_URL = "/api";
|
||||||
|
|
||||||
|
// --- MSAL Config ---
|
||||||
export const msalConfig = {
|
export const msalConfig = {
|
||||||
auth: {
|
auth: {
|
||||||
clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561", // Application (client) ID dans Azure
|
clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561",
|
||||||
authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9", // Directory (tenant) ID
|
authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9",
|
||||||
redirectUri: "http://localhost:5173"
|
|
||||||
|
// En prod, on force l'URL sans slash final pour être propre
|
||||||
|
redirectUri: isProduction
|
||||||
|
? "https://mygta.ensup-adm.net"
|
||||||
|
: `${protocol}//${hostname}`,
|
||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
cacheLocation: "sessionStorage",
|
cacheLocation: "sessionStorage",
|
||||||
storeAuthStateInCookie: false,
|
storeAuthStateInCookie: false,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Permissions Graph ---
|
||||||
export const loginRequest = {
|
export const loginRequest = {
|
||||||
scopes: [
|
scopes: [
|
||||||
"User.Read",
|
"User.Read",
|
||||||
"User.Read.All", // Pour lire les profils des autres utilisateurs
|
"User.Read.All",
|
||||||
"Group.Read.All", // Pour lire les groupes
|
"Group.Read.All",
|
||||||
"GroupMember.Read.All", // Pour lire les membres des groupes
|
"GroupMember.Read.All",
|
||||||
"Mail.Send" //Envoyer les emails.
|
"Mail.Send",
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("🔧 Config Auth:", {
|
||||||
|
hostname,
|
||||||
|
protocol,
|
||||||
|
API_BASE_URL,
|
||||||
|
redirectUri: msalConfig.auth.redirectUri,
|
||||||
|
});
|
||||||
|
|||||||
@@ -178,31 +178,45 @@ const EditLeaveRequestModal = ({
|
|||||||
try {
|
try {
|
||||||
const formDataToSend = new FormData();
|
const formDataToSend = new FormData();
|
||||||
|
|
||||||
formDataToSend.append('requestId', request.id);
|
// ⭐ Ajouter tous les champs texte AVANT les fichiers
|
||||||
formDataToSend.append('leaveType', parseInt(leaveType));
|
formDataToSend.append('requestId', request.id.toString());
|
||||||
|
formDataToSend.append('leaveType', leaveType.toString());
|
||||||
formDataToSend.append('startDate', startDate);
|
formDataToSend.append('startDate', startDate);
|
||||||
formDataToSend.append('endDate', endDate);
|
formDataToSend.append('endDate', endDate);
|
||||||
formDataToSend.append('reason', reason);
|
formDataToSend.append('reason', reason || '');
|
||||||
formDataToSend.append('userId', userId);
|
formDataToSend.append('userId', userId.toString());
|
||||||
formDataToSend.append('userEmail', userEmail);
|
formDataToSend.append('userEmail', userEmail);
|
||||||
formDataToSend.append('userName', userName);
|
formDataToSend.append('userName', userName);
|
||||||
formDataToSend.append('accessToken', accessToken);
|
formDataToSend.append('accessToken', accessToken || '');
|
||||||
|
|
||||||
// ⭐ Calcul des jours selon le type
|
// ⭐ Calcul des jours selon le type
|
||||||
const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType));
|
const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType));
|
||||||
const daysToSend = selectedType?.key === 'Récup' ? saturdayCount : businessDays;
|
const daysToSend = selectedType?.key === 'Récup' ? saturdayCount : businessDays;
|
||||||
formDataToSend.append('businessDays', daysToSend);
|
formDataToSend.append('businessDays', daysToSend.toString());
|
||||||
|
|
||||||
// ⭐ Documents médicaux
|
// ⭐ Documents médicaux EN DERNIER
|
||||||
medicalDocuments.forEach((file) => {
|
if (medicalDocuments.length > 0) {
|
||||||
formDataToSend.append('medicalDocuments', file);
|
medicalDocuments.forEach((file) => {
|
||||||
});
|
formDataToSend.append('medicalDocuments', file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('http://localhost:3000/updateRequest', {
|
// ⭐ DEBUG : Vérifier le contenu
|
||||||
|
console.log('📤 FormData à envoyer:');
|
||||||
|
for (let pair of formDataToSend.entries()) {
|
||||||
|
console.log(pair[0], ':', pair[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/updateRequest', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
// ⭐ NE PAS mettre de Content-Type, le navigateur le fera automatiquement avec boundary
|
||||||
body: formDataToSend
|
body: formDataToSend
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -222,7 +236,7 @@ const EditLeaveRequestModal = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur:', error);
|
console.error('❌ Erreur:', error);
|
||||||
setSubmitMessage({
|
setSubmitMessage({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: '❌ Une erreur est survenue. Veuillez réessayer.'
|
text: '❌ Une erreur est survenue. Veuillez réessayer.'
|
||||||
|
|||||||
728
project/src/components/GlobalTutorial.jsx
Normal file
728
project/src/components/GlobalTutorial.jsx
Normal file
@@ -0,0 +1,728 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Joyride, { STATUS } from 'react-joyride';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
const GlobalTutorial = ({ userId, userRole }) => {
|
||||||
|
const [runTour, setRunTour] = useState(false);
|
||||||
|
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||||
|
const [availableSteps, setAvailableSteps] = useState([]);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isEmployee = userRole === "Collaborateur" || userRole === "Apprenti";
|
||||||
|
const canViewAllFilters = ['president', 'rh', 'admin', 'directeur de campus', 'directrice de campus'].includes(userRole?.toLowerCase());
|
||||||
|
|
||||||
|
// 🎯 NOUVELLE FONCTION : Vérifier si un élément existe dans le DOM
|
||||||
|
const elementExists = (selector) => {
|
||||||
|
return document.querySelector(selector) !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🎯 NOUVELLE FONCTION : Filtrer les étapes selon les éléments disponibles
|
||||||
|
const filterAvailableSteps = (steps) => {
|
||||||
|
return steps.filter(step => {
|
||||||
|
// Les étapes centrées (body) sont toujours affichées
|
||||||
|
if (step.target === 'body') return true;
|
||||||
|
|
||||||
|
// Pour les autres, vérifier si l'élément existe
|
||||||
|
const element = document.querySelector(step.target);
|
||||||
|
if (!element) {
|
||||||
|
console.log(`⚠️ Élément non trouvé, étape ignorée: ${step.target}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'élément est visible
|
||||||
|
const isVisible = element.offsetParent !== null;
|
||||||
|
if (!isVisible) {
|
||||||
|
console.log(`⚠️ Élément caché, étape ignorée: ${step.target}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🎯 Déclencher le tutoriel avec vérification
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId) {
|
||||||
|
let tutorialKey = '';
|
||||||
|
|
||||||
|
if (location.pathname === '/dashboard') {
|
||||||
|
tutorialKey = 'dashboard';
|
||||||
|
} else if (location.pathname === '/manager') {
|
||||||
|
tutorialKey = 'manager';
|
||||||
|
} else if (location.pathname === '/calendar') {
|
||||||
|
tutorialKey = 'calendar';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialKey) {
|
||||||
|
const hasSeenTutorial = localStorage.getItem(`${tutorialKey}-tutorial-completed-${userId}`);
|
||||||
|
|
||||||
|
if (!hasSeenTutorial) {
|
||||||
|
// ⭐ NOUVEAU : Attendre que le DOM soit chargé
|
||||||
|
setTimeout(() => {
|
||||||
|
const allSteps = getTourSteps();
|
||||||
|
const available = filterAvailableSteps(allSteps);
|
||||||
|
|
||||||
|
console.log(`📊 Étapes totales: ${allSteps.length}, disponibles: ${available.length}`);
|
||||||
|
|
||||||
|
if (available.length > 2) { // Au moins 3 étapes (intro + 1 élément + conclusion)
|
||||||
|
setAvailableSteps(available);
|
||||||
|
setRunTour(true);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ Pas assez d\'éléments pour le tutoriel, annulation');
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [userId, location.pathname]);
|
||||||
|
|
||||||
|
// 🎯 Obtenir les étapes selon la page actuelle
|
||||||
|
const getTourSteps = () => {
|
||||||
|
// ==================== DASHBOARD ====================
|
||||||
|
if (location.pathname === '/dashboard') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-2">👋 Bienvenue sur votre application GTA !</h2>
|
||||||
|
<p>Découvrez toutes les fonctionnalités en quelques étapes. Ce tutoriel ne s'affichera qu'une seule fois.</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
disableBeacon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="dashboard"]',
|
||||||
|
content: '🏠 Accédez à votre tableau de bord pour voir vos soldes de congés.',
|
||||||
|
placement: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="demandes"]',
|
||||||
|
content: '📋 Consultez et gérez toutes vos demandes de congés ici.',
|
||||||
|
placement: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="calendrier"]',
|
||||||
|
content: '📅 Visualisez vos congés et ceux de votre équipe dans le calendrier.',
|
||||||
|
placement: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="mon-equipe"]',
|
||||||
|
content: '👥 Consultez votre équipe et leurs absences.',
|
||||||
|
placement: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="nouvelle-demande"]',
|
||||||
|
content: '➕ Cliquez ici pour créer une nouvelle demande de congé, RTT ou récupération.',
|
||||||
|
placement: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="notifications"]',
|
||||||
|
content: '🔔 Consultez ici vos notifications (validations, refus, modifications de vos demandes).',
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="refresh"]',
|
||||||
|
content: '🔄 Rafraîchissez manuellement vos données. Mais pas d\'inquiétude : elles se mettent à jour automatiquement en temps réel !',
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="demandes-recentes"]',
|
||||||
|
content: '📄 Consultez rapidement vos 5 dernières demandes et leur statut. Cliquez sur "Voir toutes les demandes" pour accéder à la page complète.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="conges-service"]',
|
||||||
|
content: '👥 Visualisez les congés de votre service pour le mois en cours. Pratique pour planifier vos absences !',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold mb-2">📊 Vos compteurs de congés</h2>
|
||||||
|
<p>Découvrez maintenant vos différents soldes de congés disponibles.</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="cp-n-1"]',
|
||||||
|
content: '📅 Vos congés payés de l\'année précédente. ⚠️ Attention : ils doivent être soldés avant le 31 décembre de l\'année en cours !',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="cp-n"]',
|
||||||
|
content: '📈 Vos congés payés de l\'année en cours, en cours d\'acquisition. Ils se cumulent au fil des mois travaillés.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="rtt"]',
|
||||||
|
content: '⏰ Vos RTT disponibles pour l\'année en cours. Ils sont acquis progressivement et à consommer avant le 31/12.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="recup"]',
|
||||||
|
content: '🔄 Vos jours de récupération accumulés suite à des heures supplémentaires.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-2">🎉 Vous êtes prêt !</h2>
|
||||||
|
<p className="mb-3">Vous pouvez maintenant utiliser l'application en toute autonomie.</p>
|
||||||
|
<div className="bg-cyan-50 border border-cyan-200 rounded-lg p-3 mt-3">
|
||||||
|
<p className="text-sm text-cyan-900">
|
||||||
|
💡 <strong>Besoin d'aide ?</strong> Cliquez sur le bouton <strong>"Aide"</strong> 🆘 en bas à droite pour relancer ce tutoriel à tout moment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MANAGER ====================
|
||||||
|
if (location.pathname === '/manager') {
|
||||||
|
const baseSteps = [
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-2">👥 Bienvenue dans la gestion d'équipe !</h2>
|
||||||
|
<p>Découvrez comment gérer {isEmployee ? 'votre équipe' : 'les demandes de congés de votre équipe'}.</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
disableBeacon: true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isEmployee) {
|
||||||
|
// Pour les managers/validateurs
|
||||||
|
return [
|
||||||
|
...baseSteps,
|
||||||
|
{
|
||||||
|
target: '[data-tour="demandes-attente"]',
|
||||||
|
content: '⏳ Consultez ici toutes les demandes en attente de validation. Vous pouvez les approuver ou les refuser directement.',
|
||||||
|
placement: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="approuver-btn"]',
|
||||||
|
content: '✅ Cliquez sur "Approuver" pour valider une demande. Vous pourrez ajouter un commentaire optionnel.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="refuser-btn"]',
|
||||||
|
content: '❌ Cliquez sur "Refuser" pour rejeter une demande. Un commentaire expliquant le motif sera obligatoire.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="mon-equipe"]',
|
||||||
|
content: '👥 Consultez la liste complète de votre équipe. Cliquez sur un membre pour voir le détail de ses demandes.',
|
||||||
|
placement: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="historique-demandes"]',
|
||||||
|
content: '📋 L\'historique complet de toutes les demandes de votre équipe avec leur statut (validée, refusée, en attente).',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="document-joint"]',
|
||||||
|
content: '📎 Si un document est joint à une demande (certificat médical par exemple), vous pouvez le consulter ici.',
|
||||||
|
placement: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-2">🎉 Vous êtes prêt à gérer votre équipe !</h2>
|
||||||
|
<p className="mb-3">Vous savez maintenant valider les demandes et suivre les absences de vos collaborateurs.</p>
|
||||||
|
<div className="bg-cyan-50 border border-cyan-200 rounded-lg p-3 mt-3">
|
||||||
|
<p className="text-sm text-cyan-900">
|
||||||
|
💡 <strong>Astuce :</strong> Les données se mettent à jour automatiquement en temps réel. Vous recevrez des notifications pour chaque nouvelle demande.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Pour les collaborateurs/apprentis
|
||||||
|
return [
|
||||||
|
...baseSteps,
|
||||||
|
{
|
||||||
|
target: '[data-tour="mon-equipe"]',
|
||||||
|
content: '👥 Consultez ici la liste de votre équipe. Vous pouvez voir les membres de votre service.',
|
||||||
|
placement: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="membre-equipe"]',
|
||||||
|
content: '👤 Cliquez sur un membre pour voir le détail de ses informations et absences.',
|
||||||
|
placement: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-2">✅ C'est tout pour cette section !</h2>
|
||||||
|
<p className="mb-3">Vous pouvez maintenant consulter votre équipe facilement.</p>
|
||||||
|
<div className="bg-cyan-50 border border-cyan-200 rounded-lg p-3 mt-3">
|
||||||
|
<p className="text-sm text-cyan-900">
|
||||||
|
💡 <strong>Besoin d'aide ?</strong> N'hésitez pas à contacter votre manager pour toute question.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CALENDAR ====================
|
||||||
|
if (location.pathname === '/calendar') {
|
||||||
|
const baseSteps = [
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-2">📅 Bienvenue dans le calendrier !</h2>
|
||||||
|
<p>Découvrez comment visualiser et gérer les congés {canViewAllFilters ? 'de toute l\'entreprise' : 'de votre équipe'}.</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
disableBeacon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="pto-counter"]',
|
||||||
|
content: '📊 Votre solde PTO (Paid Time Off) total : somme de vos CP N-1, CP N et RTT disponibles.',
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="navigation-mois"]',
|
||||||
|
content: '◀️▶️ Naviguez entre les mois pour consulter les congés passés et à venir.',
|
||||||
|
placement: 'bottom',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Étapes pour les filtres selon le rôle
|
||||||
|
if (canViewAllFilters) {
|
||||||
|
baseSteps.push(
|
||||||
|
{
|
||||||
|
target: '[data-tour="filtres-btn"]',
|
||||||
|
content: '🔍 Accédez aux filtres pour affiner votre vue : société, campus, service, collaborateurs...',
|
||||||
|
placement: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="filtre-societe"]',
|
||||||
|
content: '🏢 Filtrez par société pour voir uniquement les congés d\'une entité spécifique.',
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="filtre-campus"]',
|
||||||
|
content: '🏫 Filtrez par campus pour visualiser les absences par site géographique.',
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="filtre-service"]',
|
||||||
|
content: '👔 Filtrez par service pour voir les congés d\'un département spécifique.',
|
||||||
|
placement: 'bottom',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étapes communes pour tous
|
||||||
|
baseSteps.push(
|
||||||
|
{
|
||||||
|
target: '[data-tour="selection-collaborateurs"]',
|
||||||
|
content: '👥 Sélectionnez les collaborateurs que vous souhaitez afficher dans le calendrier. Pratique pour se concentrer sur certaines personnes !',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="refresh-btn"]',
|
||||||
|
content: '🔄 Rafraîchissez manuellement les données. Mais rassurez-vous : elles se mettent à jour automatiquement en temps réel via SSE !',
|
||||||
|
placement: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold mb-2">📅 Sélectionner des dates</h2>
|
||||||
|
<p>Vous pouvez sélectionner des dates directement dans le calendrier pour créer une demande de congé rapidement.</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="calendar-grid"]',
|
||||||
|
content: '🖱️ Cliquez sur une date de début, puis sur une date de fin pour sélectionner une période. Un menu contextuel apparaîtra pour choisir le type de congé.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="legende"]',
|
||||||
|
content: '🎨 La légende vous aide à identifier les différents types de congés : validés (vert), en attente (orange), formation (bleu), etc.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-2">🎉 Vous maîtrisez le calendrier !</h2>
|
||||||
|
<p className="mb-3">Vous savez maintenant visualiser les congés, filtrer par équipe et créer rapidement des demandes.</p>
|
||||||
|
<div className="bg-cyan-50 border border-cyan-200 rounded-lg p-3 mt-3">
|
||||||
|
<p className="text-sm text-cyan-900">
|
||||||
|
💡 <strong>Astuce :</strong> Survolez une case de congé pour voir tous les détails (employé, type, période, statut). Sur mobile, appuyez sur la case !
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
placement: 'center',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return baseSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🎯 Obtenir la clé localStorage selon la page
|
||||||
|
const getTutorialKey = () => {
|
||||||
|
if (location.pathname === '/dashboard') return 'dashboard';
|
||||||
|
if (location.pathname === '/manager') return 'manager';
|
||||||
|
if (location.pathname === '/calendar') return 'calendar';
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🎯 Gérer la fin du tutoriel
|
||||||
|
const handleJoyrideCallback = (data) => {
|
||||||
|
const { status } = data;
|
||||||
|
const finishedStatuses = [STATUS.FINISHED, STATUS.SKIPPED];
|
||||||
|
|
||||||
|
if (finishedStatuses.includes(status)) {
|
||||||
|
setRunTour(false);
|
||||||
|
setDontShowAgain(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Si on n'a pas d'étapes disponibles, ne rien afficher
|
||||||
|
if (availableSteps.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Joyride
|
||||||
|
steps={availableSteps}
|
||||||
|
run={runTour}
|
||||||
|
continuous
|
||||||
|
showProgress={true}
|
||||||
|
showSkipButton={false}
|
||||||
|
scrollToFirstStep
|
||||||
|
scrollOffset={100}
|
||||||
|
callback={handleJoyrideCallback}
|
||||||
|
styles={{
|
||||||
|
options: {
|
||||||
|
primaryColor: '#0891b2',
|
||||||
|
zIndex: 10000,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
floaterProps={{
|
||||||
|
disableAnimation: true,
|
||||||
|
}}
|
||||||
|
locale={{
|
||||||
|
back: 'Retour',
|
||||||
|
close: 'Fermer',
|
||||||
|
last: 'Terminer',
|
||||||
|
next: 'Suivant',
|
||||||
|
skip: 'Passer'
|
||||||
|
}}
|
||||||
|
tooltipComponent={({
|
||||||
|
continuous,
|
||||||
|
index,
|
||||||
|
step,
|
||||||
|
backProps,
|
||||||
|
primaryProps,
|
||||||
|
skipProps,
|
||||||
|
closeProps,
|
||||||
|
tooltipProps,
|
||||||
|
size,
|
||||||
|
isLastStep
|
||||||
|
}) => {
|
||||||
|
const [showConfirmModal, setShowConfirmModal] = React.useState(false);
|
||||||
|
const tutorialKey = getTutorialKey();
|
||||||
|
|
||||||
|
const handleFinish = () => {
|
||||||
|
if (dontShowAgain) {
|
||||||
|
localStorage.setItem(`${tutorialKey}-tutorial-completed-${userId}`, 'true');
|
||||||
|
}
|
||||||
|
setRunTour(false);
|
||||||
|
setDontShowAgain(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkip = () => {
|
||||||
|
if (dontShowAgain) {
|
||||||
|
setShowConfirmModal(true);
|
||||||
|
} else {
|
||||||
|
setRunTour(false);
|
||||||
|
setDontShowAgain(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmSkip = () => {
|
||||||
|
localStorage.setItem(`${tutorialKey}-tutorial-completed-${userId}`, 'true');
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setRunTour(false);
|
||||||
|
setDontShowAgain(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelSkip = () => {
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setDontShowAgain(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Modal de confirmation */}
|
||||||
|
{showConfirmModal && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10001
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
cancelSkip();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '24px',
|
||||||
|
maxWidth: '400px',
|
||||||
|
width: '90%',
|
||||||
|
boxShadow: '0 20px 50px rgba(0,0,0,0.3)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '48px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
⚠️
|
||||||
|
</div>
|
||||||
|
<h3 style={{
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '12px',
|
||||||
|
color: '#111827',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
Ne plus afficher le tutoriel ?
|
||||||
|
</h3>
|
||||||
|
<p style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#6b7280',
|
||||||
|
marginBottom: '24px',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: '1.5'
|
||||||
|
}}>
|
||||||
|
Êtes-vous sûr de vouloir désactiver définitivement ce tutoriel ?
|
||||||
|
{tutorialKey === 'dashboard' && ' Vous pourrez le réactiver plus tard en cliquant sur le bouton "Aide".'}
|
||||||
|
</p>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '12px',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={cancelSkip}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
color: '#374151',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '500',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={confirmSkip}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: '#ef4444',
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '500',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Oui, ne plus afficher
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tooltip principal */}
|
||||||
|
<div {...tooltipProps} style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '400px',
|
||||||
|
boxShadow: '0 10px 25px rgba(0,0,0,0.15)',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}>
|
||||||
|
<div style={{ marginBottom: '15px', color: '#374151' }}>
|
||||||
|
{step.content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Case à cocher "Ne plus afficher" */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
marginTop: '12px',
|
||||||
|
marginBottom: '12px',
|
||||||
|
padding: '10px',
|
||||||
|
backgroundColor: '#f9fafb',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #e5e7eb'
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`dont-show-again-${index}`}
|
||||||
|
checked={dontShowAgain}
|
||||||
|
onChange={(e) => setDontShowAgain(e.target.checked)}
|
||||||
|
style={{
|
||||||
|
width: '18px',
|
||||||
|
height: '18px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
accentColor: '#0891b2'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`dont-show-again-${index}`}
|
||||||
|
style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#374151',
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ne plus afficher ce tutoriel
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: '12px',
|
||||||
|
borderTop: '1px solid #e5e7eb'
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: '13px', color: '#6b7280', fontWeight: '500' }}>
|
||||||
|
Étape {index + 1} sur {size}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
{index > 0 && (
|
||||||
|
<button
|
||||||
|
{...backProps}
|
||||||
|
style={{
|
||||||
|
padding: '8px 14px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
color: '#6b7280',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '500',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLastStep && (
|
||||||
|
<button
|
||||||
|
{...primaryProps}
|
||||||
|
style={{
|
||||||
|
padding: '8px 18px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: '#0891b2',
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '500',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Suivant
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLastStep && (
|
||||||
|
<button
|
||||||
|
onClick={handleFinish}
|
||||||
|
style={{
|
||||||
|
padding: '8px 18px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: '#0891b2',
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '500',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Terminer
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSkip}
|
||||||
|
style={{
|
||||||
|
padding: '8px 14px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
color: '#6b7280',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '500',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Passer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GlobalTutorial;
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -15,7 +15,7 @@ const MedicalDocuments = ({ demandeId }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(`http://localhost:3000/medical-documents/${demandeId}`);
|
const response = await fetch(`/medical-documents/${demandeId}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -116,7 +116,7 @@ const MedicalDocuments = ({ demandeId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href={`http://localhost:3000${doc.downloadUrl}`}
|
href={`${doc.downloadUrl}`}
|
||||||
download
|
download
|
||||||
className="flex-shrink-0 p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
className="flex-shrink-0 p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
title="Télécharger"
|
title="Télécharger"
|
||||||
|
|||||||
@@ -28,11 +28,52 @@ const NewLeaveRequestModal = ({
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const safeCounters = {
|
// ⭐ État pour les données des compteurs
|
||||||
availableCP: availableLeaveCounters?.availableCP ?? 0,
|
const [countersData, setCountersData] = useState(null);
|
||||||
availableRTT: availableLeaveCounters?.availableRTT ?? 0
|
const [isLoadingCounters, setIsLoadingCounters] = useState(true);
|
||||||
|
|
||||||
|
// ⭐ Charger les compteurs au montage
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCounters = async () => {
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
setIsLoadingCounters(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/getDetailedLeaveCounters?user_id=${userId}`
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
console.log('📊 Compteurs reçus:', data);
|
||||||
|
setCountersData(data);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Erreur compteurs:', data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur réseau compteurs:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingCounters(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCounters();
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
// ⭐ Utiliser les données des compteurs
|
||||||
|
const safeCounters = countersData ? {
|
||||||
|
availableCP: parseFloat(countersData.data?.totalDisponible?.cp || 0),
|
||||||
|
availableRTT: parseFloat(countersData.data?.totalDisponible?.rtt || 0),
|
||||||
|
availableRecup: parseFloat(countersData.data?.totalDisponible?.recup || 0)
|
||||||
|
} : {
|
||||||
|
availableCP: 0,
|
||||||
|
availableRTT: 0,
|
||||||
|
availableRecup: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('📊 Compteurs disponibles:', safeCounters);
|
||||||
|
console.log('📊 Données complètes:', countersData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (preselectedStartDate || preselectedEndDate) {
|
if (preselectedStartDate || preselectedEndDate) {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
@@ -44,7 +85,6 @@ const NewLeaveRequestModal = ({
|
|||||||
}
|
}
|
||||||
}, [preselectedStartDate, preselectedEndDate, preselectedType]);
|
}, [preselectedStartDate, preselectedEndDate, preselectedType]);
|
||||||
|
|
||||||
// 🔹 Calcul automatique - JOURS OUVRÉS UNIQUEMENT (Lun-Ven)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.startDate && formData.endDate) {
|
if (formData.startDate && formData.endDate) {
|
||||||
const start = new Date(formData.startDate);
|
const start = new Date(formData.startDate);
|
||||||
@@ -70,8 +110,7 @@ const NewLeaveRequestModal = ({
|
|||||||
debut: formData.startDate,
|
debut: formData.startDate,
|
||||||
fin: formData.endDate,
|
fin: formData.endDate,
|
||||||
joursOuvres: workingDays,
|
joursOuvres: workingDays,
|
||||||
samedis: saturdays,
|
samedis: saturdays
|
||||||
message: 'Les samedis ne sont comptés QUE si "Récup" est coché'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [formData.startDate, formData.endDate]);
|
}, [formData.startDate, formData.endDate]);
|
||||||
@@ -105,20 +144,15 @@ const NewLeaveRequestModal = ({
|
|||||||
const handlePeriodeChange = (type, periode) => {
|
const handlePeriodeChange = (type, periode) => {
|
||||||
setPeriodeSelection(prev => ({ ...prev, [type]: periode }));
|
setPeriodeSelection(prev => ({ ...prev, [type]: periode }));
|
||||||
|
|
||||||
// ⭐ CORRECTION : Ajuster automatiquement pour TOUTES les situations
|
|
||||||
if (formData.types.length === 1) {
|
if (formData.types.length === 1) {
|
||||||
// Si un seul type : ajuster selon la période
|
|
||||||
if (periode === 'Matin' || periode === 'Après-midi') {
|
if (periode === 'Matin' || periode === 'Après-midi') {
|
||||||
setRepartition(prev => ({ ...prev, [type]: 0.5 }));
|
setRepartition(prev => ({ ...prev, [type]: 0.5 }));
|
||||||
} else {
|
} else {
|
||||||
setRepartition(prev => ({ ...prev, [type]: totalDays }));
|
setRepartition(prev => ({ ...prev, [type]: totalDays }));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Si plusieurs types : garder la répartition manuelle
|
|
||||||
// MAIS si c'est une période partielle sur une journée unique
|
|
||||||
if (formData.startDate === formData.endDate && (periode === 'Matin' || periode === 'Après-midi')) {
|
if (formData.startDate === formData.endDate && (periode === 'Matin' || periode === 'Après-midi')) {
|
||||||
const currentRepartition = repartition[type] || 0;
|
const currentRepartition = repartition[type] || 0;
|
||||||
// Suggérer 0.5 si pas encore défini
|
|
||||||
if (currentRepartition === 0 || currentRepartition === 1) {
|
if (currentRepartition === 0 || currentRepartition === 1) {
|
||||||
setRepartition(prev => ({ ...prev, [type]: 0.5 }));
|
setRepartition(prev => ({ ...prev, [type]: 0.5 }));
|
||||||
}
|
}
|
||||||
@@ -126,7 +160,6 @@ const NewLeaveRequestModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleFileUpload = (e) => {
|
const handleFileUpload = (e) => {
|
||||||
const files = Array.from(e.target.files);
|
const files = Array.from(e.target.files);
|
||||||
const validFiles = [];
|
const validFiles = [];
|
||||||
@@ -170,6 +203,15 @@ const NewLeaveRequestModal = ({
|
|||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
console.log('\n🔍 === VALIDATION FORMULAIRE ===');
|
console.log('\n🔍 === VALIDATION FORMULAIRE ===');
|
||||||
|
|
||||||
|
// 🔥 AJOUTER CES LOGS
|
||||||
|
console.log('📊 countersData:', countersData);
|
||||||
|
console.log('📊 countersData.success:', countersData?.success);
|
||||||
|
console.log('📊 countersData.data:', countersData?.data);
|
||||||
|
console.log('📊 totalDisponible:', countersData?.data?.totalDisponible);
|
||||||
|
console.log('📊 totalDisponible.cp:', countersData?.data?.totalDisponible?.cp);
|
||||||
|
console.log('📊 safeCounters:', safeCounters);
|
||||||
|
|
||||||
|
// Vérifications de base
|
||||||
if (formData.types.length === 0) {
|
if (formData.types.length === 0) {
|
||||||
setError('Veuillez sélectionner au moins un type de congé');
|
setError('Veuillez sélectionner au moins un type de congé');
|
||||||
return false;
|
return false;
|
||||||
@@ -200,58 +242,82 @@ const NewLeaveRequestModal = ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasRecup = formData.types.includes('Récup');
|
|
||||||
const hasABS = formData.types.includes('ABS');
|
const hasABS = formData.types.includes('ABS');
|
||||||
|
|
||||||
// ⭐ NOUVEAU : Calculer le total attendu en tenant compte des demi-journées
|
if (hasABS && formData.types.length > 1) {
|
||||||
let expectedTotal = totalDays;
|
setError('Un arrêt maladie ne peut pas être mélangé avec d\'autres types de congés');
|
||||||
|
|
||||||
// Si un seul type avec demi-journée sur une journée unique
|
|
||||||
if (formData.types.length === 1 && formData.startDate === formData.endDate) {
|
|
||||||
const type = formData.types[0];
|
|
||||||
const periode = periodeSelection[type];
|
|
||||||
|
|
||||||
if ((type === 'CP' || type === 'RTT') && (periode === 'Matin' || periode === 'Après-midi')) {
|
|
||||||
expectedTotal = 0.5;
|
|
||||||
console.log('📊 Demi-journée détectée, expectedTotal = 0.5');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('📊 Analyse:', {
|
|
||||||
joursOuvres: totalDays,
|
|
||||||
expectedTotal: expectedTotal,
|
|
||||||
samedis: saturdayCount,
|
|
||||||
typesCoches: formData.types,
|
|
||||||
hasRecup
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasRecup && saturdayCount === 0) {
|
|
||||||
setError('Une récupération nécessite au moins un samedi dans la période. Veuillez sélectionner une période incluant un samedi ou décocher "Récupération (samedi)".');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saturdayCount > 0 && !hasRecup) {
|
|
||||||
console.log(`⚠️ ${saturdayCount} samedi(s) détecté(s) mais "Récup" non coché - Les samedis seront ignorés`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasABS && formData.medicalDocuments.length === 0) {
|
if (hasABS && formData.medicalDocuments.length === 0) {
|
||||||
setError('Un justificatif médical est obligatoire pour un arrêt maladie');
|
setError('Un justificatif médical est obligatoire pour un arrêt maladie');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ VALIDATION RÉPARTITION AMÉLIORÉE
|
// VALIDATION DES SOLDES AVEC ANTICIPATION
|
||||||
if (formData.types.length > 1) {
|
// 🔥 CONDITION MODIFIÉE : Vérifier que les données sont bien chargées
|
||||||
const sum = Object.values(repartition).reduce((a, b) => a + b, 0);
|
if (!countersData || !countersData.data || !countersData.data.totalDisponible) {
|
||||||
|
console.error('❌ Données compteurs non disponibles pour validation !');
|
||||||
|
setError('Erreur : Les compteurs ne sont pas chargés. Veuillez réessayer.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('📊 Validation répartition:', {
|
// Calculer les jours demandés par type
|
||||||
somme: sum,
|
const joursDemandesParType = {};
|
||||||
attendu: expectedTotal,
|
|
||||||
joursOuvres: totalDays,
|
if (formData.types.length === 1) {
|
||||||
samedisIgnores: !hasRecup ? saturdayCount : 0
|
const type = formData.types[0];
|
||||||
|
const periode = periodeSelection[type] || 'Journée entière';
|
||||||
|
|
||||||
|
if (formData.startDate === formData.endDate && (periode === 'Matin' || periode === 'Après-midi')) {
|
||||||
|
joursDemandesParType[type] = 0.5;
|
||||||
|
} else {
|
||||||
|
joursDemandesParType[type] = totalDays;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formData.types.forEach(type => {
|
||||||
|
joursDemandesParType[type] = repartition[type] || 0;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (Math.abs(sum - expectedTotal) > 0.01) {
|
console.log('📊 Jours demandés:', joursDemandesParType);
|
||||||
setError(`La somme des jours répartis (${sum.toFixed(1)}j) doit être égale au total de jours ouvrés (${expectedTotal}j). ${saturdayCount > 0 && !hasRecup ? `Les ${saturdayCount} samedi(s) ne sont pas comptés.` : ''}`);
|
console.log('📊 Soldes disponibles:', safeCounters);
|
||||||
|
|
||||||
|
// Vérifier CP
|
||||||
|
if (joursDemandesParType['CP'] > 0) {
|
||||||
|
const cpDemande = joursDemandesParType['CP'];
|
||||||
|
const cpDisponible = safeCounters.availableCP;
|
||||||
|
|
||||||
|
console.log(`🔍 CP: ${cpDemande}j demandés vs ${cpDisponible}j disponibles`);
|
||||||
|
|
||||||
|
if (cpDemande > cpDisponible) {
|
||||||
|
setError(`Solde CP insuffisant (${cpDisponible.toFixed(2)}j disponibles avec anticipation, ${cpDemande}j demandés)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier RTT
|
||||||
|
if (joursDemandesParType['RTT'] > 0) {
|
||||||
|
const rttDemande = joursDemandesParType['RTT'];
|
||||||
|
const rttDisponible = safeCounters.availableRTT;
|
||||||
|
|
||||||
|
console.log(`🔍 RTT: ${rttDemande}j demandés vs ${rttDisponible}j disponibles`);
|
||||||
|
|
||||||
|
if (rttDemande > rttDisponible) {
|
||||||
|
setError(`Solde RTT insuffisant (${rttDisponible.toFixed(2)}j disponibles avec anticipation, ${rttDemande}j demandés)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier Récup
|
||||||
|
if (joursDemandesParType['Récup'] > 0) {
|
||||||
|
const recupDemande = joursDemandesParType['Récup'];
|
||||||
|
const recupDisponible = safeCounters.availableRecup;
|
||||||
|
|
||||||
|
console.log(`🔍 Récup: ${recupDemande}j demandés vs ${recupDisponible}j disponibles`);
|
||||||
|
|
||||||
|
if (recupDemande > recupDisponible) {
|
||||||
|
setError(`Solde Récup insuffisant (${recupDisponible.toFixed(2)}j disponibles, ${recupDemande}j demandés)`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,6 +327,7 @@ const NewLeaveRequestModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
@@ -281,15 +348,13 @@ const NewLeaveRequestModal = ({
|
|||||||
formDataToSend.append('Nom', userName);
|
formDataToSend.append('Nom', userName);
|
||||||
formDataToSend.append('Commentaire', formData.reason || '');
|
formDataToSend.append('Commentaire', formData.reason || '');
|
||||||
|
|
||||||
// ⭐ CORRECTION : Calculer le NombreJours total correctement
|
|
||||||
let totalJoursToSend = totalDays;
|
let totalJoursToSend = totalDays;
|
||||||
|
|
||||||
// Si un seul type avec demi-journée
|
|
||||||
if (formData.types.length === 1 && formData.startDate === formData.endDate) {
|
if (formData.types.length === 1 && formData.startDate === formData.endDate) {
|
||||||
const type = formData.types[0];
|
const type = formData.types[0];
|
||||||
const periode = periodeSelection[type];
|
const periode = periodeSelection[type];
|
||||||
|
|
||||||
if ((type === 'CP' || type === 'RTT') && (periode === 'Matin' || periode === 'Après-midi')) {
|
if ((type === 'CP' || type === 'RTT' || type === 'Récup') && (periode === 'Matin' || periode === 'Après-midi')) {
|
||||||
totalJoursToSend = 0.5;
|
totalJoursToSend = 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,50 +362,31 @@ const NewLeaveRequestModal = ({
|
|||||||
formDataToSend.append('NombreJours', totalJoursToSend);
|
formDataToSend.append('NombreJours', totalJoursToSend);
|
||||||
|
|
||||||
const repartitionArray = formData.types.map(type => {
|
const repartitionArray = formData.types.map(type => {
|
||||||
if (type === 'Récup' && formData.types.length === 1) {
|
|
||||||
console.log(`📝 Récup seul: ${saturdayCount} samedi(s)`);
|
|
||||||
return {
|
|
||||||
TypeConge: type,
|
|
||||||
NombreJours: saturdayCount,
|
|
||||||
PeriodeJournee: 'Journée entière'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'Récup' && formData.types.length > 1) {
|
|
||||||
const joursRecup = repartition[type] || saturdayCount;
|
|
||||||
console.log(`📝 Récup (répartition): ${joursRecup}j`);
|
|
||||||
return {
|
|
||||||
TypeConge: type,
|
|
||||||
NombreJours: joursRecup,
|
|
||||||
PeriodeJournee: 'Journée entière'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ CORRECTION : Gérer demi-journées pour un seul type
|
|
||||||
let nombreJours;
|
let nombreJours;
|
||||||
|
let periodeJournee = 'Journée entière';
|
||||||
|
|
||||||
if (formData.types.length === 1) {
|
if (formData.types.length === 1) {
|
||||||
// Un seul type : utiliser soit 0.5 (demi-journée) soit totalDays
|
const periode = periodeSelection[type] || 'Journée entière';
|
||||||
const periode = periodeSelection[type];
|
|
||||||
if ((type === 'CP' || type === 'RTT') &&
|
if ((type === 'CP' || type === 'RTT' || type === 'Récup') &&
|
||||||
formData.startDate === formData.endDate &&
|
formData.startDate === formData.endDate &&
|
||||||
(periode === 'Matin' || periode === 'Après-midi')) {
|
(periode === 'Matin' || periode === 'Après-midi')) {
|
||||||
nombreJours = 0.5;
|
nombreJours = 0.5;
|
||||||
|
periodeJournee = periode;
|
||||||
} else {
|
} else {
|
||||||
nombreJours = totalDays;
|
nombreJours = totalDays;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Plusieurs types : utiliser la répartition manuelle
|
|
||||||
nombreJours = repartition[type] || 0;
|
nombreJours = repartition[type] || 0;
|
||||||
|
periodeJournee = periodeSelection[type] || 'Journée entière';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📝 ${type}: ${nombreJours}j (${periodeSelection[type] || 'Journée entière'})`);
|
console.log(`📝 ${type}: ${nombreJours}j (${periodeJournee})`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
TypeConge: type,
|
TypeConge: type,
|
||||||
NombreJours: nombreJours,
|
NombreJours: nombreJours,
|
||||||
PeriodeJournee: ['CP', 'RTT'].includes(type)
|
PeriodeJournee: ['CP', 'RTT', 'Récup'].includes(type) ? periodeJournee : 'Journée entière'
|
||||||
? (periodeSelection[type] || 'Journée entière')
|
|
||||||
: 'Journée entière'
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -353,7 +399,7 @@ const NewLeaveRequestModal = ({
|
|||||||
formDataToSend.append('medicalDocuments', file);
|
formDataToSend.append('medicalDocuments', file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch('http://localhost:3000/submitLeaveRequest', {
|
const response = await fetch('/submitLeaveRequest', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formDataToSend
|
body: formDataToSend
|
||||||
});
|
});
|
||||||
@@ -376,7 +422,6 @@ const NewLeaveRequestModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleTypeToggle = (type) => {
|
const handleTypeToggle = (type) => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -386,20 +431,74 @@ const NewLeaveRequestModal = ({
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isTypeDisabled = (typeKey) => {
|
||||||
|
const hasABS = formData.types.includes('ABS');
|
||||||
|
const hasOtherTypes = formData.types.some(t => t !== 'ABS');
|
||||||
|
|
||||||
|
if (hasABS && typeKey !== 'ABS') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOtherTypes && typeKey === 'ABS') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisabledTooltip = (typeKey) => {
|
||||||
|
if (formData.types.includes('ABS') && typeKey !== 'ABS') {
|
||||||
|
return '⚠️ Un arrêt maladie ne peut pas être mélangé avec d\'autres types';
|
||||||
|
}
|
||||||
|
if (formData.types.some(t => t !== 'ABS') && typeKey === 'ABS') {
|
||||||
|
return '⚠️ Un arrêt maladie ne peut pas être mélangé avec d\'autres types';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⭐ Inclure les détails des compteurs dans availableTypes
|
||||||
|
// ⭐ Inclure les détails des compteurs dans availableTypes
|
||||||
const availableTypes = userRole === 'Apprenti'
|
const availableTypes = userRole === 'Apprenti'
|
||||||
? [
|
? [
|
||||||
{ key: 'CP', label: 'Congés payés', available: safeCounters.availableCP },
|
{
|
||||||
|
key: 'CP',
|
||||||
|
label: 'Congé(s) payé(s)',
|
||||||
|
// ✅ Afficher seulement le solde actuel (sans anticipé)
|
||||||
|
available: countersData?.data?.cpN?.solde || 0,
|
||||||
|
details: countersData?.data?.cpN
|
||||||
|
},
|
||||||
{ key: 'ABS', label: 'Arrêt maladie' },
|
{ key: 'ABS', label: 'Arrêt maladie' },
|
||||||
{ key: 'Formation', label: 'Formation' },
|
{ key: 'Formation', label: 'Formation' },
|
||||||
{ key: 'Récup', label: 'Récupération (samedi)' },
|
{
|
||||||
|
key: 'Récup',
|
||||||
|
label: 'Récupération(s)',
|
||||||
|
available: countersData?.data?.recupN?.solde || 0
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ key: 'CP', label: 'Congés payés', available: safeCounters.availableCP },
|
{
|
||||||
{ key: 'RTT', label: 'RTT', available: safeCounters.availableRTT },
|
key: 'CP',
|
||||||
{ key: 'ABS', label: 'Arrêt maladie' },
|
label: 'Congé(s) payé(s)',
|
||||||
{ key: 'Récup', label: 'Récupération (samedi)' },
|
// ✅ Afficher seulement le solde actuel (sans anticipé)
|
||||||
|
available: countersData?.data?.cpN?.solde || 0,
|
||||||
|
details: countersData?.data?.cpN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'RTT',
|
||||||
|
label: 'RTT',
|
||||||
|
// ✅ Afficher seulement le solde actuel (sans anticipé)
|
||||||
|
available: countersData?.data?.rttN?.solde || 0,
|
||||||
|
details: countersData?.data?.rttN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Récup',
|
||||||
|
label: 'Récupération',
|
||||||
|
available: countersData?.data?.recupN?.solde || 0
|
||||||
|
},
|
||||||
|
{ key: 'ABS', label: 'Arrêt maladie' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
|
||||||
@@ -411,34 +510,75 @@ const NewLeaveRequestModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-5">
|
<div className="p-6 space-y-5">
|
||||||
|
{/* ⭐ BLOC SOLDES DÉTAILLÉS */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Loading */}
|
||||||
|
{isLoadingCounters && (
|
||||||
|
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 text-center">
|
||||||
|
<div className="animate-spin h-6 w-6 border-2 border-blue-600 border-t-transparent rounded-full mx-auto mb-2"></div>
|
||||||
|
<p className="text-sm text-gray-600">Chargement des soldes...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-900 mb-3">
|
<label className="block text-sm font-medium text-gray-900 mb-3">
|
||||||
Types de congé *
|
Types d'absences *
|
||||||
</label>
|
</label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{availableTypes.map(type => (
|
{availableTypes.map(type => {
|
||||||
<label key={type.key} className="flex items-center gap-3 cursor-pointer">
|
const disabled = isTypeDisabled(type.key);
|
||||||
<input
|
const tooltip = getDisabledTooltip(type.key);
|
||||||
type="checkbox"
|
|
||||||
checked={formData.types.includes(type.key)}
|
return (
|
||||||
onChange={() => handleTypeToggle(type.key)}
|
<label
|
||||||
className="w-4 h-4 rounded border-gray-300"
|
key={type.key}
|
||||||
/>
|
className={`flex items-center gap-3 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
<span className={`px-2.5 py-1 rounded text-sm font-medium ${type.key === 'CP' ? 'bg-blue-100 text-blue-800' :
|
title={tooltip}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.types.includes(type.key)}
|
||||||
|
onChange={() => handleTypeToggle(type.key)}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`w-4 h-4 rounded border-gray-300 ${disabled ? 'cursor-not-allowed' : ''}`}
|
||||||
|
/>
|
||||||
|
<span className={`px-2.5 py-1 rounded text-sm font-medium ${type.key === 'CP' ? 'bg-blue-100 text-blue-800' :
|
||||||
type.key === 'RTT' ? 'bg-green-100 text-green-800' :
|
type.key === 'RTT' ? 'bg-green-100 text-green-800' :
|
||||||
type.key === 'ABS' ? 'bg-red-100 text-red-800' :
|
type.key === 'ABS' ? 'bg-red-100 text-red-800' :
|
||||||
'bg-purple-100 text-purple-800'
|
type.key === 'Récup' ? 'bg-orange-100 text-orange-800' :
|
||||||
}`}>
|
'bg-purple-100 text-purple-800'
|
||||||
{type.label}
|
}`}>
|
||||||
</span>
|
{type.label}
|
||||||
{type.available !== undefined && (
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
({type.available.toFixed(2)} disponibles)
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
{type.available !== undefined && (
|
||||||
</label>
|
<div className="flex flex-col text-xs">
|
||||||
))}
|
<span className={`font-semibold ${type.details?.solde < 0 || type.details?.anticipe?.depassement > 0
|
||||||
|
? 'text-red-600'
|
||||||
|
: 'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
({type.available.toFixed(2)} disponibles)
|
||||||
|
</span>
|
||||||
|
{type.details?.anticipe?.depassement > 0 && (
|
||||||
|
<span className="text-red-600 text-xs italic">
|
||||||
|
⚠️ Dépassement anticipation
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{formData.types.includes('ABS') && (
|
||||||
|
<div className="mt-3 flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||||
|
<AlertCircle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-amber-700 text-xs">
|
||||||
|
Un arrêt maladie ne peut pas être combiné avec d'autres types de congés.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@@ -468,12 +608,7 @@ const NewLeaveRequestModal = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{formData.types.length === 1 && ['CP', 'RTT', 'Récup'].includes(formData.types[0]) && (
|
||||||
|
|
||||||
|
|
||||||
{/* ⭐ SECTION PÉRIODE POUR UN SEUL TYPE */}
|
|
||||||
{/* ⭐ SECTION PÉRIODE POUR UN SEUL TYPE */}
|
|
||||||
{formData.types.length === 1 && ['CP', 'RTT'].includes(formData.types[0]) && (
|
|
||||||
<div className="border-t border-gray-200 pt-4">
|
<div className="border-t border-gray-200 pt-4">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
Période de la journée
|
Période de la journée
|
||||||
@@ -496,7 +631,6 @@ const NewLeaveRequestModal = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🎯 AFFICHAGE DU NOMBRE DE JOURS */}
|
|
||||||
<div className="mt-3 flex items-center justify-center">
|
<div className="mt-3 flex items-center justify-center">
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-2 inline-flex items-center gap-2">
|
<div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-2 inline-flex items-center gap-2">
|
||||||
<span className="text-sm font-medium text-blue-900">Durée sélectionnée :</span>
|
<span className="text-sm font-medium text-blue-900">Durée sélectionnée :</span>
|
||||||
@@ -506,10 +640,8 @@ const NewLeaveRequestModal = ({
|
|||||||
const periode = periodeSelection[type] || 'Journée entière';
|
const periode = periodeSelection[type] || 'Journée entière';
|
||||||
|
|
||||||
if (formData.startDate === formData.endDate) {
|
if (formData.startDate === formData.endDate) {
|
||||||
// Journée unique
|
|
||||||
return (periode === 'Matin' || periode === 'Après-midi') ? '0.5 jour' : '1 jour';
|
return (periode === 'Matin' || periode === 'Après-midi') ? '0.5 jour' : '1 jour';
|
||||||
} else {
|
} else {
|
||||||
// Plusieurs jours
|
|
||||||
return (periode === 'Matin' || periode === 'Après-midi')
|
return (periode === 'Matin' || periode === 'Après-midi')
|
||||||
? `${(totalDays - 0.5).toFixed(1)} jours`
|
? `${(totalDays - 0.5).toFixed(1)} jours`
|
||||||
: `${totalDays} jour${totalDays > 1 ? 's' : ''}`;
|
: `${totalDays} jour${totalDays > 1 ? 's' : ''}`;
|
||||||
@@ -521,20 +653,17 @@ const NewLeaveRequestModal = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* ⭐ SECTION RÉPARTITION POUR PLUSIEURS TYPES */}
|
|
||||||
{/* ⭐ SECTION RÉPARTITION POUR PLUSIEURS TYPES */}
|
|
||||||
{formData.types.length > 1 && totalDays > 0 && (
|
{formData.types.length > 1 && totalDays > 0 && (
|
||||||
<div className="border-t border-gray-200 pt-4">
|
<div className="border-t border-gray-200 pt-4">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
Répartition des {totalDays} jours ouvrés
|
Répartition des {totalDays} jours ouvrés
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-500 mb-4">
|
<p className="text-xs text-gray-500 mb-4">
|
||||||
La somme doit être égale à {totalDays} jour(s)
|
Indiquez la répartition souhaitée (le système vérifiera automatiquement)
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{formData.types.map((type) => {
|
{formData.types.map((type) => {
|
||||||
const showPeriode = ['CP', 'RTT'].includes(type);
|
const showPeriode = ['CP', 'RTT', 'Récup'].includes(type);
|
||||||
const currentValue = repartition[type] || 0;
|
const currentValue = repartition[type] || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -547,11 +676,10 @@ const NewLeaveRequestModal = ({
|
|||||||
type="number"
|
type="number"
|
||||||
step="0.5"
|
step="0.5"
|
||||||
min="0"
|
min="0"
|
||||||
max={type === 'Récup' ? saturdayCount : totalDays}
|
max={totalDays}
|
||||||
value={repartition[type] || ''}
|
value={repartition[type] || ''}
|
||||||
onChange={(e) => handleRepartitionChange(type, e.target.value)}
|
onChange={(e) => handleRepartitionChange(type, e.target.value)}
|
||||||
className="w-24 px-2 py-1 border rounded text-right text-sm"
|
className="w-24 px-2 py-1 border rounded text-right text-sm"
|
||||||
placeholder={type === 'Récup' ? `Max ${saturdayCount}` : ''}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -593,7 +721,6 @@ const NewLeaveRequestModal = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🎯 AFFICHAGE DU NOMBRE DE JOURS POUR CE TYPE */}
|
|
||||||
<div className="mt-2 text-center">
|
<div className="mt-2 text-center">
|
||||||
<span className="text-xs font-medium text-gray-600">
|
<span className="text-xs font-medium text-gray-600">
|
||||||
Durée :
|
Durée :
|
||||||
@@ -611,7 +738,6 @@ const NewLeaveRequestModal = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-900 mb-2">
|
<label className="block text-sm font-medium text-gray-900 mb-2">
|
||||||
Motif (optionnel)
|
Motif (optionnel)
|
||||||
@@ -660,7 +786,7 @@ const NewLeaveRequestModal = ({
|
|||||||
{formData.medicalDocuments.length > 0 && (
|
{formData.medicalDocuments.length > 0 && (
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<p className="text-sm font-medium text-gray-900 mb-2">
|
<p className="text-sm font-medium text-gray-900 mb-2">
|
||||||
Fichiers sélectionnés ({formData.medicalDocuments.length}) :
|
Fichiers sélectionnés ({formData.medicalDocuments.length})
|
||||||
</p>
|
</p>
|
||||||
{formData.medicalDocuments.map((file, index) => (
|
{formData.medicalDocuments.map((file, index) => (
|
||||||
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
|
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
@@ -698,7 +824,7 @@ const NewLeaveRequestModal = ({
|
|||||||
{error && (
|
{error && (
|
||||||
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
|
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
|
||||||
<p className="text-red-700 text-sm">{error}</p>
|
<p className="text-red-700 text-sm whitespace-pre-line">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -713,7 +839,7 @@ const NewLeaveRequestModal = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isLoadingCounters}
|
||||||
className="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium transition-colors"
|
className="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Envoi...' : 'Soumettre'}
|
{isSubmitting ? 'Envoi...' : 'Soumettre'}
|
||||||
@@ -725,4 +851,4 @@ const NewLeaveRequestModal = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NewLeaveRequestModal;
|
export default NewLeaveRequestModal;
|
||||||
|
|||||||
@@ -32,22 +32,17 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Vérifier si l'utilisateur peut voir le compte-rendu d'activités
|
// 🔧 CORRECTION : Vérification améliorée du type de contrat
|
||||||
const canViewCompteRendu = () => {
|
const isForfaitJour =
|
||||||
const allowedRoles = [
|
user?.TypeContrat === 'forfait_jour' ||
|
||||||
'Validateur',
|
user?.typeContrat === 'forfait_jour' ||
|
||||||
'Validatrice',
|
user?.TypeContrat === 'forfaitjour' ||
|
||||||
'Directeur de campus',
|
user?.typeContrat === 'forfaitjour';
|
||||||
'Directrice de campus',
|
|
||||||
'President',
|
|
||||||
'Admin',
|
|
||||||
'RH'
|
|
||||||
];
|
|
||||||
return allowedRoles.includes(user?.role);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Vérifier si l'utilisateur est en forfait jour
|
// 🐛 DEBUG : Décommentez cette ligne pour voir la valeur
|
||||||
const isForfaitJour = user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfaitjour';
|
console.log('👤 User:', user);
|
||||||
|
console.log('📋 Type Contrat:', user?.TypeContrat, user?.typeContrat);
|
||||||
|
console.log('✅ isForfaitJour:', isForfaitJour);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -64,14 +59,12 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
${isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
${isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* Bouton fermer (mobile) */}
|
|
||||||
<div className="lg:hidden flex justify-end p-4">
|
<div className="lg:hidden flex justify-end p-4">
|
||||||
<button onClick={onToggle} className="p-2 rounded-lg hover:bg-gray-100">
|
<button onClick={onToggle} className="p-2 rounded-lg hover:bg-gray-100">
|
||||||
<X className="w-6 h-6" />
|
<X className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Logo */}
|
|
||||||
<div className="p-6 border-b border-gray-100">
|
<div className="p-6 border-b border-gray-100">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<img
|
<img
|
||||||
@@ -82,7 +75,6 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Infos utilisateur */}
|
|
||||||
<div className="p-4 lg:p-6 border-b border-gray-100">
|
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
<img
|
<img
|
||||||
@@ -110,10 +102,10 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<nav className="flex-1 p-4 space-y-2">
|
<nav className="flex-1 p-4 space-y-2">
|
||||||
<Link
|
<Link
|
||||||
to="/dashboard"
|
to="/dashboard"
|
||||||
|
data-tour="dashboard"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/dashboard")
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/dashboard")
|
||||||
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
@@ -121,11 +113,12 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Home className="w-5 h-5" />
|
<Home className="w-5 h-5" />
|
||||||
<span className="font-medium">Dashboard</span>
|
<span className="font-medium">Tableau de bord</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/demandes"
|
to="/demandes"
|
||||||
|
data-tour="demandes"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/demandes")
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/demandes")
|
||||||
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
@@ -138,6 +131,7 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/calendrier"
|
to="/calendrier"
|
||||||
|
data-tour="calendrier"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/calendrier")
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/calendrier")
|
||||||
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
@@ -148,10 +142,11 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
<span className="font-medium">Calendrier</span>
|
<span className="font-medium">Calendrier</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Lien Compte-Rendu d'Activités - Visible pour validateurs et directeurs */}
|
{/* 🔧 LIEN COMPTE-RENDU AVEC DEBUG */}
|
||||||
{(canViewCompteRendu() || isForfaitJour) && (
|
{isForfaitJour && (
|
||||||
<Link
|
<Link
|
||||||
to="/compte-rendu-activites"
|
to="/compte-rendu-activites"
|
||||||
|
data-tour="compte-rendu"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/compte-rendu-activites")
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/compte-rendu-activites")
|
||||||
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
@@ -181,6 +176,7 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={targetPath}
|
to={targetPath}
|
||||||
|
data-tour="mon-equipe"
|
||||||
onClick={() => window.innerWidth < 1024 && onToggle()}
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(targetPath)
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(targetPath)
|
||||||
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
|
||||||
@@ -194,7 +190,6 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
})()}
|
})()}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Bouton déconnexion */}
|
|
||||||
<div className="p-4 border-t border-gray-100">
|
<div className="p-4 border-t border-gray-100">
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
@@ -209,4 +204,4 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import * as msal from '@azure/msal-browser';
|
import * as msal from '@azure/msal-browser';
|
||||||
import { msalConfig, loginRequest } from '../AuthConfig';
|
// ✅ Correction: Import de API_BASE_URL
|
||||||
|
import { msalConfig, loginRequest, API_BASE_URL } from '../authConfig';
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
@@ -19,7 +20,12 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isMsalInitialized, setIsMsalInitialized] = useState(false);
|
const [isMsalInitialized, setIsMsalInitialized] = useState(false);
|
||||||
|
|
||||||
const getApiUrl = (endpoint) => `http://localhost:3000/${endpoint}`;
|
// ✅ Fonction corrigée pour construire l'URL
|
||||||
|
const getApiUrl = (endpoint) => {
|
||||||
|
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
||||||
|
// API_BASE_URL est "/api", donc cela retourne "/api/endpoint"
|
||||||
|
return `${API_BASE_URL}/${cleanEndpoint}`;
|
||||||
|
};
|
||||||
|
|
||||||
// --- Vérifie l'autorisation de l'utilisateur via groupes
|
// --- Vérifie l'autorisation de l'utilisateur via groupes
|
||||||
const checkUserAuthorization = async (userPrincipalName, accessToken) => {
|
const checkUserAuthorization = async (userPrincipalName, accessToken) => {
|
||||||
@@ -167,7 +173,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
if (authResult.authorized) {
|
if (authResult.authorized) {
|
||||||
setUser({
|
setUser({
|
||||||
id: syncResult?.localUserId || entraUser.id,
|
id: syncResult?.localUserId || entraUser.id,
|
||||||
CollaborateurADId: syncResult?.localUserId, // ⭐ AJOUT
|
CollaborateurADId: syncResult?.localUserId,
|
||||||
entraUserId: entraUser.id,
|
entraUserId: entraUser.id,
|
||||||
name: entraUser.displayName,
|
name: entraUser.displayName,
|
||||||
prenom: entraUser.givenName || entraUser.displayName?.split(' ')[0] || '',
|
prenom: entraUser.givenName || entraUser.displayName?.split(' ')[0] || '',
|
||||||
@@ -179,8 +185,8 @@ export const AuthProvider = ({ children }) => {
|
|||||||
jobTitle: entraUser.jobTitle,
|
jobTitle: entraUser.jobTitle,
|
||||||
department: entraUser.department,
|
department: entraUser.department,
|
||||||
officeLocation: entraUser.officeLocation,
|
officeLocation: entraUser.officeLocation,
|
||||||
typeContrat: syncResult?.typeContrat || '37h', // ⭐ AJOUT
|
typeContrat: syncResult?.typeContrat || '37h',
|
||||||
dateEntree: syncResult?.dateEntree || null, // ⭐ AJOUT
|
dateEntree: syncResult?.dateEntree || null,
|
||||||
groups: authResult.groups
|
groups: authResult.groups
|
||||||
});
|
});
|
||||||
setIsAuthorized(true);
|
setIsAuthorized(true);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import App from './App.jsx';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import { MsalProvider } from "@azure/msal-react";
|
import { MsalProvider } from "@azure/msal-react";
|
||||||
import { PublicClientApplication } from "@azure/msal-browser";
|
import { PublicClientApplication } from "@azure/msal-browser";
|
||||||
import { msalConfig } from "./AuthConfig";
|
import { msalConfig } from "./authConfig";
|
||||||
|
|
||||||
const msalInstance = new PublicClientApplication(msalConfig);
|
const msalInstance = new PublicClientApplication(msalConfig);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@ const Collaborateur = () => {
|
|||||||
|
|
||||||
const fetchTeamMembers = async () => {
|
const fetchTeamMembers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:3000/getTeamMembers?manager_id=${user.id}`);
|
const response = await fetch(`/getTeamMembers?manager_id=${user.id}`);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
console.log('Réponse équipe:', text);
|
console.log('Réponse équipe:', text);
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ const Collaborateur = () => {
|
|||||||
|
|
||||||
const fetchPendingRequests = async () => {
|
const fetchPendingRequests = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:3000/getPendingRequests?manager_id=${user.id}`);
|
const response = await fetch(`/getPendingRequests?manager_id=${user.id}`);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
console.log('Réponse demandes en attente:', text);
|
console.log('Réponse demandes en attente:', text);
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ const Collaborateur = () => {
|
|||||||
|
|
||||||
const fetchAllTeamRequests = async () => {
|
const fetchAllTeamRequests = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:3000/getAllTeamRequests?SuperieurId=${user.id}`);
|
const response = await fetch(`/getAllTeamRequests?SuperieurId=${user.id}`);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
console.log('Réponse toutes demandes équipe:', text);
|
console.log('Réponse toutes demandes équipe:', text);
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ const Collaborateur = () => {
|
|||||||
|
|
||||||
const handleValidateRequest = async (requestId, action, comment = '') => {
|
const handleValidateRequest = async (requestId, action, comment = '') => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:3000/validateRequest', {
|
const response = await fetch('/validateRequest', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -191,9 +191,7 @@ const Collaborateur = () => {
|
|||||||
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
||||||
{isEmployee ? 'Mon équipe 👥' : 'Gestion d\'équipe 👥'}
|
{isEmployee ? 'Mon équipe 👥' : 'Gestion d\'équipe 👥'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm lg:text-base text-gray-600">
|
|
||||||
{isEmployee ? 'Consultez les congés de votre équipe' : 'Gérez les demandes de congés de votre équipe'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
@@ -224,35 +222,9 @@ const Collaborateur = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs lg:text-sm font-medium text-gray-600">Approuvées</p>
|
|
||||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">
|
|
||||||
{allRequests.filter(r => r.status === 'Validée' || r.status === 'Approuvé').length}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">demandes</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
|
||||||
<CheckCircle className="w-4 h-4 lg:w-6 lg:h-6 text-green-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs lg:text-sm font-medium text-gray-600">Refusées</p>
|
|
||||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">
|
|
||||||
{allRequests.filter(r => r.status === 'Refusée').length}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">demandes</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-red-100 rounded-lg flex items-center justify-center">
|
|
||||||
<XCircle className="w-4 h-4 lg:w-6 lg:h-6 text-red-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
@@ -408,7 +380,7 @@ const Collaborateur = () => {
|
|||||||
<div className="text-sm mt-1">
|
<div className="text-sm mt-1">
|
||||||
<p className="text-gray-500">Document joint</p>
|
<p className="text-gray-500">Document joint</p>
|
||||||
<a
|
<a
|
||||||
href={`http://localhost/GTA/project/uploads/${request.file}`}
|
href={`/GTA/project/uploads/${request.file}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:underline flex items-center gap-1 mt-1"
|
className="text-blue-600 hover:underline flex items-center gap-1 mt-1"
|
||||||
@@ -464,7 +436,7 @@ const Collaborateur = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-gray-500">Document joint</p>
|
<p className="text-gray-500">Document joint</p>
|
||||||
<a
|
<a
|
||||||
href={`http://localhost/GTA/project/uploads/${selectedRequest.file}`}
|
href={`/GTA/project/uploads/${selectedRequest.file}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:underline flex items-center gap-2"
|
className="text-blue-600 hover:underline flex items-center gap-2"
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const CompteRenduActivites = () => {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:3000/getCompteRenduActivites?user_id=${userId}&annee=${annee}&mois=${mois}`);
|
const response = await fetch(`/getCompteRenduActivites?user_id=${userId}&annee=${annee}&mois=${mois}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -106,7 +106,7 @@ const CompteRenduActivites = () => {
|
|||||||
console.log('📊 Détail des jours:', data.jours);
|
console.log('📊 Détail des jours:', data.jours);
|
||||||
}
|
}
|
||||||
|
|
||||||
const congesResponse = await fetch(`http://localhost:3000/getTeamLeaves?user_id=${userId}&role=${user.role}`);
|
const congesResponse = await fetch(`/getTeamLeaves?user_id=${userId}&role=${user.role}`);
|
||||||
const congesData = await congesResponse.json();
|
const congesData = await congesResponse.json();
|
||||||
|
|
||||||
if (congesData.success) {
|
if (congesData.success) {
|
||||||
@@ -125,7 +125,7 @@ const CompteRenduActivites = () => {
|
|||||||
if (!userId || !hasAccess()) return;
|
if (!userId || !hasAccess()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:3000/getStatsAnnuelles?user_id=${userId}&annee=${annee}`);
|
const response = await fetch(`/getStatsAnnuelles?user_id=${userId}&annee=${annee}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -163,22 +163,38 @@ const CompteRenduActivites = () => {
|
|||||||
return selectedYear === previousYear && selectedMonth === previousMonth;
|
return selectedYear === previousYear && selectedMonth === previousMonth;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Générer les jours du mois (lundi-vendredi)
|
// Générer les jours du mois (lundi-samedi) avec décalage correct
|
||||||
const getDaysInMonth = () => {
|
const getDaysInMonth = () => {
|
||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
const month = currentDate.getMonth();
|
const month = currentDate.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
const lastDay = new Date(year, month + 1, 0);
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
const daysInMonth = lastDay.getDate();
|
const daysInMonth = lastDay.getDate();
|
||||||
|
|
||||||
|
// Jour de la semaine du 1er (0=dimanche, 1=lundi, ..., 6=samedi)
|
||||||
|
let firstDayOfWeek = firstDay.getDay();
|
||||||
|
|
||||||
|
// Convertir pour que lundi = 0, mardi = 1, ..., samedi = 5, dimanche = 6
|
||||||
|
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
|
||||||
|
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
|
// Ajouter des cases vides pour le décalage initial
|
||||||
|
for (let i = 0; i < firstDayOfWeek; i++) {
|
||||||
|
days.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter tous les jours du mois (lundi-samedi uniquement)
|
||||||
for (let day = 1; day <= daysInMonth; day++) {
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
const currentDay = new Date(year, month, day);
|
const currentDay = new Date(year, month, day);
|
||||||
const dayOfWeek = currentDay.getDay();
|
const dayOfWeek = currentDay.getDay();
|
||||||
|
|
||||||
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
|
// Exclure les dimanches (0)
|
||||||
|
if (dayOfWeek !== 0) {
|
||||||
days.push(currentDay);
|
days.push(currentDay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return days;
|
return days;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,6 +251,8 @@ const CompteRenduActivites = () => {
|
|||||||
|
|
||||||
// Ouvrir le modal de saisie
|
// Ouvrir le modal de saisie
|
||||||
const handleJourClick = (date) => {
|
const handleJourClick = (date) => {
|
||||||
|
if (!date) return; // Ignorer les cases vides
|
||||||
|
|
||||||
if (!isMoisAutorise() && !isRH) {
|
if (!isMoisAutorise() && !isRH) {
|
||||||
showInfo('Vous ne pouvez saisir que pour le mois en cours ou le mois précédent', 'warning');
|
showInfo('Vous ne pouvez saisir que pour le mois en cours ou le mois précédent', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -284,7 +302,7 @@ const CompteRenduActivites = () => {
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:3000/saveCompteRenduJour', {
|
const response = await fetch('/saveCompteRenduJour', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -327,7 +345,7 @@ const CompteRenduActivites = () => {
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:3000/saveCompteRenduMasse', {
|
const response = await fetch('/saveCompteRenduMasse', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -487,14 +505,7 @@ const CompteRenduActivites = () => {
|
|||||||
<p className="text-sm opacity-90">Jours travaillés</p>
|
<p className="text-sm opacity-90">Jours travaillés</p>
|
||||||
<p className="text-3xl font-bold">{statsAnnuelles.totalJoursTravailles || 0}</p>
|
<p className="text-3xl font-bold">{statsAnnuelles.totalJoursTravailles || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white bg-opacity-20 rounded-lg p-4">
|
|
||||||
<p className="text-sm opacity-90">Non-respect repos quotidien</p>
|
|
||||||
<p className="text-3xl font-bold">{statsAnnuelles.totalNonRespectQuotidien || 0}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white bg-opacity-20 rounded-lg p-4">
|
|
||||||
<p className="text-sm opacity-90">Non-respect repos hebdo</p>
|
|
||||||
<p className="text-3xl font-bold">{statsAnnuelles.totalNonRespectHebdo || 0}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -552,23 +563,30 @@ const CompteRenduActivites = () => {
|
|||||||
<span className="hidden sm:inline">Saisie en masse</span>
|
<span className="hidden sm:inline">Saisie en masse</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Calendrier */}
|
{/* Calendrier */}
|
||||||
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
|
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
|
||||||
<div className="grid grid-cols-5 gap-2 p-4 bg-gray-50">
|
<div className="grid grid-cols-6 gap-2 p-4 bg-gray-50">
|
||||||
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi'].map(day => (
|
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'].map(day => (
|
||||||
<div key={day} className="text-center font-semibold text-gray-700 text-sm">
|
<div key={day} className="text-center font-semibold text-gray-700 text-sm">
|
||||||
{day}
|
{day}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-2 p-4">
|
<div className="grid grid-cols-6 gap-2 p-4">
|
||||||
{days.map((date, index) => {
|
{days.map((date, index) => {
|
||||||
|
// Case vide pour le décalage
|
||||||
|
if (date === null) {
|
||||||
|
return (
|
||||||
|
<div key={`empty-${index}`} className="min-h-[100px] p-3"></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const jourData = getJourData(date);
|
const jourData = getJourData(date);
|
||||||
const enConge = isJourEnConge(date);
|
const enConge = isJourEnConge(date);
|
||||||
const ferie = isHoliday(date);
|
const ferie = isHoliday(date);
|
||||||
@@ -707,17 +725,7 @@ const CompteRenduActivites = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
|
||||||
<label className="flex items-center gap-3 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedJour.jourTravaille}
|
|
||||||
onChange={(e) => setSelectedJour({ ...selectedJour, jourTravaille: e.target.checked })}
|
|
||||||
className="w-5 h-5 text-blue-600 rounded"
|
|
||||||
/>
|
|
||||||
<span className="text-gray-700 font-medium">Jour travaillé</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedJour.jourTravaille && (
|
{selectedJour.jourTravaille && (
|
||||||
<>
|
<>
|
||||||
@@ -815,7 +823,7 @@ const CompteRenduActivites = () => {
|
|||||||
<SaisieMasseModal
|
<SaisieMasseModal
|
||||||
mois={mois}
|
mois={mois}
|
||||||
annee={annee}
|
annee={annee}
|
||||||
days={days}
|
days={days.filter(d => d !== null)} // Filtrer les cases vides
|
||||||
congesData={congesData}
|
congesData={congesData}
|
||||||
holidays={holidays}
|
holidays={holidays}
|
||||||
onClose={() => setShowSaisieMasse(false)}
|
onClose={() => setShowSaisieMasse(false)}
|
||||||
@@ -894,6 +902,30 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
|||||||
onSave(joursTravailles);
|
onSave(joursTravailles);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Générer les jours avec décalage pour la saisie en masse aussi
|
||||||
|
const getDaysWithOffset = () => {
|
||||||
|
const year = annee;
|
||||||
|
const month = mois - 1;
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
|
||||||
|
let firstDayOfWeek = firstDay.getDay();
|
||||||
|
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
|
||||||
|
|
||||||
|
const daysWithOffset = [];
|
||||||
|
|
||||||
|
// Ajouter des cases vides pour le décalage
|
||||||
|
for (let i = 0; i < firstDayOfWeek; i++) {
|
||||||
|
daysWithOffset.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les jours réels
|
||||||
|
daysWithOffset.push(...days);
|
||||||
|
|
||||||
|
return daysWithOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
const daysWithOffset = getDaysWithOffset();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||||
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full p-6 max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full p-6 max-h-[90vh] overflow-y-auto">
|
||||||
@@ -915,8 +947,15 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
|||||||
Sélectionner tous les jours ouvrés disponibles
|
Sélectionner tous les jours ouvrés disponibles
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-2 mb-6">
|
<div className="grid grid-cols-6 gap-2 p-4">
|
||||||
{days.map((date, index) => {
|
{daysWithOffset.map((date, index) => {
|
||||||
|
// Case vide
|
||||||
|
if (date === null) {
|
||||||
|
return (
|
||||||
|
<div key={`empty-${index}`} className="p-3"></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const dateStr = formatDateToString(date);
|
const dateStr = formatDateToString(date);
|
||||||
const enConge = isJourEnConge(date);
|
const enConge = isJourEnConge(date);
|
||||||
const ferie = isHoliday(date);
|
const ferie = isHoliday(date);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import Sidebar from '../components/Sidebar';
|
import Sidebar from '../components/Sidebar';
|
||||||
import { Calendar, Clock, Plus, RefreshCw, Menu, Info, Briefcase, AlertCircle, Wifi, WifiOff, TrendingUp } from 'lucide-react';
|
import { Calendar, Clock, Plus, RefreshCw, Menu, Info, Briefcase, AlertCircle, Wifi, WifiOff, TrendingUp, HelpCircle, FileText, ChevronRight, Users } from 'lucide-react';
|
||||||
import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
||||||
import { useMsal } from "@azure/msal-react";
|
import { useMsal } from "@azure/msal-react";
|
||||||
import { loginRequest } from "../authConfig";
|
import { loginRequest } from "../authConfig";
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -19,16 +20,23 @@ const Dashboard = () => {
|
|||||||
const [showNotifications, setShowNotifications] = useState(false);
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
const [lastRefresh, setLastRefresh] = useState(new Date());
|
const [lastRefresh, setLastRefresh] = useState(new Date());
|
||||||
|
|
||||||
// ⭐ NOUVEAUX STATES POUR SSE
|
|
||||||
const [sseConnected, setSseConnected] = useState(false);
|
const [sseConnected, setSseConnected] = useState(false);
|
||||||
const [toasts, setToasts] = useState([]);
|
const [toasts, setToasts] = useState([]);
|
||||||
|
|
||||||
// ⭐ NOUVEAU STATE POUR CONGÉS ANTICIPÉS
|
|
||||||
const [congesAnticipes, setCongesAnticipes] = useState(null);
|
const [congesAnticipes, setCongesAnticipes] = useState(null);
|
||||||
const [showAnticipes, setShowAnticipes] = useState(false);
|
const [showAnticipes, setShowAnticipes] = useState(false);
|
||||||
|
const [recentRequests, setRecentRequests] = useState([]);
|
||||||
|
const [teamLeaves, setTeamLeaves] = useState([]);
|
||||||
|
const [isUpdatingCounters, setIsUpdatingCounters] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
const userId = user?.id || user?.CollaborateurADId || user?.ID;
|
const userId = user?.id || user?.CollaborateurADId || user?.ID;
|
||||||
|
|
||||||
|
// 🎯 FONCTION POUR RELANCER LE TUTORIEL
|
||||||
|
const handleRestartTutorial = () => {
|
||||||
|
localStorage.removeItem(`global-tutorial-completed-${userId}`);
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accounts.length > 0) {
|
if (accounts.length > 0) {
|
||||||
const request = {
|
const request = {
|
||||||
@@ -52,7 +60,7 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
const fetchNotifications = async () => {
|
const fetchNotifications = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:3000/getNotifications?user_id=${userId}`);
|
const response = await fetch(`/getNotifications?user_id=${userId}`);
|
||||||
if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`);
|
if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`);
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type');
|
||||||
if (!contentType || !contentType.includes('application/json')) {
|
if (!contentType || !contentType.includes('application/json')) {
|
||||||
@@ -73,29 +81,46 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
const fetchDetailedCounters = async () => {
|
const fetchDetailedCounters = async () => {
|
||||||
try {
|
try {
|
||||||
if (!isLoading) setIsRefreshing(true);
|
if (!isLoading) {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
setIsUpdatingCounters(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔄 Appel getDetailedLeaveCounters pour userId:', userId);
|
||||||
|
|
||||||
|
const response = await fetch(`/getDetailedLeaveCounters?user_id=${userId}`);
|
||||||
|
|
||||||
|
// Debug: afficher le statut HTTP
|
||||||
|
console.log('📡 Statut HTTP:', response.status);
|
||||||
|
|
||||||
const response = await fetch(`http://localhost:3000/getDetailedLeaveCounters?user_id=${userId}`);
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Debug: afficher les données reçues
|
||||||
|
console.log('📊 Données compteurs reçues:', JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
console.log('✅ Compteurs mis à jour dans le state');
|
||||||
|
console.log(' - CP N-1:', data.data?.cpN1?.solde);
|
||||||
|
console.log(' - CP N:', data.data?.cpN?.solde);
|
||||||
|
console.log(' - RTT:', data.data?.rttN?.solde);
|
||||||
setDetailedCounters(data.data);
|
setDetailedCounters(data.data);
|
||||||
} else {
|
} else {
|
||||||
console.error("Erreur compteurs:", data.message);
|
console.error("❌ Erreur compteurs:", data.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("💥 Erreur compteurs:", error);
|
console.error("💥 Erreur compteurs:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
|
setIsUpdatingCounters(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ NOUVELLE FONCTION : Récupérer les congés anticipés
|
|
||||||
const fetchCongesAnticipes = async () => {
|
const fetchCongesAnticipes = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('🔍 Récupération des congés anticipés...');
|
console.log('🔍 Récupération des congés anticipés...');
|
||||||
const response = await fetch(`http://localhost:3000/getCongesAnticipes?user_id=${userId}`);
|
const response = await fetch(`/getCongesAnticipes?user_id=${userId}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -109,10 +134,84 @@ const Dashboard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ FONCTION DE RAFRAÎCHISSEMENT UNIFIÉE (modifiée)
|
const fetchRecentRequests = async () => {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Récupération des demandes récentes pour userId:', userId);
|
||||||
|
|
||||||
|
const url = `/getRequests?user_id=${userId}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(text);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('❌ Erreur parsing JSON:', parseError);
|
||||||
|
console.log('📄 Réponse brute:', text.substring(0, 200));
|
||||||
|
setRecentRequests([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Données reçues:', data);
|
||||||
|
|
||||||
|
if (data.success && data.requests) {
|
||||||
|
console.log('✅ Nombre total de demandes:', data.requests.length);
|
||||||
|
|
||||||
|
if (data.requests.length > 0) {
|
||||||
|
console.log('📋 Structure de la première demande:', data.requests[0]);
|
||||||
|
console.log('📋 Tous les champs disponibles:', Object.keys(data.requests[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedRequests = data.requests
|
||||||
|
.sort((a, b) => {
|
||||||
|
const dateA = new Date(a.submittedAt || a.DateCreation || a.created_at);
|
||||||
|
const dateB = new Date(b.submittedAt || b.DateCreation || b.created_at);
|
||||||
|
return dateB - dateA;
|
||||||
|
})
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
console.log('✅ 5 demandes récentes:', sortedRequests);
|
||||||
|
setRecentRequests(sortedRequests);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Aucune demande trouvée ou erreur:', data.message);
|
||||||
|
setRecentRequests([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("💥 Erreur demandes récentes:", error);
|
||||||
|
setRecentRequests([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTeamLeaves = async () => {
|
||||||
|
try {
|
||||||
|
const currentMonth = new Date().getMonth();
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const response = await fetch(`/getTeamLeaves?user_id=${userId}&role=${user.role}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const filteredLeaves = (data.leaves || []).filter(leave => {
|
||||||
|
const startDate = new Date(leave.startdate);
|
||||||
|
const endDate = new Date(leave.enddate);
|
||||||
|
const startMonth = startDate.getMonth();
|
||||||
|
const startYear = startDate.getFullYear();
|
||||||
|
const endMonth = endDate.getMonth();
|
||||||
|
const endYear = endDate.getFullYear();
|
||||||
|
|
||||||
|
return (startYear === currentYear && startMonth === currentMonth) ||
|
||||||
|
(endYear === currentYear && endMonth === currentMonth) ||
|
||||||
|
(startDate <= new Date(currentYear, currentMonth, 1) &&
|
||||||
|
endDate >= new Date(currentYear, currentMonth + 1, 0));
|
||||||
|
});
|
||||||
|
setTeamLeaves(filteredLeaves);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("💥 Erreur congés équipe:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshAllData = useCallback(async () => {
|
const refreshAllData = useCallback(async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
console.log('🔄 Rafraîchissement des données...');
|
console.log('🔄 Rafraîchissement des données...');
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|
||||||
@@ -120,7 +219,9 @@ const Dashboard = () => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchDetailedCounters(),
|
fetchDetailedCounters(),
|
||||||
fetchNotifications(),
|
fetchNotifications(),
|
||||||
fetchCongesAnticipes() // ⭐ AJOUT
|
fetchCongesAnticipes(),
|
||||||
|
fetchRecentRequests(),
|
||||||
|
fetchTeamLeaves()
|
||||||
]);
|
]);
|
||||||
setLastRefresh(new Date());
|
setLastRefresh(new Date());
|
||||||
console.log('✅ Données rafraîchies');
|
console.log('✅ Données rafraîchies');
|
||||||
@@ -131,7 +232,6 @@ const Dashboard = () => {
|
|||||||
}
|
}
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
// ⭐ FONCTION POUR AFFICHER DES TOASTS
|
|
||||||
const showToast = useCallback((message, type = 'info') => {
|
const showToast = useCallback((message, type = 'info') => {
|
||||||
const id = Date.now();
|
const id = Date.now();
|
||||||
const newToast = { id, message, type };
|
const newToast = { id, message, type };
|
||||||
@@ -143,13 +243,12 @@ const Dashboard = () => {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ⭐ CONNEXION SSE
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
console.log('🔌 Connexion SSE au serveur collaborateurs...');
|
console.log('🔌 Connexion SSE au serveur collaborateurs...');
|
||||||
|
|
||||||
const eventSource = new EventSource(`http://localhost:3000/api/events/collaborateur?user_id=${userId}`);
|
const eventSource = new EventSource(`/api/events/collaborateur?user_id=${userId}`);
|
||||||
|
|
||||||
eventSource.onopen = () => {
|
eventSource.onopen = () => {
|
||||||
console.log('✅ SSE connecté');
|
console.log('✅ SSE connecté');
|
||||||
@@ -167,6 +266,7 @@ const Dashboard = () => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'heartbeat':
|
case 'heartbeat':
|
||||||
|
// Ne rien afficher pour éviter de polluer les logs
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'demande-validated-rh':
|
case 'demande-validated-rh':
|
||||||
@@ -182,9 +282,44 @@ const Dashboard = () => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'compteur-updated':
|
case 'compteur-updated':
|
||||||
console.log('📊 Compteurs mis à jour via SSE');
|
console.log('\n💰 === COMPTEUR MIS À JOUR (SSE) ===');
|
||||||
fetchDetailedCounters();
|
console.log(' Collaborateur SSE:', data.collaborateurId, typeof data.collaborateurId);
|
||||||
fetchCongesAnticipes(); // ⭐ AJOUT
|
console.log(' UserId local:', userId, typeof userId);
|
||||||
|
console.log(' Type congé:', data.typeConge);
|
||||||
|
console.log(' Année:', data.annee);
|
||||||
|
console.log(' Jours:', data.jours);
|
||||||
|
console.log(' Type mise à jour:', data.typeUpdate);
|
||||||
|
|
||||||
|
// ✅ CORRECTION: Comparer en convertissant les deux en nombres
|
||||||
|
const collabIdNum = parseInt(data.collaborateurId);
|
||||||
|
const userIdNum = parseInt(userId);
|
||||||
|
|
||||||
|
console.log(' Comparaison:', collabIdNum, '===', userIdNum, '?', collabIdNum === userIdNum);
|
||||||
|
|
||||||
|
// Vérifier que c'est bien pour cet utilisateur
|
||||||
|
if (collabIdNum === userIdNum) {
|
||||||
|
console.log('✅ C\'EST POUR MOI ! Mise à jour des compteurs...');
|
||||||
|
|
||||||
|
// Afficher un toast informatif
|
||||||
|
if (data.typeUpdate === 'reinitialisation') {
|
||||||
|
showToast(`📊 Compteur ${data.typeConge} ${data.annee} réinitialisé`, 'info');
|
||||||
|
} else if (data.typeUpdate === 'actualisation_manuel') {
|
||||||
|
showToast(`🔄 Compteur ${data.typeConge} actualisé`, 'success');
|
||||||
|
} else if (data.typeUpdate === 'actualisation_globale') {
|
||||||
|
showToast('✅ Mise à jour automatique des compteurs', 'info');
|
||||||
|
} else if (data.typeUpdate === 'modification_manuelle') {
|
||||||
|
showToast(`✏️ Compteur ${data.typeConge} ${data.annee} modifié par RH`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rafraîchir les compteurs
|
||||||
|
console.log('🔄 Rafraîchissement des compteurs...');
|
||||||
|
fetchDetailedCounters().then(() => {
|
||||||
|
fetchCongesAnticipes();
|
||||||
|
console.log('✅ Compteurs rafraîchis');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('❌ Pas pour moi, j\'ignore');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'demande-list-updated':
|
case 'demande-list-updated':
|
||||||
@@ -227,14 +362,12 @@ const Dashboard = () => {
|
|||||||
};
|
};
|
||||||
}, [userId, refreshAllData, showToast]);
|
}, [userId, refreshAllData, showToast]);
|
||||||
|
|
||||||
// ⭐ RAFRAÎCHISSEMENT INITIAL
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
refreshAllData();
|
refreshAllData();
|
||||||
}
|
}
|
||||||
}, [userId, refreshAllData]);
|
}, [userId, refreshAllData]);
|
||||||
|
|
||||||
// ⭐ Polling de secours
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
@@ -246,7 +379,6 @@ const Dashboard = () => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [userId, refreshAllData]);
|
}, [userId, refreshAllData]);
|
||||||
|
|
||||||
// ⭐ Rafraîchissement quand la page redevient visible
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
if (document.visibilityState === 'visible' && userId) {
|
if (document.visibilityState === 'visible' && userId) {
|
||||||
@@ -259,7 +391,6 @@ const Dashboard = () => {
|
|||||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
}, [userId, refreshAllData]);
|
}, [userId, refreshAllData]);
|
||||||
|
|
||||||
// ⭐ Rafraîchissement au focus
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
@@ -274,7 +405,7 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
const markNotificationRead = async (id) => {
|
const markNotificationRead = async (id) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://localhost:3000/markNotificationRead`, {
|
const res = await fetch(`/markNotificationRead`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ notificationId: id }),
|
body: JSON.stringify({ notificationId: id }),
|
||||||
@@ -302,31 +433,6 @@ const Dashboard = () => {
|
|||||||
await refreshAllData();
|
await refreshAllData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateCounters = async () => {
|
|
||||||
if (!confirm("🔄 Mettre à jour vos compteurs avec l'acquisition du mois en cours ?")) return;
|
|
||||||
try {
|
|
||||||
const response = await fetch('http://localhost:3000/updateCounters', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ collaborateur_id: userId }),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
const updatesText = data.updates.map(u =>
|
|
||||||
`${u.type}: +${(u.increment || 0).toFixed(2)}j (total: ${(u.nouveauSolde || 0).toFixed(2)}j)`
|
|
||||||
).join('\n');
|
|
||||||
alert("✅ Compteurs mis à jour !\n\n" + updatesText);
|
|
||||||
|
|
||||||
await refreshAllData();
|
|
||||||
} else {
|
|
||||||
alert(`❌ Erreur : ${data.message}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur:", error);
|
|
||||||
alert("❌ Erreur serveur");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
if (!dateStr) return 'N/A';
|
if (!dateStr) return 'N/A';
|
||||||
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
||||||
@@ -445,6 +551,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 lg:gap-3">
|
<div className="flex gap-2 lg:gap-3">
|
||||||
<button
|
<button
|
||||||
|
data-tour="refresh"
|
||||||
onClick={handleManualRefresh}
|
onClick={handleManualRefresh}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
className="bg-gray-200 text-gray-700 px-3 lg:px-4 py-2 lg:py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="bg-gray-200 text-gray-700 px-3 lg:px-4 py-2 lg:py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
@@ -456,6 +563,7 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
|
data-tour="notifications"
|
||||||
onClick={() => setShowNotifications(!showNotifications)}
|
onClick={() => setShowNotifications(!showNotifications)}
|
||||||
className="relative bg-white border border-gray-200 px-3 lg:px-4 py-2 lg:py-3 rounded-lg font-medium hover:bg-gray-50 transition-colors flex items-center gap-2 shadow-sm"
|
className="relative bg-white border border-gray-200 px-3 lg:px-4 py-2 lg:py-3 rounded-lg font-medium hover:bg-gray-50 transition-colors flex items-center gap-2 shadow-sm"
|
||||||
>
|
>
|
||||||
@@ -514,6 +622,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
data-tour="nouvelle-demande"
|
||||||
onClick={() => setShowNewRequestModal(true)}
|
onClick={() => setShowNewRequestModal(true)}
|
||||||
className="bg-cyan-600 text-white px-3 lg:px-6 py-2 lg:py-3 rounded-lg font-medium hover:bg-cyan-700 transition-colors flex items-center gap-2"
|
className="bg-cyan-600 text-white px-3 lg:px-6 py-2 lg:py-3 rounded-lg font-medium hover:bg-cyan-700 transition-colors flex items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -537,10 +646,10 @@ const Dashboard = () => {
|
|||||||
<div className="bg-gradient-to-r from-cyan-500 to-blue-500 rounded-xl shadow-md p-6 mb-6 text-white">
|
<div className="bg-gradient-to-r from-cyan-500 to-blue-500 rounded-xl shadow-md p-6 mb-6 text-white">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex flex-wrap gap-3 text-sm">
|
<div className="flex flex-wrap gap-3 text-sm mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Briefcase className="w-4 h-4" />
|
<Briefcase className="w-4 h-4" />
|
||||||
<span>{detailedCounters.user.role}</span>
|
<span className="font-medium">{detailedCounters.user.role}</span>
|
||||||
</div>
|
</div>
|
||||||
{detailedCounters.user.service && (
|
{detailedCounters.user.service && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -557,8 +666,25 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-3 border-t border-white border-opacity-20">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide opacity-90 mb-1">
|
||||||
|
Fonction
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{detailedCounters.user.description || '(Non renseignée)'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{detailedCounters.user.dateEntree && (
|
{detailedCounters.user.dateEntree && (
|
||||||
<p className="text-sm mt-2 opacity-90">
|
<p className="text-sm mt-3 opacity-90">
|
||||||
📅 Ancienneté : {detailedCounters.user.ancienneteAnnees} an{detailedCounters.user.ancienneteAnnees > 1 ? 's' : ''} et {detailedCounters.user.ancienneteMoisRestants} mois
|
📅 Ancienneté : {detailedCounters.user.ancienneteAnnees} an{detailedCounters.user.ancienneteAnnees > 1 ? 's' : ''} et {detailedCounters.user.ancienneteMoisRestants} mois
|
||||||
{' '}(depuis le {formatDate(detailedCounters.user.dateEntree)})
|
{' '}(depuis le {formatDate(detailedCounters.user.dateEntree)})
|
||||||
</p>
|
</p>
|
||||||
@@ -571,255 +697,237 @@ const Dashboard = () => {
|
|||||||
{detailedCounters && (
|
{detailedCounters && (
|
||||||
<div className="space-y-6 mb-8">
|
<div className="space-y-6 mb-8">
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border-2 border-cyan-200 p-6">
|
{/* 🆕 SECTION CÔTE À CÔTE : Demandes récentes + Congés du service */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
|
||||||
<Info className="w-5 h-5 text-cyan-600" />
|
|
||||||
Total disponible
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
|
{/* DEMANDES RÉCENTES - 1 colonne */}
|
||||||
|
<div data-tour="demandes-recentes" className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 lg:col-span-1">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-cyan-600" />
|
||||||
|
Mes dernières demandes
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/demandes')}
|
||||||
|
className="text-sm text-cyan-600 hover:text-cyan-700 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Voir tout
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{recentRequests.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-sm text-center py-4">Aucune demande récente</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{recentRequests.map((request, idx) => {
|
||||||
|
const type = request.type || 'Congé';
|
||||||
|
const statut = request.status || 'En attente';
|
||||||
|
const dateDebut = request.startDate;
|
||||||
|
const dateFin = request.endDate;
|
||||||
|
const nbJours = request.days || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={request.id || idx} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer" onClick={() => navigate('/demandes')}>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-gray-900">{type}</span>
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full ${statut === 'Validée' ? 'bg-green-100 text-green-700' :
|
||||||
|
statut === 'En attente' ? 'bg-orange-100 text-orange-700' :
|
||||||
|
statut === 'Refusée' ? 'bg-red-100 text-red-700' :
|
||||||
|
statut === 'Annulée' ? 'bg-gray-100 text-gray-700' :
|
||||||
|
'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{statut}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
{dateDebut && dateFin ? (
|
||||||
|
<>
|
||||||
|
Du {new Date(dateDebut).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||||
|
{' '}au {new Date(dateFin).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' })}
|
||||||
|
</>
|
||||||
|
) : request.dateDisplay || (
|
||||||
|
<span className="text-gray-400">Dates non disponibles</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-lg font-bold text-cyan-600">
|
||||||
|
{nbJours}j
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ⭐ PANNEAU CONGÉS ANTICIPÉS */}
|
{/* CONGÉS DU SERVICE - 2 colonnes (plus large) */}
|
||||||
{showAnticipes && congesAnticipes && (
|
<div data-tour="conges-service" className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 lg:col-span-2">
|
||||||
<div className="bg-gradient-to-br from-gray-50 to-slate-50 border-2 border-gray-300 rounded-lg p-6 mb-6 shadow-lg">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||||
<TrendingUp className="w-6 h-6 text-gray-600" />
|
<Users className="w-5 h-5 text-cyan-600" />
|
||||||
<h4 className="text-xl font-bold text-gray-900">
|
Congés du service - {new Date().toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
|
||||||
Possibilités de congés anticipés
|
</h3>
|
||||||
</h4>
|
<button
|
||||||
</div>
|
onClick={() => navigate('/calendrier')}
|
||||||
|
className="text-sm text-cyan-600 hover:text-cyan-700 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Voir tout
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
{teamLeaves.length === 0 ? (
|
||||||
{/* CP Anticipés */}
|
<p className="text-gray-500 text-sm text-center py-4">Aucun congé prévu ce mois-ci</p>
|
||||||
<div className="bg-white rounded-lg p-4 border-2 border-gray-200 shadow-sm">
|
) : (
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="space-y-2 overflow-y-auto" style={{ maxHeight: '400px' }}>
|
||||||
<h5 className="font-semibold text-gray-900 flex items-center gap-2">
|
{teamLeaves.map((leave, idx) => (
|
||||||
<Calendar className="w-5 h-5" />
|
<div key={idx} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
Congés Payés
|
<div className={`w-3 h-3 rounded-full flex-shrink-0 ${leave.type === 'Formation' ? 'bg-blue-400' :
|
||||||
</h5>
|
leave.statut === 'Validée' ? 'bg-green-400' :
|
||||||
{congesAnticipes.congesPayes.limiteAnticipe > 0 && (
|
leave.statut === 'En attente' ? 'bg-orange-400' :
|
||||||
<span className="bg-green-100 text-green-800 text-xs font-bold px-2 py-1 rounded-full">
|
'bg-gray-400'
|
||||||
Disponible
|
}`}></div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-gray-900 truncate">{leave.employeename}</p>
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
{new Date(leave.startdate).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||||
|
{' '}-{' '}
|
||||||
|
{new Date(leave.enddate).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{leave.type === 'Formation' && (
|
||||||
|
<span className="text-xs text-blue-600 font-medium whitespace-nowrap bg-blue-50 px-2 py-1 rounded">
|
||||||
|
📚 Formation
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">Acquis actuellement:</span>
|
|
||||||
<span className="font-semibold text-gray-900">
|
|
||||||
{congesAnticipes.congesPayes.acquisActuelle}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">Prévision fin exercice:</span>
|
|
||||||
<span className="font-semibold text-gray-900">
|
|
||||||
{congesAnticipes.congesPayes.acquisTotalePrevu}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-t pt-2">
|
|
||||||
<span className="text-gray-600">Solde actuel:</span>
|
|
||||||
<span className="font-bold text-cyan-600">
|
|
||||||
{congesAnticipes.congesPayes.soldeActuel}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between bg-gray-100 -mx-2 px-2 py-2 rounded">
|
|
||||||
<span className="font-semibold text-gray-900">Anticipation possible:</span>
|
|
||||||
<span className="font-bold text-gray-700 text-lg">
|
|
||||||
{congesAnticipes.congesPayes.limiteAnticipe}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between bg-gradient-to-r from-gray-200 to-slate-200 -mx-2 px-2 py-2 rounded font-bold">
|
|
||||||
<span className="text-gray-900">TOTAL DISPONIBLE:</span>
|
|
||||||
<span className="text-gray-700 text-lg">
|
|
||||||
{congesAnticipes.congesPayes.totalDisponible}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 text-xs text-gray-700 bg-gray-50 p-2 rounded">
|
|
||||||
💡 {congesAnticipes.congesPayes.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* RTT Anticipés */}
|
|
||||||
{congesAnticipes.rtt && (
|
|
||||||
<div className="bg-white rounded-lg p-4 border-2 border-gray-200 shadow-sm">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h5 className="font-semibold text-gray-900 flex items-center gap-2">
|
|
||||||
<Clock className="w-5 h-5" />
|
|
||||||
RTT
|
|
||||||
</h5>
|
|
||||||
{congesAnticipes.rtt.limiteAnticipe > 0 && (
|
|
||||||
<span className="bg-green-100 text-green-800 text-xs font-bold px-2 py-1 rounded-full">
|
|
||||||
Disponible
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">Acquis actuellement:</span>
|
|
||||||
<span className="font-semibold text-gray-900">
|
|
||||||
{congesAnticipes.rtt.acquisActuelle}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">Prévision fin année:</span>
|
|
||||||
<span className="font-semibold text-gray-900">
|
|
||||||
{congesAnticipes.rtt.acquisTotalePrevu}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-t pt-2">
|
|
||||||
<span className="text-gray-600">Solde actuel:</span>
|
|
||||||
<span className="font-bold text-green-600">
|
|
||||||
{congesAnticipes.rtt.soldeActuel}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between bg-gray-100 -mx-2 px-2 py-2 rounded">
|
|
||||||
<span className="font-semibold text-gray-900">Anticipation possible:</span>
|
|
||||||
<span className="font-bold text-gray-700 text-lg">
|
|
||||||
{congesAnticipes.rtt.limiteAnticipe}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between bg-gradient-to-r from-gray-200 to-slate-200 -mx-2 px-2 py-2 rounded font-bold">
|
|
||||||
<span className="text-gray-900">TOTAL DISPONIBLE:</span>
|
|
||||||
<span className="text-gray-700 text-lg">
|
|
||||||
{congesAnticipes.rtt.totalDisponible}j
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 text-xs text-gray-700 bg-gray-50 p-2 rounded">
|
|
||||||
💡 {congesAnticipes.rtt.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Règles d'anticipation */}
|
|
||||||
<div className="bg-gray-100 border border-gray-300 rounded-lg p-4">
|
|
||||||
<h5 className="font-semibold text-gray-900 mb-2 flex items-center gap-2">
|
|
||||||
<AlertCircle className="w-4 h-4" />
|
|
||||||
Règles d'anticipation
|
|
||||||
</h5>
|
|
||||||
<ul className="text-sm text-gray-800 space-y-1">
|
|
||||||
<li>• CP : Maximum {congesAnticipes.regles.cpMaxAnnuel}j par exercice (jusqu'au 31 mai)</li>
|
|
||||||
<li>• RTT : Maximum {congesAnticipes.regles.rttMaxAnnuel}j par an (jusqu'au 31 décembre)</li>
|
|
||||||
<li>• L'anticipation est basée sur l'acquisition future prévue</li>
|
|
||||||
<li>• Vous pouvez poser des congés avant de les avoir acquis</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Soldes actuels */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div className="bg-cyan-50 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-cyan-700 mb-1">Congés Payés</p>
|
|
||||||
<p className="text-3xl font-bold text-cyan-900">
|
|
||||||
{detailedCounters.totalDisponible.cp.toFixed(2)}
|
|
||||||
<span className="text-lg">j</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-cyan-600 mt-1">
|
|
||||||
N-1: {(detailedCounters.cpN1?.solde || 0).toFixed(2)}j +
|
|
||||||
N: {(detailedCounters.cpN?.solde || 0).toFixed(2)}j
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{detailedCounters.user.role !== 'Apprenti' && (
|
|
||||||
<div className="bg-green-50 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-green-700 mb-1">RTT</p>
|
|
||||||
<p className="text-3xl font-bold text-green-900">
|
|
||||||
{detailedCounters.totalDisponible.rtt.toFixed(2)}
|
|
||||||
<span className="text-lg">j</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-green-600 mt-1">
|
|
||||||
Année {detailedCounters.anneeRTT} • {getTypeContratLabel(detailedCounters.user.typeContrat)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{detailedCounters.recupN && detailedCounters.recupN.solde > 0 && (
|
|
||||||
<div className="bg-purple-50 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-purple-700 mb-1">Récupération</p>
|
|
||||||
<p className="text-3xl font-bold text-purple-900">
|
|
||||||
{detailedCounters.recupN.solde.toFixed(2)}
|
|
||||||
<span className="text-lg">j</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-purple-600 mt-1">
|
|
||||||
Samedis travaillés
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reste du code identique... */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
|
|
||||||
|
{/* RACCOURCI COMPTE-RENDU ACTIVITÉS (si forfait jour) */}
|
||||||
|
{(user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfait_jour') && (
|
||||||
|
<div
|
||||||
|
onClick={() => navigate('/compte-rendu-activites')}
|
||||||
|
className="bg-gradient-to-r from-purple-500 to-purple-600 rounded-xl shadow-md p-6 text-white cursor-pointer hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-2 flex items-center gap-2">
|
||||||
|
<FileText className="w-6 h-6" />
|
||||||
|
Compte-Rendu d'Activités
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm opacity-90">
|
||||||
|
Suivez vos jours travaillés et repos obligatoires
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CARTES DES COMPTEURS (CP N-1, CP N, RTT, Récup) */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
{detailedCounters.cpN1 && (
|
{detailedCounters.cpN1 && (
|
||||||
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
|
<div data-tour="cp-n-1" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||||
|
{isUpdatingCounters && (
|
||||||
|
<div className="absolute top-2 right-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full animate-pulse z-10">
|
||||||
|
🔄 Mise à jour...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-4 text-white">
|
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-4 text-white">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold">CP N-1</h3>
|
<h3 className="text-lg font-bold">CP N-1</h3>
|
||||||
<p className="text-sm opacity-90">Reporté | Exercice {detailedCounters.cpN1.exercice}</p>
|
<p className="text-sm opacity-90">
|
||||||
|
Congés acquis du 1er juin {parseInt(detailedCounters.cpN1.exercice.split('-')[0])} au 31 mai {parseInt(detailedCounters.cpN1.exercice.split('-')[1])}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Calendar className="w-8 h-8 opacity-80" />
|
<Calendar className="w-8 h-8 opacity-80" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5 space-y-3">
|
<div className="p-5 space-y-3">
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
||||||
<span className="text-sm font-medium text-gray-600">Reporté initial</span>
|
|
||||||
<span className="text-lg font-bold text-gray-900">{detailedCounters.cpN1.reporte.toFixed(2)}j</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
||||||
<span className="text-sm font-medium text-gray-600">Consommé</span>
|
|
||||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.cpN1.pris.toFixed(2)}j</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center py-3 bg-blue-50 rounded-lg px-3 mt-3">
|
<div className="flex justify-between items-center py-3 bg-blue-50 rounded-lg px-3 mt-3">
|
||||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||||
<span className="text-2xl font-bold text-blue-600">{detailedCounters.cpN1.solde.toFixed(2)}j</span>
|
<span className="text-2xl font-bold text-blue-600">{detailedCounters.cpN1.solde.toFixed(2)}j</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
|
<span className="text-sm font-medium text-gray-600">Acquis </span>
|
||||||
|
<span className="text-lg font-bold text-gray-900">{detailedCounters.cpN1.reporte.toFixed(2)}j</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
|
<span className="text-sm font-medium text-gray-600">Consommé(s)</span>
|
||||||
|
<span className="text-lg font-bold text-red-600">-{detailedCounters.cpN1.pris.toFixed(2)}j</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg mt-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs text-amber-800">
|
||||||
|
<strong>À solder avant le 31/05/{parseInt(detailedCounters.cpN1.exercice.split('-')[1]) + 1}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{detailedCounters.cpN && (
|
{detailedCounters.cpN && (
|
||||||
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
|
<div data-tour="cp-n" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||||
|
{isUpdatingCounters && (
|
||||||
|
<div className="absolute top-2 right-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full animate-pulse z-10">
|
||||||
|
🔄 Mise à jour...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-gradient-to-r from-cyan-500 to-cyan-600 p-4 text-white">
|
<div className="bg-gradient-to-r from-cyan-500 to-cyan-600 p-4 text-white">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold">CP N</h3>
|
<h3 className="text-lg font-bold">CP N</h3>
|
||||||
<p className="text-sm opacity-90">Exercice {detailedCounters.cpN.exercice}</p>
|
<p className="text-sm opacity-90">
|
||||||
|
Congés acquis du 1er juin {parseInt(detailedCounters.cpN.exercice.split('-')[0])} au 31 mai {parseInt(detailedCounters.cpN.exercice.split('-')[1])}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Calendar className="w-8 h-8 opacity-80" />
|
<Calendar className="w-8 h-8 opacity-80" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5 space-y-3">
|
<div className="p-5 space-y-3">
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
||||||
<span className="text-sm font-medium text-gray-600">Acquis ({detailedCounters.cpN.moisTravailles.toFixed(1)}/12)</span>
|
|
||||||
<span className="text-lg font-bold text-green-600">+{detailedCounters.cpN.acquis.toFixed(2)}j</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
||||||
<span className="text-sm font-medium text-gray-600">Consommé</span>
|
|
||||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.cpN.pris.toFixed(2)}j</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center py-3 bg-cyan-50 rounded-lg px-3 mt-3">
|
<div className="flex justify-between items-center py-3 bg-cyan-50 rounded-lg px-3 mt-3">
|
||||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||||
<span className="text-2xl font-bold text-cyan-600">{detailedCounters.cpN.solde.toFixed(2)}j</span>
|
<span className="text-2xl font-bold text-cyan-600">{detailedCounters.cpN.solde.toFixed(2)}j</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 pt-2">
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
Reste à acquérir : {detailedCounters.cpN.joursRestantsAAcquerir.toFixed(2)}j
|
<span className="text-sm font-medium text-gray-600">Acquis </span>
|
||||||
|
<span className="text-lg font-bold text-green-600">+{detailedCounters.cpN.acquis.toFixed(2)}j</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
|
<span className="text-sm font-medium text-gray-600">Consommé(s)</span>
|
||||||
|
<span className="text-lg font-bold text-red-600">-{detailedCounters.cpN.pris.toFixed(2)}j</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{detailedCounters.cpN.solde > 0 && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-blue-50 border border-blue-200 rounded-lg mt-2">
|
||||||
|
<Info className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs text-blue-800">
|
||||||
|
<strong>À solder avant le 31/05/{parseInt(detailedCounters.cpN.exercice.split('-')[1]) + 1}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{detailedCounters.rttN && detailedCounters.user.role !== 'Apprenti' && (
|
{detailedCounters.rttN && detailedCounters.user.role !== 'Apprenti' && (
|
||||||
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
|
<div data-tour="rtt" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||||
|
{isUpdatingCounters && (
|
||||||
|
<div className="absolute top-2 right-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full animate-pulse z-10">
|
||||||
|
🔄 Mise à jour...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-gradient-to-r from-green-500 to-green-600 p-4 text-white">
|
<div className="bg-gradient-to-r from-green-500 to-green-600 p-4 text-white">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -832,49 +940,58 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5 space-y-3">
|
<div className="p-5 space-y-3">
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
||||||
<span className="text-sm font-medium text-gray-600">Acquis ({detailedCounters.rttN.moisTravailles.toFixed(1)}/12)</span>
|
|
||||||
<span className="text-lg font-bold text-green-600">+{detailedCounters.rttN.acquis.toFixed(2)}j</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
|
||||||
<span className="text-sm font-medium text-gray-600">Consommé</span>
|
|
||||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.rttN.pris.toFixed(2)}j</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center py-3 bg-green-50 rounded-lg px-3 mt-3">
|
<div className="flex justify-between items-center py-3 bg-green-50 rounded-lg px-3 mt-3">
|
||||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||||
<span className="text-2xl font-bold text-green-600">{detailedCounters.rttN.solde.toFixed(2)}j</span>
|
<span className="text-2xl font-bold text-green-600">{detailedCounters.rttN.solde.toFixed(2)}j</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 pt-2">
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
Reste à acquérir : {detailedCounters.rttN.joursRestantsAAcquerir.toFixed(2)}j
|
<span className="text-sm font-medium text-gray-600">Acquis </span>
|
||||||
|
<span className="text-lg font-bold text-green-600">+{detailedCounters.rttN.acquis.toFixed(2)}j</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
|
<span className="text-sm font-medium text-gray-600">Consommé(s)</span>
|
||||||
|
<span className="text-lg font-bold text-red-600">-{detailedCounters.rttN.pris.toFixed(2)}j</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-green-50 border border-green-200 rounded-lg mt-2">
|
||||||
|
<Info className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs text-green-800">
|
||||||
|
<strong>À consommer avant le 31/12/{detailedCounters.rttN.annee}</strong>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{detailedCounters.recupN && (
|
{detailedCounters.recupN && (
|
||||||
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
|
<div data-tour="recup" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||||
|
{isUpdatingCounters && (
|
||||||
|
<div className="absolute top-2 right-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full animate-pulse z-10">
|
||||||
|
🔄 Mise à jour...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-gradient-to-r from-purple-500 to-purple-600 p-4 text-white">
|
<div className="bg-gradient-to-r from-purple-500 to-purple-600 p-4 text-white">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold">Récupération {detailedCounters.recupN.annee}</h3>
|
<h3 className="text-lg font-bold">Récupérations {detailedCounters.recupN.annee}</h3>
|
||||||
<p className="text-sm opacity-90">Samedis travaillés</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Calendar className="w-8 h-8 opacity-80" />
|
<Calendar className="w-8 h-8 opacity-80" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5 space-y-3">
|
<div className="p-5 space-y-3">
|
||||||
|
<div className="flex justify-between items-center py-3 bg-purple-50 rounded-lg px-3 mt-3">
|
||||||
|
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||||
|
<span className="text-2xl font-bold text-purple-600">{detailedCounters.recupN.solde.toFixed(2)}j</span>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
<span className="text-sm font-medium text-gray-600">Jours accumulés</span>
|
<span className="text-sm font-medium text-gray-600">Jours accumulés</span>
|
||||||
<span className="text-lg font-bold text-green-600">+{detailedCounters.recupN.acquis.toFixed(2)}j</span>
|
<span className="text-lg font-bold text-green-600">+{detailedCounters.recupN.acquis.toFixed(2)}j</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||||
<span className="text-sm font-medium text-gray-600">Consommé</span>
|
<span className="text-sm font-medium text-gray-600">Consommé(s)</span>
|
||||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.recupN.pris.toFixed(2)}j</span>
|
<span className="text-lg font-bold text-red-600">-{detailedCounters.recupN.pris.toFixed(2)}j</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center py-3 bg-purple-50 rounded-lg px-3 mt-3">
|
|
||||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
|
||||||
<span className="text-2xl font-bold text-purple-600">{detailedCounters.recupN.solde.toFixed(2)}j</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500 pt-2">
|
<div className="text-xs text-gray-500 pt-2">
|
||||||
{detailedCounters.recupN.message}
|
{detailedCounters.recupN.message}
|
||||||
</div>
|
</div>
|
||||||
@@ -882,18 +999,21 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{detailedCounters.user.role === 'Apprenti' && (
|
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-blue-800">
|
|
||||||
<strong>ℹ️ Information :</strong> En tant qu'apprenti, vous ne bénéficiez pas de jours RTT.
|
|
||||||
Vous disposez uniquement de vos congés payés.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 🎯 BOUTON AIDE POUR RELANCER LE TUTORIEL */}
|
||||||
|
<button
|
||||||
|
onClick={handleRestartTutorial}
|
||||||
|
className="fixed bottom-20 right-4 bg-cyan-600 text-white shadow-lg hover:bg-cyan-700 transition-all flex items-center gap-2 z-40 group overflow-hidden rounded-full hover:rounded-lg hover:px-4 px-3 py-3"
|
||||||
|
title="Relancer le tutoriel"
|
||||||
|
>
|
||||||
|
<HelpCircle className="w-5 h-5 flex-shrink-0" />
|
||||||
|
<span className="max-w-0 group-hover:max-w-xs overflow-hidden transition-all duration-300 text-sm font-medium whitespace-nowrap">
|
||||||
|
Aide
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{showNewRequestModal && detailedCounters && (
|
{showNewRequestModal && detailedCounters && (
|
||||||
<NewLeaveRequestModal
|
<NewLeaveRequestModal
|
||||||
onClose={() => setShowNewRequestModal(false)}
|
onClose={() => setShowNewRequestModal(false)}
|
||||||
@@ -906,9 +1026,10 @@ const Dashboard = () => {
|
|||||||
availableRTT_N1: 0,
|
availableRTT_N1: 0,
|
||||||
availableABS: 0,
|
availableABS: 0,
|
||||||
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
||||||
availableRTT: detailedCounters.rttN?.solde || 0
|
availableRTT: detailedCounters.rttN?.solde || 0,
|
||||||
|
availableRecup: detailedCounters?.recupN?.solde || 0,
|
||||||
|
availableRecup_N: detailedCounters?.recupN?.solde || 0
|
||||||
}}
|
}}
|
||||||
// ⭐ PASSER LES DONNÉES D'ANTICIPATION
|
|
||||||
congesAnticipes={congesAnticipes}
|
congesAnticipes={congesAnticipes}
|
||||||
accessToken={graphToken}
|
accessToken={graphToken}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const EmployeeDetails = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// 1️⃣ Données employé (avec compteurs inclus)
|
// 1️⃣ Données employé (avec compteurs inclus)
|
||||||
const resEmployee = await fetch(`http://localhost:3000/getEmploye?id=${id}`);
|
const resEmployee = await fetch(`/getEmploye?id=${id}`);
|
||||||
const dataEmployee = await resEmployee.json();
|
const dataEmployee = await resEmployee.json();
|
||||||
console.log("Réponse API employé:", dataEmployee);
|
console.log("Réponse API employé:", dataEmployee);
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ const EmployeeDetails = () => {
|
|||||||
setEmployee(dataEmployee.employee);
|
setEmployee(dataEmployee.employee);
|
||||||
|
|
||||||
// 2️⃣ Historique des demandes
|
// 2️⃣ Historique des demandes
|
||||||
const resRequests = await fetch(`http://localhost:3000/getEmployeRequest?id=${id}`);
|
const resRequests = await fetch(`/getEmployeRequest?id=${id}`);
|
||||||
const dataRequests = await resRequests.json();
|
const dataRequests = await resRequests.json();
|
||||||
|
|
||||||
if (dataRequests.success) {
|
if (dataRequests.success) {
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const Login = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirection vers le dashboard
|
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -67,7 +66,6 @@ const Login = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex flex-col lg:flex-row">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex flex-col lg:flex-row">
|
||||||
{/* Image côté gauche */}
|
{/* Image côté gauche */}
|
||||||
<div className="h-32 lg:h-auto lg:flex lg:w-1/2 bg-cover bg-center"
|
<div className="h-32 lg:h-auto lg:flex lg:w-1/2 bg-cover bg-center"
|
||||||
@@ -91,8 +89,9 @@ const Login = () => {
|
|||||||
GESTION DES TEMPS ET DES ACTIVITÉS
|
GESTION DES TEMPS ET DES ACTIVITÉS
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bouton Office 365 */}
|
{/* Bouton Office 365 */}
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<button
|
<button
|
||||||
data-testid="o365-login-btn"
|
data-testid="o365-login-btn"
|
||||||
onClick={handleO365Login}
|
onClick={handleO365Login}
|
||||||
@@ -113,6 +112,63 @@ const Login = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Séparateur */}
|
||||||
|
<div className="flex items-center my-4">
|
||||||
|
<div className="flex-1 h-px bg-gray-200" />
|
||||||
|
<span className="px-3 text-xs text-gray-500">ou connexion locale</span>
|
||||||
|
<div className="flex-1 h-px bg-gray-200" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Formulaire local email/mot de passe */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email professionnel
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
|
placeholder="prenom.nom@ensup.fr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 pr-10"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 px-3 text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
{showPassword ? 'Masquer' : 'Afficher'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading && authMethod === 'local'}
|
||||||
|
className="w-full bg-indigo-600 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isLoading && authMethod === 'local'
|
||||||
|
? 'Connexion...'
|
||||||
|
: 'Se connecter'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
{/* Message d'erreur */}
|
{/* Message d'erreur */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg mt-4">
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg mt-4">
|
||||||
@@ -135,8 +191,8 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Sidebar from "../components/Sidebar";
|
import Sidebar from "../components/Sidebar";
|
||||||
|
import GlobalTutorial from '../components/GlobalTutorial';
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
@@ -30,6 +31,7 @@ const Manager = () => {
|
|||||||
const [comment, setComment] = useState("");
|
const [comment, setComment] = useState("");
|
||||||
const [isValidating, setIsValidating] = useState(false);
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.id) fetchTeamData();
|
if (user?.id) fetchTeamData();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
@@ -51,7 +53,7 @@ const Manager = () => {
|
|||||||
|
|
||||||
const fetchTeamMembers = async () => {
|
const fetchTeamMembers = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://localhost:3000/getTeamMembers?manager_id=${user.id}`);
|
const res = await fetch(`/getTeamMembers?manager_id=${user.id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) setTeamMembers(data.team_members || []);
|
if (data.success) setTeamMembers(data.team_members || []);
|
||||||
else setTeamMembers([]);
|
else setTeamMembers([]);
|
||||||
@@ -62,7 +64,7 @@ const Manager = () => {
|
|||||||
|
|
||||||
const fetchPendingRequests = async () => {
|
const fetchPendingRequests = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://localhost:3000/getPendingRequests?manager_id=${user.id}`);
|
const res = await fetch(`/getPendingRequests?manager_id=${user.id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) setPendingRequests(data.requests || []);
|
if (data.success) setPendingRequests(data.requests || []);
|
||||||
else setPendingRequests([]);
|
else setPendingRequests([]);
|
||||||
@@ -73,7 +75,7 @@ const Manager = () => {
|
|||||||
|
|
||||||
const fetchAllTeamRequests = async () => {
|
const fetchAllTeamRequests = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://localhost:3000/getAllTeamRequests?SuperieurId=${user.id}`);
|
const res = await fetch(`/getAllTeamRequests?SuperieurId=${user.id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) setAllRequests(data.requests || []);
|
if (data.success) setAllRequests(data.requests || []);
|
||||||
else setAllRequests([]);
|
else setAllRequests([]);
|
||||||
@@ -113,7 +115,7 @@ const Manager = () => {
|
|||||||
try {
|
try {
|
||||||
setIsValidating(true); // ✅ Maintenant défini
|
setIsValidating(true); // ✅ Maintenant défini
|
||||||
|
|
||||||
const response = await fetch('http://localhost:3000/validateRequest', {
|
const response = await fetch('/validateRequest', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -353,7 +355,7 @@ const Manager = () => {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{!isEmployee && (
|
{!isEmployee && (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100" data-tour="demandes-attente">
|
||||||
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
||||||
<Clock className="w-5 h-5 text-yellow-600" />
|
<Clock className="w-5 h-5 text-yellow-600" />
|
||||||
<h2 className="font-semibold text-gray-900">Demandes en attente ({pendingRequests.length})</h2>
|
<h2 className="font-semibold text-gray-900">Demandes en attente ({pendingRequests.length})</h2>
|
||||||
@@ -382,14 +384,14 @@ const Manager = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => openValidationModal(r, "approve")}
|
onClick={() => openValidationModal(r, "approve")}
|
||||||
className="flex-1 bg-green-600 text-white px-3 py-2 rounded-lg hover:bg-green-700 text-sm"
|
className="flex-1 bg-green-600 text-white px-3 py-2 rounded-lg hover:bg-green-700 text-sm"
|
||||||
>
|
data-tour="approuver-btn" >
|
||||||
<CheckCircle className="w-4 h-4 inline mr-1" />
|
<CheckCircle className="w-4 h-4 inline mr-1" />
|
||||||
Approuver
|
Approuver
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openValidationModal(r, "reject")}
|
onClick={() => openValidationModal(r, "reject")}
|
||||||
className="flex-1 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 text-sm"
|
className="flex-1 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 text-sm"
|
||||||
>
|
data-tour="refuser-btn" >
|
||||||
<XCircle className="w-4 h-4 inline mr-1" />
|
<XCircle className="w-4 h-4 inline mr-1" />
|
||||||
Refuser
|
Refuser
|
||||||
</button>
|
</button>
|
||||||
@@ -401,7 +403,7 @@ const Manager = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 ${isEmployee ? "lg:col-span-2" : ""}`}>
|
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 ${isEmployee ? "lg:col-span-2" : ""}`} data-tour="mon-equipe">
|
||||||
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
||||||
<Users className="w-5 h-5 text-blue-600" />
|
<Users className="w-5 h-5 text-blue-600" />
|
||||||
<h2 className="font-semibold text-gray-900">Mon équipe ({teamMembers.length})</h2>
|
<h2 className="font-semibold text-gray-900">Mon équipe ({teamMembers.length})</h2>
|
||||||
@@ -415,7 +417,7 @@ const Manager = () => {
|
|||||||
key={m.id}
|
key={m.id}
|
||||||
onClick={() => navigate(`/employee/${m.id}`)}
|
onClick={() => navigate(`/employee/${m.id}`)}
|
||||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer transition"
|
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer transition"
|
||||||
>
|
data-tour="membre-equipe" >
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
<span className="text-blue-600 font-medium text-sm">
|
<span className="text-blue-600 font-medium text-sm">
|
||||||
@@ -445,7 +447,7 @@ const Manager = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isEmployee && (
|
{!isEmployee && (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 mt-6">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 mt-6" data-tour="historique-demandes">
|
||||||
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
||||||
<FileText className="w-5 h-5 text-gray-600" />
|
<FileText className="w-5 h-5 text-gray-600" />
|
||||||
<h2 className="font-semibold text-gray-900">Historique des demandes ({allRequests.length})</h2>
|
<h2 className="font-semibold text-gray-900">Historique des demandes ({allRequests.length})</h2>
|
||||||
@@ -473,10 +475,10 @@ const Manager = () => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{r.file && (
|
{r.file && (
|
||||||
<div className="text-sm mt-1">
|
<div className="text-sm mt-1" data-tour="document-joint">
|
||||||
<p className="text-gray-500">Document joint</p>
|
<p className="text-gray-500">Document joint</p>
|
||||||
<a
|
<a
|
||||||
href={`http://localhost:3000/uploads/${r.file}`}
|
href={`/uploads/${r.file}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:underline flex items-center gap-1 mt-1"
|
className="text-blue-600 hover:underline flex items-center gap-1 mt-1"
|
||||||
@@ -493,7 +495,8 @@ const Manager = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<GlobalTutorial userId={user?.id} userRole={user?.role} />
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
|||||||
import EditLeaveRequestModal from '../components/EditLeaveRequestModal';
|
import EditLeaveRequestModal from '../components/EditLeaveRequestModal';
|
||||||
import { useMsal } from "@azure/msal-react";
|
import { useMsal } from "@azure/msal-react";
|
||||||
import MedicalDocuments from '../components/MedicalDocuments';
|
import MedicalDocuments from '../components/MedicalDocuments';
|
||||||
|
import Joyride, { STATUS } from 'react-joyride';
|
||||||
|
|
||||||
|
|
||||||
const Requests = () => {
|
const Requests = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -45,6 +47,55 @@ const Requests = () => {
|
|||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [requestToDelete, setRequestToDelete] = useState(null);
|
const [requestToDelete, setRequestToDelete] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
// 🎯 STATES POUR LE TUTORIEL
|
||||||
|
const [runTour, setRunTour] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 🎯 DÉCLENCHER LE TUTORIEL À CHAQUE FOIS
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId && !isLoading) {
|
||||||
|
setTimeout(() => setRunTour(true), 1500);
|
||||||
|
}
|
||||||
|
}, [userId, isLoading]);
|
||||||
|
|
||||||
|
// 🎯 DÉFINITION DES ÉTAPES DU TUTORIEL
|
||||||
|
const tourSteps = [
|
||||||
|
{
|
||||||
|
target: '[data-tour="nouvelle-demande"]',
|
||||||
|
content: '➕ Créez une nouvelle demande de congé en cliquant ici.',
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="recherche"]',
|
||||||
|
content: '🔍 Recherchez vos demandes par type ou statut.',
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="filtres"]',
|
||||||
|
content: '🎯 Filtrez vos demandes par statut ou type de congé.',
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '[data-tour="liste-demandes"]',
|
||||||
|
content: '📋 Consultez la liste de toutes vos demandes ici.',
|
||||||
|
placement: 'top',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🎯 GÉRER LA FIN DU TOUR
|
||||||
|
const handleJoyrideCallback = (data) => {
|
||||||
|
const { status } = data;
|
||||||
|
const finishedStatuses = [STATUS.FINISHED, STATUS.SKIPPED];
|
||||||
|
if (finishedStatuses.includes(status)) {
|
||||||
|
setRunTour(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accounts.length > 0) {
|
if (accounts.length > 0) {
|
||||||
const request = {
|
const request = {
|
||||||
@@ -64,7 +115,7 @@ const Requests = () => {
|
|||||||
|
|
||||||
const fetchDetailedCounters = async () => {
|
const fetchDetailedCounters = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:3000/getDetailedLeaveCounters?user_id=${userId}`);
|
const response = await fetch(`/getDetailedLeaveCounters?user_id=${userId}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -79,7 +130,7 @@ const Requests = () => {
|
|||||||
|
|
||||||
const fetchAllRequests = async () => {
|
const fetchAllRequests = async () => {
|
||||||
try {
|
try {
|
||||||
const url = `http://localhost:3000/getRequests?user_id=${userId}`;
|
const url = `/getRequests?user_id=${userId}`;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
let data;
|
let data;
|
||||||
@@ -144,56 +195,157 @@ const Requests = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ NOUVELLE FONCTION : Supprimer une demande
|
// ⭐ NOUVELLE FONCTION : Supprimer une demande
|
||||||
const handleDeleteRequest = (request) => {
|
// ⭐ NOUVELLE FONCTION : Annuler une demande (En attente OU Validée, si date future)
|
||||||
setRequestToDelete(request);
|
const handleDeleteRequest = async (requestId) => {
|
||||||
setShowDeleteConfirm(true);
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
console.log('🗑️ Début annulation, ID:', requestId);
|
||||||
|
|
||||||
|
// Chercher la demande dans allRequests
|
||||||
|
let request = allRequests.find(r => r.id === requestId);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
console.log('⚠️ Demande non trouvée dans l\'état local, récupération via API...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/getRequests?user_id=${user.id}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.requests) {
|
||||||
|
request = result.requests.find(r => r.id === requestId);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur récupération demande:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
showToast('❌ Demande introuvable', 'error');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Demande trouvée:', request);
|
||||||
|
|
||||||
|
// Vérifier la date de début
|
||||||
|
const dateDebut = new Date(request.startDate);
|
||||||
|
const aujourdhui = new Date();
|
||||||
|
aujourdhui.setHours(0, 0, 0, 0);
|
||||||
|
dateDebut.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
console.log('📅 Date début:', dateDebut.toLocaleDateString('fr-FR'));
|
||||||
|
console.log('📅 Aujourd\'hui:', aujourdhui.toLocaleDateString('fr-FR'));
|
||||||
|
|
||||||
|
if (dateDebut <= aujourdhui) {
|
||||||
|
showToast(
|
||||||
|
`❌ Impossible d'annuler : la date de début (${dateDebut.toLocaleDateString('fr-FR')}) est déjà passée ou c'est aujourd'hui`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ CONFIRMATION AVEC MODAL AU LIEU DE alert()
|
||||||
|
setRequestToDelete(request);
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur annulation:', error);
|
||||||
|
showToast(`Erreur: ${error.message}`, 'error');
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Fonction helper pour formater les dates
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('fr-FR');
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⭐ NOUVELLE FONCTION : Confirmer la suppression
|
|
||||||
|
// ⭐ NOUVELLE FONCTION : Confirmer la suppression (sans setDeletedRequests)
|
||||||
const confirmDeleteRequest = async () => {
|
const confirmDeleteRequest = async () => {
|
||||||
if (!requestToDelete) return;
|
if (!requestToDelete) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:3000/deleteRequest', {
|
const requestData = {
|
||||||
|
requestId: requestToDelete.id,
|
||||||
|
userId: userId,
|
||||||
|
userEmail: user.email,
|
||||||
|
userName: `${user.prenom} ${user.nom}`,
|
||||||
|
accessToken: graphToken
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 Envoi requête:', requestData);
|
||||||
|
|
||||||
|
const response = await fetch('/deleteRequest', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify(requestData)
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
requestId: requestToDelete.id,
|
|
||||||
userId: userId,
|
|
||||||
userEmail: user.email,
|
|
||||||
userName: `${user.prenom} ${user.nom}`,
|
|
||||||
accessToken: graphToken
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
if (data.success) {
|
|
||||||
showToast('✅ Demande supprimée avec succès', 'success');
|
|
||||||
refreshAllData();
|
|
||||||
setShowDeleteConfirm(false);
|
|
||||||
setRequestToDelete(null);
|
|
||||||
if (selectedRequest?.id === requestToDelete.id) {
|
|
||||||
setSelectedRequest(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showToast(`❌ Erreur : ${data.message}`, 'error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('📥 Réponse API:', result);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// ✅ Succès
|
||||||
|
showToast('✅ Demande annulée avec succès', 'success');
|
||||||
|
|
||||||
|
if (result.counterRestored && result.repartition) {
|
||||||
|
// Afficher les détails de la restauration
|
||||||
|
const repartitionText = result.repartition
|
||||||
|
.map(r => `${r.type}: ${r.jours}j${r.periode !== 'Journée entière' ? ` (${r.periode})` : ''}`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
showToast(`📊 Compteurs restaurés: ${repartitionText}`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.emailsSent) {
|
||||||
|
if (result.emailsSent.collaborateur) {
|
||||||
|
showToast('📧 Email de confirmation envoyé', 'info');
|
||||||
|
}
|
||||||
|
if (result.emailsSent.manager) {
|
||||||
|
showToast('📧 Manager notifié par email', 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ Rafraîchir les données
|
||||||
|
await refreshAllData();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// ❌ Erreur
|
||||||
|
showToast(result.message || 'Erreur lors de l\'annulation', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur suppression:', error);
|
console.error('❌ Erreur annulation:', error);
|
||||||
showToast('❌ Erreur lors de la suppression', 'error');
|
showToast(`Erreur serveur: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setRequestToDelete(null);
|
||||||
|
|
||||||
|
// Fermer les détails si c'était la demande affichée
|
||||||
|
if (selectedRequest?.id === requestToDelete?.id) {
|
||||||
|
setSelectedRequest(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connexion SSE
|
// Connexion SSE
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
console.log('🔌 Connexion SSE au serveur collaborateurs...');
|
console.log('🔌 Connexion SSE au serveur collaborateurs...');
|
||||||
|
|
||||||
const eventSource = new EventSource(`http://localhost:3000/api/events/collaborateur?user_id=${userId}`);
|
const eventSource = new EventSource(`/api/events/collaborateur?user_id=${userId}`);
|
||||||
|
|
||||||
eventSource.onopen = () => {
|
eventSource.onopen = () => {
|
||||||
console.log('✅ SSE connecté');
|
console.log('✅ SSE connecté');
|
||||||
@@ -275,7 +427,7 @@ const Requests = () => {
|
|||||||
|
|
||||||
setFilteredRequests(filtered);
|
setFilteredRequests(filtered);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [allRequests, searchTerm, statusFilter, typeFilter]);
|
}, [allRequests, searchTerm, statusFilter, typeFilter]);
|
||||||
|
|
||||||
const indexOfLastRequest = currentPage * requestsPerPage;
|
const indexOfLastRequest = currentPage * requestsPerPage;
|
||||||
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
|
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
|
||||||
@@ -289,6 +441,7 @@ const Requests = () => {
|
|||||||
case 'En attente': return 'bg-yellow-100 text-yellow-800';
|
case 'En attente': return 'bg-yellow-100 text-yellow-800';
|
||||||
case 'Validée': return 'bg-green-100 text-green-800';
|
case 'Validée': return 'bg-green-100 text-green-800';
|
||||||
case 'Refusée': return 'bg-red-100 text-red-800';
|
case 'Refusée': return 'bg-red-100 text-red-800';
|
||||||
|
case 'Annulée': return 'bg-gray-100 text-gray-800'; // ⭐ AJOUT
|
||||||
default: return 'bg-gray-100 text-gray-800';
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -307,6 +460,32 @@ const Requests = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
|
||||||
|
{/* 🎯 TUTORIEL INTERACTIF */}
|
||||||
|
<Joyride
|
||||||
|
steps={tourSteps}
|
||||||
|
run={runTour}
|
||||||
|
continuous
|
||||||
|
showProgress={false}
|
||||||
|
showSkipButton
|
||||||
|
callback={handleJoyrideCallback}
|
||||||
|
styles={{ options: { primaryColor: '#0891b2', zIndex: 10000 } }}
|
||||||
|
tooltipComponent={({ continuous, index, step, backProps, primaryProps, skipProps, tooltipProps, size }) => (
|
||||||
|
<div {...tooltipProps} style={{ backgroundColor: 'white', borderRadius: '12px', padding: '20px', maxWidth: '350px', boxShadow: '0 10px 25px rgba(0,0,0,0.15)', fontSize: '14px' }}>
|
||||||
|
<div style={{ marginBottom: '15px', color: '#374151' }}>{step.content}</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingTop: '12px', borderTop: '1px solid #e5e7eb' }}>
|
||||||
|
<span style={{ fontSize: '13px', color: '#6b7280', fontWeight: '500' }}>Étape {index + 1} sur {size}</span>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
{index > 0 && <button {...backProps} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #d1d5db', backgroundColor: 'white', color: '#6b7280', cursor: 'pointer', fontSize: '13px', fontWeight: '500' }}>Retour</button>}
|
||||||
|
{continuous && index < size - 1 && <button {...primaryProps} style={{ padding: '6px 16px', borderRadius: '6px', border: 'none', backgroundColor: '#0891b2', color: 'white', cursor: 'pointer', fontSize: '13px', fontWeight: '500' }}>Suivant</button>}
|
||||||
|
{(!continuous || index === size - 1) && <button {...primaryProps} style={{ padding: '6px 16px', borderRadius: '6px', border: 'none', backgroundColor: '#0891b2', color: 'white', cursor: 'pointer', fontSize: '13px', fontWeight: '500' }}>Terminer</button>}
|
||||||
|
<button {...skipProps} style={{ padding: '6px 10px', borderRadius: '6px', border: 'none', backgroundColor: 'transparent', color: '#9ca3af', cursor: 'pointer', fontSize: '12px' }}>Passer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
||||||
|
|
||||||
{/* Toast container */}
|
{/* Toast container */}
|
||||||
@@ -327,30 +506,72 @@ const Requests = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal de confirmation de suppression */}
|
{/* Modal de confirmation de suppression */}
|
||||||
{showDeleteConfirm && (
|
{showDeleteConfirm && requestToDelete && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<div className="absolute inset-0 bg-black bg-opacity-50" onClick={() => setShowDeleteConfirm(false)}></div>
|
<div className="absolute inset-0 bg-black bg-opacity-50" onClick={() => !isSubmitting && setShowDeleteConfirm(false)}></div>
|
||||||
<div className="relative bg-white rounded-xl shadow-xl p-6 max-w-md w-full mx-4">
|
<div className="relative bg-white rounded-xl shadow-xl p-6 max-w-md w-full mx-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">Confirmer la suppression</h3>
|
<h3 className="text-lg font-semibold mb-4 text-gray-900">
|
||||||
<p className="text-gray-600 mb-6">
|
⚠️ Confirmer l'annulation
|
||||||
Êtes-vous sûr de vouloir supprimer cette demande ?
|
</h3>
|
||||||
<br /><strong>Type :</strong> {requestToDelete?.type}
|
|
||||||
<br /><strong>Dates :</strong> {requestToDelete?.dateDisplay}
|
<div className="space-y-3 mb-6">
|
||||||
<br /><br />
|
<p className="text-gray-700">
|
||||||
<span className="text-sm text-gray-500">Un email sera envoyé à votre manager pour l'informer.</span>
|
Voulez-vous annuler cette demande de congé ?
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Type :</span>
|
||||||
|
<span className="font-medium">{requestToDelete.type}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Période :</span>
|
||||||
|
<span className="font-medium">{requestToDelete.dateDisplay}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Durée :</span>
|
||||||
|
<span className="font-medium">{requestToDelete.days} jour(s)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Statut :</span>
|
||||||
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${getStatusColor(requestToDelete.status)}`}>
|
||||||
|
{requestToDelete.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(requestToDelete.status === 'Validée' || requestToDelete.status === 'Validé') && (
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm text-blue-800">
|
||||||
|
<p className="font-medium">ℹ️ Information</p>
|
||||||
|
<p className="mt-1">Cette demande a été validée. Vos compteurs seront automatiquement restaurés.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowDeleteConfirm(false)}
|
onClick={() => setShowDeleteConfirm(false)}
|
||||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
disabled={isSubmitting}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={confirmDeleteRequest}
|
onClick={confirmDeleteRequest}
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
disabled={isSubmitting}
|
||||||
|
className="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 disabled:opacity-50 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
Supprimer
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||||
|
Annulation...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
Confirmer l'annulation
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -380,6 +601,7 @@ const Requests = () => {
|
|||||||
<RefreshCw className={`w-5 h-5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
<RefreshCw className={`w-5 h-5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
data-tour="nouvelle-demande"
|
||||||
onClick={() => setShowNewRequestModal(true)}
|
onClick={() => setShowNewRequestModal(true)}
|
||||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-700 text-sm lg:text-base"
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-700 text-sm lg:text-base"
|
||||||
>
|
>
|
||||||
@@ -389,50 +611,7 @@ const Requests = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compteurs */}
|
{/* Compteurs */}
|
||||||
{detailedCounters && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
||||||
{/* CP N */}
|
|
||||||
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-500">CP Année N</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-gray-900">{detailedCounters.cpN?.solde?.toFixed(1) || '0.0'}</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Sur {detailedCounters.cpN?.acquis?.toFixed(1) || '0.0'} acquis</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CP N-1 */}
|
|
||||||
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-500">CP Année N-1</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-gray-900">{detailedCounters.cpN1?.solde?.toFixed(1) || '0.0'}</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Sur {detailedCounters.cpN1?.acquis?.toFixed(1) || '0.0'} acquis</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* RTT N */}
|
|
||||||
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-500">RTT Année N</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-gray-900">{detailedCounters.rttN?.solde?.toFixed(1) || '0.0'}</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Sur {detailedCounters.rttN?.acquis?.toFixed(1) || '0.0'} acquis</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Total disponible */}
|
|
||||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 p-4 rounded-xl shadow-sm text-white">
|
|
||||||
<h3 className="text-sm font-medium opacity-90 mb-2">Total disponible</h3>
|
|
||||||
<p className="text-2xl font-bold">
|
|
||||||
{(
|
|
||||||
(detailedCounters.cpN?.solde || 0) +
|
|
||||||
(detailedCounters.cpN1?.solde || 0) +
|
|
||||||
(detailedCounters.rttN?.solde || 0)
|
|
||||||
).toFixed(1)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs opacity-75 mt-1">Jours ouvrés</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Left: list */}
|
{/* Left: list */}
|
||||||
@@ -441,7 +620,7 @@ const Requests = () => {
|
|||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-4">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-4">
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="relative">
|
<div className="flex-1 relative" data-tour="recherche">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -453,6 +632,7 @@ const Requests = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
data-tour="filtres"
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
className="flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50"
|
className="flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
@@ -472,6 +652,7 @@ const Requests = () => {
|
|||||||
<option value="En attente">En attente</option>
|
<option value="En attente">En attente</option>
|
||||||
<option value="Validée">Validée</option>
|
<option value="Validée">Validée</option>
|
||||||
<option value="Refusée">Refusée</option>
|
<option value="Refusée">Refusée</option>
|
||||||
|
<option value="Annulée">Annulée</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
@@ -479,10 +660,10 @@ const Requests = () => {
|
|||||||
className="px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">Tous les types</option>
|
<option value="all">Tous les types</option>
|
||||||
<option value="Congé payé">Congé payé</option>
|
<option value="Congé payé">Congé(s) payé(s)</option>
|
||||||
<option value="RTT">RTT</option>
|
<option value="RTT">RTT</option>
|
||||||
<option value="Arrêt maladie">Arrêt maladie</option>
|
<option value="Arrêt maladie">Arrêt maladie</option>
|
||||||
<option value="Absence">Absence</option>
|
<option value="Récupération">Récupération</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -495,13 +676,13 @@ const Requests = () => {
|
|||||||
<p className="text-gray-500">Chargement...</p>
|
<p className="text-gray-500">Chargement...</p>
|
||||||
</div>
|
</div>
|
||||||
) : currentRequests.length === 0 ? (
|
) : currentRequests.length === 0 ? (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 text-center">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 text-center" >
|
||||||
<Info className="w-12 h-12 mx-auto text-gray-300 mb-3" />
|
<Info className="w-12 h-12 mx-auto text-gray-300 mb-3" />
|
||||||
<p className="text-gray-500">Aucune demande trouvée</p>
|
<p className="text-gray-500">Aucune demande trouvée</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3" data-tour="liste-demandes">
|
||||||
{currentRequests.map((request) => (
|
{currentRequests.map((request) => (
|
||||||
<div key={request.id} className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
|
<div key={request.id} className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
@@ -534,14 +715,16 @@ const Requests = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bouton Supprimer */}
|
{/* Bouton Supprimer */}
|
||||||
<button
|
{request.status === 'En attente' && (
|
||||||
onClick={() => handleDeleteRequest(request)}
|
<button
|
||||||
className="text-red-600 hover:text-red-700 flex items-center gap-1 px-2 py-1 hover:bg-red-50 rounded"
|
onClick={() => handleDeleteRequest(request.id)}
|
||||||
title="Supprimer"
|
className="text-orange-600 hover:text-orange-700 flex items-center gap-1 px-2 py-1 hover:bg-orange-50 rounded"
|
||||||
>
|
title="Annuler"
|
||||||
<Trash2 className="w-4 h-4" />
|
>
|
||||||
<span className="hidden sm:inline">Supprimer</span>
|
<X className="w-4 h-4" />
|
||||||
</button>
|
<span className="hidden sm:inline">Annuler</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Bouton Voir */}
|
{/* Bouton Voir */}
|
||||||
<button
|
<button
|
||||||
@@ -637,11 +820,11 @@ const Requests = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteRequest(selectedRequest)}
|
onClick={() => handleDeleteRequest(selectedRequest.id)}
|
||||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
Supprimer cette demande
|
Annuler cette demande
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -667,7 +850,9 @@ const Requests = () => {
|
|||||||
availableRTT_N1: 0,
|
availableRTT_N1: 0,
|
||||||
availableABS: 0,
|
availableABS: 0,
|
||||||
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
||||||
availableRTT: detailedCounters.rttN?.solde || 0
|
availableRTT: detailedCounters.rttN?.solde || 0,
|
||||||
|
availableRecup: detailedCounters?.recupN?.solde || 0,
|
||||||
|
availableRecup_N: detailedCounters?.recupN?.solde || 0
|
||||||
}}
|
}}
|
||||||
accessToken={graphToken}
|
accessToken={graphToken}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
@@ -697,13 +882,15 @@ const Requests = () => {
|
|||||||
availableRTT_N1: 0,
|
availableRTT_N1: 0,
|
||||||
availableABS: 0,
|
availableABS: 0,
|
||||||
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
||||||
availableRTT: detailedCounters.rttN?.solde || 0
|
availableRTT: detailedCounters.rttN?.solde || 0,
|
||||||
|
availableRecup: detailedCounters?.recupN?.solde || 0,
|
||||||
|
availableRecup_N: detailedCounters?.recupN?.solde || 0
|
||||||
}}
|
}}
|
||||||
accessToken={graphToken}
|
accessToken={graphToken}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
userEmail={user.email}
|
userEmail={user.email}
|
||||||
userRole={user.role}
|
userRole={user.role}
|
||||||
userName={`${user.prenom} ${user.nom}`}
|
userName={`${user.prenom} ${user.nom}`} // ⭐ CORRIGÉ : ajout des accolades {}
|
||||||
onRequestUpdated={() => {
|
onRequestUpdated={() => {
|
||||||
refreshAllData();
|
refreshAllData();
|
||||||
}}
|
}}
|
||||||
|
|||||||
21
project/src/vite.config.js
Normal file
21
project/src/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
port: 3000
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ['react', 'react-dom', 'react-router-dom']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
153
setup-complete.ps1
Normal file
153
setup-complete.ps1
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
Write-Host "=== Configuration complète du projet GTA ===" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 1. Créer la structure
|
||||||
|
Write-Host "`n1. Création de la structure..." -ForegroundColor Yellow
|
||||||
|
$dirs = @(
|
||||||
|
"C:\GTA\project\public\backend",
|
||||||
|
"C:\GTA\project\public\backend\uploads",
|
||||||
|
|
||||||
|
"C:\GTA\project\src"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($dir in $dirs) {
|
||||||
|
if (!(Test-Path $dir)) {
|
||||||
|
New-Item -Path $dir -ItemType Directory -Force | Out-Null
|
||||||
|
Write-Host " ✓ Créé: $dir" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Backend package.json
|
||||||
|
Write-Host "`n2. Création de package.json..." -ForegroundColor Yellow
|
||||||
|
$backendPackage = @"
|
||||||
|
{
|
||||||
|
"name": "gta-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "GTA Backend API",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"mysql2": "^3.6.5",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
|
"@azure/identity": "^4.0.0",
|
||||||
|
"body-parser": "^1.20.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content -Path "C:\GTA\project\backend\package.json" -Value $backendPackage
|
||||||
|
Write-Host " ✓ package.json créé" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 3. Backend .env
|
||||||
|
Write-Host "`n3. Création de .env..." -ForegroundColor Yellow
|
||||||
|
$envContent = @"
|
||||||
|
DB_HOST=mysql
|
||||||
|
DB_USER=wpuser
|
||||||
|
DB_PASSWORD=-2b/)ru5/Bi8P[7_
|
||||||
|
DB_NAME=DemandeConge
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=production
|
||||||
|
AZURE_TENANT_ID=9840a2a0-6ae1-4688-b03d-d2ec291be0f9
|
||||||
|
AZURE_CLIENT_ID=4bb4cc24-bac3-427c-b02c-5d14fc67b561
|
||||||
|
AZURE_CLIENT_SECRET=gvf8Q~545Bafn8yYsgjW~QG_P1lpzaRe6gJNgb2t
|
||||||
|
AZURE_GROUP_ID=c1ea877c-6bca-4f47-bfad-f223640813a0
|
||||||
|
EMAIL_FROM=gtanoreply@ensup.eu
|
||||||
|
UPLOAD_DIR=./uploads
|
||||||
|
MAX_FILE_SIZE=5242880
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content -Path "C:\GTA\project\backend\.env" -Value $envContent
|
||||||
|
Write-Host " ✓ .env créé" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 4. Backend server.js
|
||||||
|
Write-Host "`n4. Création de server.js..." -ForegroundColor Yellow
|
||||||
|
$serverJs = @"
|
||||||
|
require('dotenv').config();
|
||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
env: {
|
||||||
|
dbHost: process.env.DB_HOST,
|
||||||
|
dbName: process.env.DB_NAME
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/test', (req, res) => {
|
||||||
|
res.json({ message: 'Backend GTA opérationnel!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(\`✓ Serveur démarré sur le port \${PORT}\`);
|
||||||
|
});
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content -Path "C:\GTA\project\backend\server.js" -Value $serverJs
|
||||||
|
Write-Host " ✓ server.js créé" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 5. Backend Dockerfile
|
||||||
|
Write-Host "`n5. Création de Dockerfile..." -ForegroundColor Yellow
|
||||||
|
$dockerfile = @"
|
||||||
|
FROM node:18-alpine AS base
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl mysql-client
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN if [ -f package-lock.json ]; then \
|
||||||
|
npm ci --omit=dev; \
|
||||||
|
else \
|
||||||
|
npm install --production; \
|
||||||
|
fi && npm cache clean --force
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /app/uploads && chmod 755 /app/uploads
|
||||||
|
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nodejs -u 1001 && \
|
||||||
|
chown -R nodejs:nodejs /app
|
||||||
|
|
||||||
|
USER nodejs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3000/health || exit 1
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content -Path "C:\GTA\project\backend\DockerfileGTA.backend" -Value $dockerfile
|
||||||
|
Write-Host " ✓ Dockerfile créé" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 6. Afficher le résumé
|
||||||
|
Write-Host "`n=== Configuration terminée ===" -ForegroundColor Green
|
||||||
|
Write-Host "`nFichiers créés:" -ForegroundColor Cyan
|
||||||
|
Get-ChildItem C:\GTA\project\public\Backend | Select-Object Name, Length
|
||||||
|
|
||||||
|
Write-Host "`n=== Prochaines étapes ===" -ForegroundColor Yellow
|
||||||
|
Write-Host "1. cd C:\GTA" -ForegroundColor White
|
||||||
|
Write-Host "2. docker-compose up --build -d" -ForegroundColor White
|
||||||
|
Write-Host "3. docker-compose logs -f backend" -ForegroundColor White
|
||||||
Reference in New Issue
Block a user