diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..9494d37 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,32 @@ +# É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;"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a7b89be --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + frontend: + image: ouijdaneim/gta-frontend:latest + ports: + - "3000:80" + depends_on: + - backend + + backend: + image: ouijdaneim/gta-backend:latest + ports: + - "8000:80" \ No newline at end of file diff --git a/project/public/php/Dockerfile.backend b/project/public/php/Dockerfile.backend new file mode 100644 index 0000000..bbcc787 --- /dev/null +++ b/project/public/php/Dockerfile.backend @@ -0,0 +1,14 @@ +# 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 \ No newline at end of file diff --git a/project/public/php/check-user-groups.php b/project/public/php/check-user-groups.php new file mode 100644 index 0000000..9c18c98 --- /dev/null +++ b/project/public/php/check-user-groups.php @@ -0,0 +1,147 @@ +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, // 🔹 ajout important + "user" => [ + "id" => $newUserId, + "entraUserId" => $entraUserId, + "prenom" => $prenom, + "nom" => $nom, + "email" => $email, + "service" => $service, + "role" => $role + ] +]); + + +$conn->close(); +?> diff --git a/project/public/php/db.php b/project/public/php/db.php new file mode 100644 index 0000000..a3ab16f --- /dev/null +++ b/project/public/php/db.php @@ -0,0 +1,20 @@ +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"); diff --git a/project/public/php/getAllTeamRequests.php b/project/public/php/getAllTeamRequests.php index efaf6dc..8a65dad 100644 --- a/project/public/php/getAllTeamRequests.php +++ b/project/public/php/getAllTeamRequests.php @@ -27,7 +27,6 @@ if ($conn->connect_error) { exit(); } - // Récupération ID manager $managerId = $_GET['SuperieurId'] ?? null; if (!$managerId) { @@ -36,31 +35,29 @@ if (!$managerId) { } $sql = " - SELECT - dc.Id, - dc.DateDebut, - dc.DateFin, - dc.Statut, - dc.DateDemande, - dc.Commentaire, - dc.DocumentJoint, - dc.EmployeeId, - CONCAT(u.Prenom, ' ', u.Nom) as employee_name, - u.Email as employee_email, - tc.Nom as type - FROM DemandeConge dc - JOIN Users u ON dc.EmployeeId = u.ID - JOIN TypeConge tc ON dc.TypeCongeId = tc.Id - JOIN HierarchieValidation hv ON hv.EmployeId = u.ID - WHERE hv.SuperieurId = ? - ORDER BY dc.DateDemande DESC + 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(); - -// Manquant dans ton code $result = $stmt->get_result(); $requests = []; @@ -78,7 +75,7 @@ while ($row = $result->fetch_assoc()) { $requests[] = [ "id" => (int)$row['Id'], - "employee_id" => (int)$row['EmployeeId'], + "employee_id" => (int)$row['employee_id'], "employee_name" => $row['employee_name'], "employee_email" => $row['employee_email'], "type" => $row['type'], diff --git a/project/public/php/getEmploye.php b/project/public/php/getEmploye.php new file mode 100644 index 0000000..82ee97d --- /dev/null +++ b/project/public/php/getEmploye.php @@ -0,0 +1,51 @@ +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, Matricule, Telephone, Adresse + FROM CollaborateurAD + WHERE id = ? AND Actif = 1 + "); + $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(); +?> diff --git a/project/public/php/getEmployeRequest.php b/project/public/php/getEmployeRequest.php new file mode 100644 index 0000000..bdb7358 --- /dev/null +++ b/project/public/php/getEmployeRequest.php @@ -0,0 +1,66 @@ +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(); +?> diff --git a/project/public/php/getLeaveCounters.php b/project/public/php/getLeaveCounters.php index 61bc93b..1631237 100644 --- a/project/public/php/getLeaveCounters.php +++ b/project/public/php/getLeaveCounters.php @@ -27,22 +27,22 @@ $leaveYear = getLeaveYear(); $rttYear = getRTTYear(); $currentDate = date('Y-m-d'); -// --- Soldes initiaux (CompteurConges) restent inchangés --- +// --- Soldes initiaux (CompteurConges pour CollaborateurAD) --- $cpSolde = 0; $rttSolde = 0; $absSolde = 0; if ($cpTypeId !== null) { - $q="SELECT Solde FROM CompteurConges WHERE EmployeeId=? AND TypeCongeId=? AND Annee=?"; + $q="SELECT Solde FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?"; $s=$conn->prepare($q); $s->bind_param("iii",$userId,$cpTypeId,$leaveYear); $s->execute(); $res=$s->get_result(); if($r=$res->fetch_assoc()) $cpSolde=$r['Solde']; $s->close(); } if ($rttTypeId !== null) { - $q="SELECT Solde FROM CompteurConges WHERE EmployeeId=? AND TypeCongeId=? AND Annee=?"; + $q="SELECT Solde FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?"; $s=$conn->prepare($q); $s->bind_param("iii",$userId,$rttTypeId,$rttYear); $s->execute(); $res=$s->get_result(); if($r=$res->fetch_assoc()) $rttSolde=$r['Solde']; $s->close(); } if ($absTypeId !== null) { - $q="SELECT Solde FROM CompteurConges WHERE EmployeeId=? AND TypeCongeId=? AND Annee=?"; + $q="SELECT Solde FROM CompteurConges WHERE CollaborateurADId=? AND TypeCongeId=? AND Annee=?"; $s=$conn->prepare($q); $s->bind_param("iii",$userId,$absTypeId,$rttYear); $s->execute(); $res=$s->get_result(); if($r=$res->fetch_assoc()) $absSolde=$r['Solde']; $s->close(); } -// --- Calcul CP in process : priorité DemandeCongeType, fallback = working days on DemandeConge --- +// --- Calcul CP en cours --- $cpInProcess = 0; if ($cpTypeId !== null) { $sql = " @@ -50,13 +50,13 @@ if ($cpTypeId !== null) { FROM DemandeConge dc LEFT JOIN DemandeCongeType dct ON dct.DemandeCongeId = dc.Id AND dct.TypeCongeId = ? - WHERE dc.EmployeeId = ? + WHERE dc.CollaborateurADId = ? AND dc.Statut IN ('En attente','Validée') AND dc.DateFin >= ? AND (dct.NombreJours IS NOT NULL OR FIND_IN_SET(?, dc.TypeCongeId)) "; $s = $conn->prepare($sql); - $s->bind_param("iiss", $cpTypeId, $userId, $currentDate, $cpTypeId); + $s->bind_param("iisi", $cpTypeId, $userId, $currentDate, $cpTypeId); $s->execute(); $res = $s->get_result(); while ($r = $res->fetch_assoc()) { @@ -69,7 +69,7 @@ if ($cpTypeId !== null) { $s->close(); } -// --- Calcul RTT in process (même logique) --- +// --- Calcul RTT en cours --- $rttInProcess = 0; if ($rttTypeId !== null) { $sql = " @@ -77,13 +77,13 @@ if ($rttTypeId !== null) { FROM DemandeConge dc LEFT JOIN DemandeCongeType dct ON dct.DemandeCongeId = dc.Id AND dct.TypeCongeId = ? - WHERE dc.EmployeeId = ? + WHERE dc.CollaborateurADId = ? AND dc.Statut IN ('En attente','Validée') AND dc.DateFin >= ? AND (dct.NombreJours IS NOT NULL OR FIND_IN_SET(?, dc.TypeCongeId)) "; $s = $conn->prepare($sql); - $s->bind_param("iiss", $rttTypeId, $userId, $currentDate, $rttTypeId); + $s->bind_param("iisi", $rttTypeId, $userId, $currentDate, $rttTypeId); $s->execute(); $res = $s->get_result(); while ($r = $res->fetch_assoc()) { @@ -96,7 +96,7 @@ if ($rttTypeId !== null) { $s->close(); } -// --- Calcul absenteisme (validation) : priorité DemandeCongeType, fallback = DATEDIFF+1 --- +// --- Calcul absenteisme validé --- $absenteism = 0; if ($absTypeId !== null) { $sql = " @@ -104,7 +104,7 @@ if ($absTypeId !== null) { FROM DemandeConge dc LEFT JOIN DemandeCongeType dct ON dct.DemandeCongeId = dc.Id AND dct.TypeCongeId = ? - WHERE dc.EmployeeId = ? + WHERE dc.CollaborateurADId = ? AND dc.Statut = 'Validée' AND (dct.NombreJours IS NOT NULL OR FIND_IN_SET(?, dc.TypeCongeId)) "; @@ -116,7 +116,6 @@ if ($absTypeId !== null) { if ($r['NombreJours'] !== null) { $absenteism += (float)$r['NombreJours']; } else { - // fallback : DATEDIFF + 1 $d1 = new DateTime($r['DateDebut']); $d2 = new DateTime($r['DateFin']); $absenteism += ($d2->diff($d1)->days + 1); } diff --git a/project/public/php/getPendingRequests.php b/project/public/php/getPendingRequests.php index 859e602..92a7941 100644 --- a/project/public/php/getPendingRequests.php +++ b/project/public/php/getPendingRequests.php @@ -55,8 +55,8 @@ function getWorkingDays($startDate, $endDate) { } try { - // Récupérer le service du manager - $queryManagerService = "SELECT ServiceId FROM Users WHERE ID = ?"; + // 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(); @@ -75,19 +75,19 @@ try { dc.Statut, dc.DateDemande, dc.Commentaire, - dc.EmployeeId, - CONCAT(u.Prenom, ' ', u.Nom) as employee_name, - u.Email as employee_email, + 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 Users u ON dc.EmployeeId = u.ID + JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.id JOIN TypeConge tc ON FIND_IN_SET(tc.Id, dc.TypeCongeId) - WHERE u.ServiceId = ? + WHERE ca.ServiceId = ? AND dc.Statut = 'En attente' - AND u.ID != ? + AND ca.id != ? GROUP BY dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, - dc.Commentaire, dc.EmployeeId, u.Prenom, u.Nom, u.Email + dc.Commentaire, dc.CollaborateurADId, ca.prenom, ca.nom, ca.email ORDER BY dc.DateDemande ASC "; @@ -112,7 +112,7 @@ try { $requests[] = [ 'id' => (int)$row['Id'], - 'employee_id' => (int)$row['EmployeeId'], + 'employee_id' => (int)$row['CollaborateurADId'], 'employee_name' => $row['employee_name'], 'employee_email' => $row['employee_email'], 'type' => $row['types'], // ex: "Congé payé, RTT" diff --git a/project/public/php/getRequests.php b/project/public/php/getRequests.php index 79fdf10..b5a02d2 100644 --- a/project/public/php/getRequests.php +++ b/project/public/php/getRequests.php @@ -53,31 +53,31 @@ function getWorkingDays($startDate, $endDate) { 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 = ? - GROUP BY - dc.Id, dc.DateDebut, dc.DateFin, dc.Statut, dc.DateDemande, - dc.Commentaire, dc.Validateur, dc.DocumentJoint - ORDER BY dc.DateDemande DESC - "; + 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("i", $userId); + $stmt->bind_param("ii", $userId, $userId); $stmt->execute(); $result = $stmt->get_result(); diff --git a/project/public/php/getTeamMembers.php b/project/public/php/getTeamMembers.php index 3000286..e6a910b 100644 --- a/project/public/php/getTeamMembers.php +++ b/project/public/php/getTeamMembers.php @@ -1,5 +1,5 @@ connect_error) { - error_log("Erreur connexion DB getTeamMembers: " . $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(); } @@ -36,11 +36,11 @@ if ($managerId === null) { exit(); } -error_log("getTeamMembers - Manager ID: $managerId"); +error_log("getTeamMembersAD - Manager ID: $managerId"); try { - // D'abord, récupérer le service du manager - $queryManagerService = "SELECT ServiceId FROM Users WHERE ID = ?"; + // 🔹 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(); @@ -48,22 +48,22 @@ try { if ($managerRow = $resultManager->fetch_assoc()) { $serviceId = $managerRow['ServiceId']; - error_log("getTeamMembers - Service ID du manager: $serviceId"); + error_log("getTeamMembersAD - ServiceId du manager: $serviceId"); - // Récupérer tous les membres du même service (sauf le manager lui-même) + // 🔹 2. Récupérer tous les collaborateurs du même service (sauf le manager) $queryTeam = " SELECT - u.ID as id, - u.Nom as nom, - u.Prenom as prenom, - u.Email as email, - u.Role as role, - u.DateEmbauche as date_embauche, + c.id, + c.nom, + c.prenom, + c.email, + c.role, + s.Nom as service_name - FROM Users u - JOIN Services s ON u.ServiceId = s.Id - WHERE u.ServiceId = ? AND u.ID != ? AND u.Actif = 1 - ORDER BY u.Prenom, u.Nom + 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); @@ -79,12 +79,12 @@ try { 'prenom' => $row['prenom'], 'email' => $row['email'], 'role' => $row['role'], - 'date_embauche' => $row['date_embauche'], + 'service_name' => $row['service_name'] ]; } - error_log("getTeamMembers - Membres trouvés: " . count($teamMembers)); + error_log("getTeamMembersAD - Membres trouvés: " . count($teamMembers)); echo json_encode([ "success" => true, @@ -95,7 +95,7 @@ try { $stmtTeam->close(); } else { - error_log("getTeamMembers - Manager non trouvé: $managerId"); + error_log("getTeamMembersAD - Manager non trouvé: $managerId"); echo json_encode([ "success" => false, "message" => "Manager non trouvé" @@ -105,7 +105,7 @@ try { $stmtManager->close(); } catch (Exception $e) { - error_log("Erreur getTeamMembers: " . $e->getMessage()); + error_log("Erreur getTeamMembersAD: " . $e->getMessage()); echo json_encode([ "success" => false, "message" => "Erreur lors de la récupération de l'équipe: " . $e->getMessage() @@ -113,4 +113,4 @@ try { } $conn->close(); -?> \ No newline at end of file +?> diff --git a/project/public/php/initial-sync.php b/project/public/php/initial-sync.php new file mode 100644 index 0000000..f603981 --- /dev/null +++ b/project/public/php/initial-sync.php @@ -0,0 +1,128 @@ +connect_error) { + die(json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error])); +} + +// --- Authentification (client credentials) --- +$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"; // 🔹 Mets l'Object ID de ton groupe ici + +$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"] ?? ""; +$description = $group["description"] ?? ""; +$mail = $group["mail"] ?? ""; +$createdAt = null; +if (!empty($group["createdDateTime"])) { + $dt = new DateTime($group["createdDateTime"]); + $createdAt = $dt->format("Y-m-d H:i:s"); // format MySQL +} + + + + +// --- Insérer / mettre à jour le groupe dans EntraGroups --- +$stmt = $conn->prepare("INSERT INTO EntraGroups (Id, DisplayName, Description, Mail, CreatedAt, UpdatedAt, SyncDate, IsActive) + VALUES (?, ?, ?, ?, ?, NOW(), NOW(), 1) + ON DUPLICATE KEY UPDATE + DisplayName=?, Description=?, Mail=?, UpdatedAt=NOW(), SyncDate=NOW(), IsActive=1"); +if ($stmt) { + $stmt->bind_param("ssssssss", + $groupId, $displayName, $description, $mail, $createdAt, + $displayName, $description, $mail + ); + $stmt->execute(); +} + + + +// --- 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"] ?? ""; + $role = "Collaborateur"; // par défaut + + if (!$email) continue; + + $stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, role) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE prenom=?, nom=?, email=?, service=?, role=?"); + if ($stmt) { + $stmt->bind_param("sssssssssss", + $entraUserId, $prenom, $nom, $email, $service, $role, + $prenom, $nom, $email, $service, $role + ); + $stmt->execute(); + $usersInserted++; + } +} + +echo json_encode([ + "success" => true, + "message" => "Synchronisation terminée", + "groupe_sync" => $displayName, + "users_sync" => $usersInserted +]); + +$conn->close(); +?> diff --git a/project/public/php/login.php b/project/public/php/login.php index af75631..cbb2e1f 100644 --- a/project/public/php/login.php +++ b/project/public/php/login.php @@ -1,7 +1,7 @@ connect_error) { - die(json_encode(["success" => false, "message" => "Erreur de connexion à la base de données : " . $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'] ?? ''; -$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 = ? -"; +$headers = getallheaders(); +$accessToken = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : ''; -$stmt = $conn->prepare($query); +// ====================================================== +// 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 ($stmt === false) { - die(json_encode(["success" => false, "message" => "Erreur de préparation de la requête : " . $conn->error])); -} - -$stmt->bind_param("ss", $email, $mot_de_passe); -$stmt->execute(); - -$result = $stmt->get_result(); - -if ($result->num_rows === 1) { + if ($result->num_rows === 0) { + echo json_encode(["success" => false, "message" => "Utilisateur non autorisé (pas dans l'annuaire)"]); + exit(); + } $user = $result->fetch_assoc(); - - echo json_encode([ - "success" => true, - "message" => "Connexion réussie.", - "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."]); + + // 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(); } -$stmt->close(); +// ====================================================== +// 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(); ?> diff --git a/project/public/php/submitLeaveRequest.php b/project/public/php/submitLeaveRequest.php index 1a30e18..5595b95 100644 --- a/project/public/php/submitLeaveRequest.php +++ b/project/public/php/submitLeaveRequest.php @@ -1,100 +1,293 @@ connect_error) { echo json_encode(["success"=>false,"message"=>"Erreur DB: ".$conn->connect_error]); exit(); } +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(200); + exit(); +} -// Lecture JSON (support FormData via $_POST['data']) -if (isset($_POST['data'])) { - $data = json_decode($_POST['data'], true); +// 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 { - $input = file_get_contents('php://input'); - $data = json_decode($input, true); -} -if ($data === null) { - echo json_encode(["success"=>false,"message"=>"JSON invalide"]); $conn->close(); exit(); + $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']; } -// Vérifs minimales -if (!isset($data['EmployeeId'],$data['DateDebut'],$data['DateFin'],$data['Repartition'],$data['NombreJours'])) { - echo json_encode(["success"=>false,"message"=>"Données manquantes"]); $conn->close(); exit(); -} - -$employeeId = (int)$data['EmployeeId']; -$dateDebut = $data['DateDebut']; -$dateFin = $data['DateFin']; -$commentaire= $data['Commentaire'] ?? ''; -$numDays = (float)$data['NombreJours']; -$statut = 'En attente'; -$currentDate= date('Y-m-d H:i:s'); - -// 1) Construire la liste d'IDs pour TypeCongeId (CSV) (compatibilité) +// 🔎 Résoudre les IDs des types de congés $typeIds = []; foreach ($data['Repartition'] as $rep) { - $code = $rep['TypeConge']; // CP, RTT, ABS ou texte libre + $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; + 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']; } - $s = $conn->prepare("SELECT Id FROM TypeConge WHERE Nom = ?"); - $s->bind_param("s", $name); - $s->execute(); - $res = $s->get_result(); - if ($r = $res->fetch_assoc()) $typeIds[] = $r['Id']; - $s->close(); } -if (empty($typeIds)) { echo json_encode(["success"=>false,"message"=>"Aucun type valide"]); $conn->close(); exit(); } +if (empty($typeIds)) { + echo json_encode(["success"=>false,"message"=>"Aucun type de congé valide"]); + exit; +} $typeCongeIdCsv = implode(',', $typeIds); -// 2) Insertion unique dans DemandeConge -$insert = $conn->prepare("INSERT INTO DemandeConge (EmployeeId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); -$validParam = ''; -$insert->bind_param("isssssssd", $employeeId, $dateDebut, $dateFin, $typeCongeIdCsv, $statut, $currentDate, $commentaire, $validParam, $numDays); -if (!$insert->execute()) { - echo json_encode(["success"=>false,"message"=>"Erreur insert DemandeConge: ".$insert->error]); - $insert->close(); $conn->close(); exit(); -} -$demandeId = $conn->insert_id; -$insert->close(); +// ✅ 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)"; -// 3) INSÉRER la répartition réelle dans DemandeCongeType (une ligne par type) -$insertType = $conn->prepare("INSERT INTO DemandeCongeType (DemandeCongeId, TypeCongeId, NombreJours) VALUES (?, ?, ?)"); -if (!$insertType) { - echo json_encode(["success"=>false,"message"=>"Erreur préparation DemandeCongeType: ".$conn->error]); $conn->close(); exit(); -} +$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) { - $code = $rep['TypeConge']; $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; + default: $name = $code; break; } - $s = $conn->prepare("SELECT Id FROM TypeConge WHERE Nom = ?"); - $s->bind_param("s", $name); - $s->execute(); - $res = $s->get_result(); - if ($r = $res->fetch_assoc()) { - $typeId = (int)$r['Id']; - $insertType->bind_param("iid", $demandeId, $typeId, $jours); // i,i,d - $insertType->execute(); + $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 + ]); } - $s->close(); } -$insertType->close(); -echo json_encode(["success"=>true,"message"=>"Demande soumise", "request_id"=>$demandeId]); -$conn->close(); -?> +// ✅ 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},

+ Votre demande du {$dateDebut} au {$dateFin} + ({$numDays} jour(s)) a bien été enregistrée.
+ Elle est en attente de validation par votre manager.

+ Merci. + " +); + +# Mail aux managers +foreach ($managers as $managerEmail) { + sendMailGraph( + $accessToken, + $fromEmail, + $managerEmail, + "Nouvelle demande de congé - {$userName}", + " + Bonjour,

+ {$userName} a soumis une demande de congé :
+ - Du {$dateDebut} au {$dateFin} ({$numDays} jour(s))
+ - Commentaire : " . (!empty($commentaire) ? $commentaire : "Aucun") . "

+ Merci de valider cette demande. + " + ); +} + +# ✅ Réponse finale +echo json_encode([ + "success"=>true, + "message"=>"Demande soumise", + "request_id"=>$demandeId, + "managers"=>$managers +]); diff --git a/project/public/php/sync-groups.php b/project/public/php/sync-groups.php new file mode 100644 index 0000000..e69de29 diff --git a/project/public/php/test_db.php b/project/public/php/test_db.php deleted file mode 100644 index a599817..0000000 --- a/project/public/php/test_db.php +++ /dev/null @@ -1,14 +0,0 @@ -connect_error) { - die("❌ Connexion échouée : " . $conn->connect_error); -} -echo "✅ Connexion réussie à la base de données !"; - -?> diff --git a/project/public/php/validateRequest.php b/project/public/php/validateRequest.php index 5792465..7fe78d1 100644 --- a/project/public/php/validateRequest.php +++ b/project/public/php/validateRequest.php @@ -11,187 +11,197 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { 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); +// 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 validateRequest: " . $conn->connect_error); - echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]); + echo json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error]); exit(); } // Lecture du JSON envoyé $input = file_get_contents('php://input'); -error_log("validateRequest - Input reçu: " . $input); - $data = json_decode($input, true); if (!isset($data['request_id'], $data['action'], $data['validator_id'])) { - error_log("validateRequest - Données manquantes: " . print_r($data, true)); - echo json_encode([ - "success" => false, - "message" => "Données manquantes pour la validation" - ]); + echo json_encode(["success" => false, "message" => "Données manquantes"]); exit(); } -$requestId = (int)$data['request_id']; -$action = $data['action']; // 'approve' ou 'reject' +$requestId = (int)$data['request_id']; +$action = $data['action']; // "approve" | "reject" $validatorId = (int)$data['validator_id']; -$comment = $data['comment'] ?? ''; - -error_log("validateRequest - Request ID: $requestId, Action: $action, Validator: $validatorId"); +$comment = $data['comment'] ?? ''; try { $conn->begin_transaction(); - - // Vérifier que la demande existe et est en attente + + // Vérifier si validateur est Users ou CollaborateurAD + $isUserValidator = false; + $stmt = $conn->prepare("SELECT ID FROM Users WHERE ID = ?"); + $stmt->bind_param("i", $validatorId); + $stmt->execute(); + $res = $stmt->get_result(); + if ($res->fetch_assoc()) { + $isUserValidator = true; + } else { + $stmt = $conn->prepare("SELECT Id FROM CollaborateurAD WHERE Id = ?"); + $stmt->bind_param("i", $validatorId); + $stmt->execute(); + $res = $stmt->get_result(); + if (!$res->fetch_assoc()) { + throw new Exception("Validateur introuvable dans Users ou CollaborateurAD"); + } + } + $stmt->close(); + + // Récupération demande $queryCheck = " - SELECT dc.Id, dc.EmployeeId, dc.TypeCongeId, dc.DateDebut, dc.DateFin, dc.NombreJours, - u.Nom, u.Prenom, tc.Nom as TypeNom + SELECT dc.Id, dc.EmployeeId, dc.CollaborateurADId, dc.TypeCongeId, dc.DateDebut, dc.DateFin, dc.NombreJours, + u.Nom as UserNom, u.Prenom as UserPrenom, + ca.nom as CADNom, ca.prenom as CADPrenom, + tc.Nom as TypeNom FROM DemandeConge dc - JOIN Users u ON dc.EmployeeId = u.ID JOIN TypeConge tc ON dc.TypeCongeId = tc.Id + LEFT JOIN Users u ON dc.EmployeeId = u.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(); $resultCheck = $stmtCheck->get_result(); - - if ($requestRow = $resultCheck->fetch_assoc()) { - $employeeId = $requestRow['EmployeeId']; - $typeCongeId = $requestRow['TypeCongeId']; - $nombreJours = $requestRow['NombreJours']; - $employeeName = $requestRow['Prenom'] . ' ' . $requestRow['Nom']; - $typeNom = $requestRow['TypeNom']; - - error_log("validateRequest - Demande trouvée: $employeeName, Type: $typeNom, Jours: $nombreJours"); - - // Déterminer le nouveau statut - $newStatus = ($action === 'approve') ? 'Validée' : 'Refusée'; - - // Mettre à jour la demande + + if (!($requestRow = $resultCheck->fetch_assoc())) { + throw new Exception("Demande non trouvée ou déjà traitée"); + } + $stmtCheck->close(); + + $employeeId = $requestRow['EmployeeId']; + $collaborateurId = $requestRow['CollaborateurADId']; + $typeCongeId = $requestRow['TypeCongeId']; + $nombreJours = $requestRow['NombreJours']; + $employeeName = $employeeId + ? $requestRow['UserPrenom']." ".$requestRow['UserNom'] + : $requestRow['CADPrenom']." ".$requestRow['CADNom']; + $typeNom = $requestRow['TypeNom']; + + $newStatus = ($action === 'approve') ? 'Validée' : 'Refusée'; + + // 🔹 Mise à jour DemandeConge + if ($isUserValidator) { $queryUpdate = " - UPDATE DemandeConge + UPDATE DemandeConge SET Statut = ?, ValidateurId = ?, + ValidateurADId = NULL, DateValidation = NOW(), CommentaireValidation = ? WHERE Id = ? "; - - $stmtUpdate = $conn->prepare($queryUpdate); - $stmtUpdate->bind_param("sisi", $newStatus, $validatorId, $comment, $requestId); - - if ($stmtUpdate->execute()) { - error_log("validateRequest - Demande mise à jour avec succès"); - - // Si approuvée, déduire du solde (sauf pour congé maladie) - if ($action === 'approve' && $typeNom !== 'Congé maladie') { - // Déterminer l'année selon le type de congé - $currentDate = new DateTime(); - if ($typeNom === 'Congé payé') { - // Exercice CP: 01/06 au 31/05 - $year = ($currentDate->format('m') < 6) ? $currentDate->format('Y') - 1 : $currentDate->format('Y'); - } else { - // RTT: année civile - $year = $currentDate->format('Y'); - } - - error_log("validateRequest - Déduction solde: Type=$typeNom, Année=$year, Jours=$nombreJours"); - - // Déduire du solde - $queryDeduct = " - UPDATE CompteurConges - SET Solde = GREATEST(0, Solde - ?) - WHERE EmployeeId = ? AND TypeCongeId = ? AND Annee = ? - "; - - $stmtDeduct = $conn->prepare($queryDeduct); - $stmtDeduct->bind_param("diii", $nombreJours, $employeeId, $typeCongeId, $year); - - if ($stmtDeduct->execute()) { - error_log("validateRequest - Solde déduit avec succès"); - } else { - error_log("validateRequest - Erreur déduction solde: " . $stmtDeduct->error); - } - - $stmtDeduct->close(); - } - - // Créer une notification pour l'employé - $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"; - } - - $queryNotif = " - INSERT INTO Notifications (UserId, Titre, Message, Type, DemandeCongeId) - VALUES (?, ?, ?, ?, ?) - "; - - $notifType = ($action === 'approve') ? 'Success' : 'Error'; - $stmtNotif = $conn->prepare($queryNotif); - $stmtNotif->bind_param("isssi", $employeeId, $notificationTitle, $notificationMessage, $notifType, $requestId); - $stmtNotif->execute(); - $stmtNotif->close(); - - // Log dans l'historique - $actionText = ($action === 'approve') ? 'Validation congé' : 'Refus congé'; - $actionDetails = "$actionText $employeeName ($typeNom)"; - if ($comment) { - $actionDetails .= " - $comment"; - } - - $queryHistory = " - INSERT INTO HistoriqueActions (UserId, 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') . " avec succès", - "new_status" => $newStatus - ]); - - } else { - throw new Exception("Erreur lors de la mise à jour: " . $stmtUpdate->error); - } - - $stmtUpdate->close(); } else { - throw new Exception("Demande non trouvée ou déjà traitée"); + $queryUpdate = " + UPDATE DemandeConge + SET Statut = ?, + ValidateurId = NULL, + ValidateurADId = ?, + DateValidation = NOW(), + CommentaireValidation = ? + WHERE Id = ? + "; } - - $stmtCheck->close(); - + $stmtUpdate = $conn->prepare($queryUpdate); + $stmtUpdate->bind_param("sisi", $newStatus, $validatorId, $comment, $requestId); + $stmtUpdate->execute(); + $stmtUpdate->close(); + + // 🔹 Déduction solde (seulement Users, pas AD, hors maladie) + if ($action === 'approve' && $typeNom !== 'Congé maladie' && $employeeId) { + $currentDate = new DateTime(); + $year = ($typeNom === 'Congé payé' && (int)$currentDate->format('m') < 6) + ? $currentDate->format('Y') - 1 + : $currentDate->format('Y'); + + $queryDeduct = " + UPDATE CompteurConges + SET Solde = GREATEST(0, Solde - ?) + WHERE EmployeeId = ? AND TypeCongeId = ? AND Annee = ? + "; + $stmtDeduct = $conn->prepare($queryDeduct); + $stmtDeduct->bind_param("diii", $nombreJours, $employeeId, $typeCongeId, $year); + $stmtDeduct->execute(); + $stmtDeduct->close(); + } + + // 🔹 Notification (User ou CollaborateurAD) + $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'; + + if ($employeeId) { + $queryNotif = " + INSERT INTO Notifications (UserId, CollaborateurADId, Titre, Message, Type, DemandeCongeId) + VALUES (?, NULL, ?, ?, ?, ?) + "; + $stmtNotif = $conn->prepare($queryNotif); + $stmtNotif->bind_param("isssi", $employeeId, $notificationTitle, $notificationMessage, $notifType, $requestId); + $stmtNotif->execute(); + $stmtNotif->close(); + } elseif ($collaborateurId) { + $queryNotif = " + INSERT INTO Notifications (UserId, CollaborateurADId, Titre, Message, Type, DemandeCongeId) + VALUES (NULL, ?, ?, ?, ?, ?) + "; + $stmtNotif = $conn->prepare($queryNotif); + $stmtNotif->bind_param("isssi", $collaborateurId, $notificationTitle, $notificationMessage, $notifType, $requestId); + $stmtNotif->execute(); + $stmtNotif->close(); + } + + // 🔹 Historique (User ou CollaborateurAD) + $actionText = ($action === 'approve') ? 'Validation congé' : 'Refus congé'; + $actionDetails = "$actionText $employeeName ($typeNom)"; + if ($comment) $actionDetails .= " - $comment"; + + if ($isUserValidator) { + $queryHistory = " + INSERT INTO HistoriqueActions (UserId, CollaborateurADId, Action, Details, DemandeCongeId) + VALUES (?, NULL, ?, ?, ?) + "; + $stmtHistory = $conn->prepare($queryHistory); + $stmtHistory->bind_param("issi", $validatorId, $actionText, $actionDetails, $requestId); + } else { + $queryHistory = " + INSERT INTO HistoriqueActions (UserId, CollaborateurADId, Action, Details, DemandeCongeId) + VALUES (NULL, ?, ?, ?, ?) + "; + $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(); - error_log("Erreur validateRequest: " . $e->getMessage()); - echo json_encode([ - "success" => false, - "message" => "Erreur lors de la validation: " . $e->getMessage() - ]); + echo json_encode(["success" => false, "message" => $e->getMessage()]); } $conn->close(); -?> \ No newline at end of file +?> diff --git a/project/src/App.jsx b/project/src/App.jsx index 8dab5db..ddec53c 100644 --- a/project/src/App.jsx +++ b/project/src/App.jsx @@ -7,6 +7,7 @@ import Requests from './pages/Requests'; import Calendar from './pages/Calendar'; import Manager from './pages/Manager'; import ProtectedRoute from './components/ProtectedRoute'; +import EmployeeDetails from './pages/EmployeeDetails'; function App() { return ( @@ -34,6 +35,7 @@ function App() { } /> + } /> } /> diff --git a/project/src/AuthConfig.js b/project/src/AuthConfig.js index 21c7b82..e9e1b02 100644 --- a/project/src/AuthConfig.js +++ b/project/src/AuthConfig.js @@ -4,9 +4,19 @@ export const msalConfig = { clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561", // Application (client) ID dans Azure authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9", // Directory (tenant) ID redirectUri: "http://localhost:5173" + }, + cache: { + cacheLocation: "sessionStorage", + storeAuthStateInCookie: false, } }; export const loginRequest = { - scopes: ["User.Read"] // Permet de lire le profil utilisateur + scopes: [ + "User.Read", + "User.Read.All", // Pour lire les profils des autres utilisateurs + "Group.Read.All", // Pour lire les groupes + "GroupMember.Read.All", // Pour lire les membres des groupes + "Mail.Send" //Envoyer les emails. + ] }; diff --git a/project/src/components/NewLeaveRequestModal.jsx b/project/src/components/NewLeaveRequestModal.jsx index daae10d..e094c26 100644 --- a/project/src/components/NewLeaveRequestModal.jsx +++ b/project/src/components/NewLeaveRequestModal.jsx @@ -1,14 +1,21 @@ import React, { useState, useEffect } from 'react'; import { X, Calendar, Clock, AlertCircle, RotateCcw } from 'lucide-react'; +import { useMsal } from "@azure/msal-react"; +import { loginRequest } from "../AuthConfig"; + const NewLeaveRequestModal = ({ onClose, availableLeaveCounters, userId, + userEmail, + userName, + accessToken, onRequestSubmitted, preselectedStartDate = null, preselectedEndDate = null, - preselectedType = null + preselectedType = null, + }) => { const [formData, setFormData] = useState({ types: preselectedType ? [preselectedType] : [], @@ -18,6 +25,7 @@ const NewLeaveRequestModal = ({ medicalDocuments: [] }); + const [typeDistribution, setTypeDistribution] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); @@ -27,6 +35,25 @@ const NewLeaveRequestModal = ({ const [isOtherChecked, setIsOtherChecked] = useState(false); const [otherLeaveType, setOtherLeaveType] = useState(''); + const { instance, accounts } = useMsal(); + + + // --- Helper pour garantir un token Graph valide + const ensureGraphToken = async () => { + if (!accounts[0]) throw new Error("Aucun utilisateur connecté"); + + const request = { ...loginRequest, account: accounts[0] }; + + try { + const response = await instance.acquireTokenSilent(request); + return response.accessToken; + } catch (err) { + console.warn("⚠️ Silent token failed, trying popup:", err); + const response = await instance.acquireTokenPopup(request); + return response.accessToken; + } + }; + // Vérifier si des valeurs sont pré-sélectionnées useEffect(() => { if (preselectedStartDate || preselectedEndDate || preselectedType) { @@ -273,55 +300,50 @@ const NewLeaveRequestModal = ({ })); const requestData = { - EmployeeId: userId, DateDebut: formData.startDate, DateFin: formData.endDate, Commentaire: formData.reason, NombreJours: calculatedDays, - Repartition: repartition + Repartition: repartition, + Email: userEmail, + Nom: userName }; - console.log("Payload envoyé au backend :", JSON.stringify(requestData, null, 2)); - const formDataToSend = new FormData(); - formDataToSend.append('data', JSON.stringify(requestData)); - - // Ajouter les fichiers - formData.medicalDocuments.forEach((file, index) => { - formDataToSend.append(`medicalDocuments[]`, file); - }); + console.log("📤 Payload envoyé au backend :", JSON.stringify(requestData, null, 2)); const response = await fetch('http://localhost/GTA/project/public/php/submitLeaveRequest.php', { method: 'POST', - body: formDataToSend + headers: { + + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) }); + const result = await response.json(); + console.log("📥 Réponse backend :", result); - const text = await response.text(); - let result; - try { - result = JSON.parse(text); - } catch (err) { - console.error("Réponse non JSON:", text); - setError("Erreur serveur : réponse invalide."); + if (!result.success) { + setError(result.message || "Erreur lors de l'enregistrement"); return; } - if (result.success) { - onRequestSubmitted?.(); - onClose(); - } else { - setError(result.message || 'Erreur lors de la soumission'); - } + console.log("✅ Demande enregistrée (et mails envoyés côté PHP)"); + + // Fermer modal et rafraîchir + onRequestSubmitted?.(); + onClose(); } catch (error) { - console.error('Erreur:', error); - setError('Erreur de connexion au serveur'); + console.error('❌ Erreur handleSubmit:', error); + setError("Erreur de connexion au serveur"); } finally { setIsSubmitting(false); } }; + const getTypeLabel = (type) => { switch (type) { case 'CP': return 'Congés payés'; diff --git a/project/src/components/Sidebar.jsx b/project/src/components/Sidebar.jsx index dabd958..9919ce7 100644 --- a/project/src/components/Sidebar.jsx +++ b/project/src/components/Sidebar.jsx @@ -13,7 +13,7 @@ const Sidebar = ({ isOpen, onToggle }) => { switch (role) { case 'Admin': return 'bg-red-100 text-red-800'; - case 'Manager': + case 'Validateur': return 'bg-green-100 text-green-800'; default: return 'bg-blue-100 text-blue-800'; @@ -109,7 +109,7 @@ const Sidebar = ({ isOpen, onToggle }) => { Calendrier - {(user?.role === 'Manager' || user?.role === 'Admin' || user?.role === 'Employe') && ( + {(user?.role === 'Validateur' || user?.role === 'Admin' || user?.role === 'Collaborateur') && ( window.innerWidth < 1024 && onToggle()} @@ -118,7 +118,7 @@ const Sidebar = ({ isOpen, onToggle }) => { > - {user?.role === 'Employe' ? 'Mon équipe' : 'Équipe'} + {user?.role === 'Collaborateur' ? 'Mon équipe' : 'Équipe'} )} diff --git a/project/src/context/AuthContext.jsx b/project/src/context/AuthContext.jsx index 76a76cc..5d4ecc8 100644 --- a/project/src/context/AuthContext.jsx +++ b/project/src/context/AuthContext.jsx @@ -1,5 +1,6 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import * as msal from '@azure/msal-browser'; +import { msalConfig, loginRequest } from '../AuthConfig'; const AuthContext = createContext(); @@ -11,70 +12,196 @@ export const useAuth = () => { return context; }; -const msalConfig = { - auth: { - clientId: '4bb4cc24-bac3-427c-b02c-5d14fc67b561', - authority: 'https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9', - redirectUri: window.location.origin, - }, -}; - const msalInstance = new msal.PublicClientApplication(msalConfig); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [userGroups, setUserGroups] = useState([]); + const [isAuthorized, setIsAuthorized] = useState(false); - // Initialise MSAL au montage + // Fonction pour obtenir l'URL de l'API backend + const getApiUrl = (endpoint) => { + const possibleUrls = [ + 'http://localhost/GTA/project/public/php/', + 'http://localhost:80/GTA/project/public/php/', + 'http://localhost/GTA/public/php/', + 'http://localhost/public/php/' + ]; + return possibleUrls[0] + endpoint; // Utilisez votre URL préférée + }; + + // Vérifier les groupes utilisateur via l'API backend + const checkUserAuthorization = async (userPrincipalName, accessToken) => { + try { + const response = await fetch(getApiUrl('check-user-groups.php'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}` + }, + body: JSON.stringify({ userPrincipalName }) + }); + + if (response.ok) { + const data = await response.json(); + setUserGroups(data.groups || []); + setIsAuthorized(data.authorized || false); + return data; + } + return { authorized: false, groups: [] }; + } catch (error) { + console.error('Erreur vérification groupes:', error); + return { authorized: false, groups: [] }; + } + }; + + // Synchroniser l'utilisateur avec la base locale + const syncUserToDatabase = async (entraUser, accessToken) => { + try { + const response = await fetch(getApiUrl('check-user-groups.php'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}` + }, + body: JSON.stringify({ + entraUserId: entraUser.id, + userPrincipalName: entraUser.userPrincipalName, + email: entraUser.mail || entraUser.userPrincipalName, + displayName: entraUser.displayName, + givenName: entraUser.givenName, + surname: entraUser.surname, + jobTitle: entraUser.jobTitle, + department: entraUser.department, + officeLocation: entraUser.officeLocation + }) + }); + + if (response.ok) { + return await response.json(); + } + } catch (error) { + console.error('Erreur synchronisation utilisateur:', error); + } + return null; + }; + + // Initialisation MSAL useEffect(() => { const initializeMsal = async () => { try { await msalInstance.initialize(); + + // Vérifier si il y a un utilisateur connecté + const accounts = msalInstance.getAllAccounts(); + if (accounts.length > 0) { + // Essayer de récupérer un token silencieusement + try { + const response = await msalInstance.acquireTokenSilent({ + ...loginRequest, + account: accounts[0] + }); + + await handleSuccessfulAuth(response); + } catch (error) { + console.log('Token silent acquisition failed:', error); + } + } } catch (error) { console.error("Erreur d'initialisation MSAL:", error); + } finally { + setIsLoading(false); } }; initializeMsal(); - - const savedUser = localStorage.getItem('user'); - if (savedUser) { - try { - setUser(JSON.parse(savedUser)); - } catch (error) { - console.error("Erreur parsing utilisateur sauvegardé:", error); - localStorage.removeItem('user'); - } - } - setIsLoading(false); }, []); - const login = async (email, password) => { + // Gérer l'authentification réussie + const handleSuccessfulAuth = async (authResponse) => { try { - const possibleUrls = [ - 'http://localhost/GTA/project/public/php/login.php', - 'http://localhost:80/GTA/project/public/php/login.php', - 'http://localhost/GTA/public/php/login.php', - 'http://localhost/public/php/login.php' - ]; + const account = authResponse.account; + const accessToken = authResponse.accessToken; - let response = null; - for (const url of possibleUrls) { + // Récupérer profil Microsoft Graph + const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + + let entraUser = { + id: account.homeAccountId, + displayName: account.name, + userPrincipalName: account.username, + mail: account.username + }; + + if (graphResponse.ok) { + const graphData = await graphResponse.json(); + entraUser = { ...entraUser, ...graphData }; + } + + // 🔹 Synchroniser l’utilisateur dans la DB + const syncResult = await syncUserToDatabase(entraUser, accessToken); + + // 🚀 NEW : si admin → lancer full-sync.php + if (syncResult?.role === "Admin") { try { - console.log("Test URL:", url); - response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, mot_de_passe: password }), + const syncResp = await fetch(getApiUrl('full-sync.php'), { + method: "POST", + headers: { "Authorization": `Bearer ${accessToken}` } }); - if (response.ok) break; - } catch { - continue; + const syncData = await syncResp.json(); + console.log("Résultat Full Sync:", syncData); + } catch (err) { + console.error("Erreur synchronisation groupes:", err); } } - if (!response || !response.ok) { - throw new Error('Aucune URL de connexion accessible'); + // 🔹 Vérifier autorisation via groupes DB + const authResult = await checkUserAuthorization(entraUser.userPrincipalName, accessToken); + + if (authResult.authorized) { + const userData = { + id: syncResult?.localUserId || entraUser.id, + entraUserId: entraUser.id, + name: entraUser.displayName, + prenom: entraUser.givenName || entraUser.displayName?.split(' ')[0] || '', + nom: entraUser.surname || entraUser.displayName?.split(' ')[1] || '', + email: entraUser.mail || entraUser.userPrincipalName, + userPrincipalName: entraUser.userPrincipalName, + role: syncResult?.role || 'Employe', + service: syncResult?.service || 'Non défini', + jobTitle: entraUser.jobTitle, + department: entraUser.department, + officeLocation: entraUser.officeLocation, + groups: authResult.groups + }; + + setUser(userData); + setIsAuthorized(true); + return true; + } else { + throw new Error('Utilisateur non autorisé - pas membre des groupes requis'); + } + } catch (error) { + console.error('Erreur lors de la gestion de l\'authentification:', error); + throw error; + } + }; + + + // Connexion classique (email/mot de passe) + const login = async (email, password) => { + try { + const response = await fetch(getApiUrl('login.php'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, mot_de_passe: password }), + }); + + if (!response.ok) { + throw new Error('Erreur de connexion'); } const text = await response.text(); @@ -98,49 +225,79 @@ export const AuthProvider = ({ children }) => { }; setUser(userData); - localStorage.setItem('user', JSON.stringify(userData)); + setIsAuthorized(true); return true; - } else { - console.error("Échec connexion:", data.message); - return false; } + return false; } catch (error) { console.error("Erreur de connexion:", error); return false; } }; + // Connexion Office 365 const loginWithO365 = async () => { try { - const loginResponse = await msalInstance.loginPopup({ - scopes: ["user.read"] - }); - const account = loginResponse.account; - if (account) { - const userData = { - id: account.homeAccountId, - name: account.name, - email: account.username, - role: 'Employe', - service: 'Non défini', - }; - setUser(userData); - localStorage.setItem('user', JSON.stringify(userData)); - return true; - } - return false; + const authResponse = await msalInstance.loginPopup(loginRequest); + await handleSuccessfulAuth(authResponse); + return true; } catch (error) { console.error('Erreur login Office 365:', error); - return false; + if (error.message?.includes('non autorisé')) { + throw new Error('Accès refusé: Vous n\'êtes pas membre d\'un groupe autorisé.'); + } + throw error; } }; - const logout = () => { - setUser(null); - localStorage.removeItem('user'); + // Déconnexion + const logout = async () => { + try { + const accounts = msalInstance.getAllAccounts(); + if (accounts.length > 0) { + await msalInstance.logoutPopup({ + account: accounts[0] + }); + } + } catch (error) { + console.error('Erreur lors de la déconnexion:', error); + } finally { + setUser(null); + setUserGroups([]); + setIsAuthorized(false); + } }; - const value = { user, login, loginWithO365, logout, isLoading }; + // Obtenir un token pour l'API + const getAccessToken = async () => { + try { + const accounts = msalInstance.getAllAccounts(); + if (accounts.length === 0) { + throw new Error('Aucun compte connecté'); + } + + const response = await msalInstance.acquireTokenSilent({ + ...loginRequest, + account: accounts[0] + }); + + return response.accessToken; + } catch (error) { + console.error('Erreur obtention token:', error); + return null; + } + }; + + const value = { + user, + userGroups, + isAuthorized, + login, + loginWithO365, + logout, + isLoading, + getAccessToken + }; return ( @@ -149,4 +306,4 @@ export const AuthProvider = ({ children }) => { ); }; -export default AuthContext; +export default AuthContext; \ No newline at end of file diff --git a/project/src/main.jsx b/project/src/main.jsx index 987c65a..8bb7206 100644 --- a/project/src/main.jsx +++ b/project/src/main.jsx @@ -1,10 +1,17 @@ -import { StrictMode } from 'react'; +import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App.jsx'; import './index.css'; +import { MsalProvider } from "@azure/msal-react"; +import { PublicClientApplication } from "@azure/msal-browser"; +import { msalConfig } from "./AuthConfig"; + +const msalInstance = new PublicClientApplication(msalConfig); createRoot(document.getElementById('root')).render( - - + + + + ); \ No newline at end of file diff --git a/project/src/pages/Calendar.jsx b/project/src/pages/Calendar.jsx index 306a3dc..c1a440c 100644 --- a/project/src/pages/Calendar.jsx +++ b/project/src/pages/Calendar.jsx @@ -642,7 +642,7 @@ END:VEVENT`; - {/* Ton rendu de calendrier ici */} + {/* Modal nouvelle demande */} @@ -654,9 +654,12 @@ END:VEVENT`; }} availableLeaveCounters={leaveCounters} userId={user?.id} + onRequestSubmitted={() => { resetSelection(); }} + userEmail={user.email} + userName={`${user.prenom} ${user.nom}`} preselectedStartDate={selectedDate} preselectedEndDate={selectedEndDate} preselectedType={preselectedType} diff --git a/project/src/pages/Dashboard.jsx b/project/src/pages/Dashboard.jsx index 3577fad..45297ab 100644 --- a/project/src/pages/Dashboard.jsx +++ b/project/src/pages/Dashboard.jsx @@ -1,11 +1,14 @@ import React, { useState, useEffect } from 'react'; import { useAuth } from '../context/AuthContext'; import Sidebar from '../components/Sidebar'; -import { Calendar as CalendarIcon, Clock, Users, TrendingUp, Plus, Settings, RefreshCw, Menu, FileText } from 'lucide-react'; +import { Calendar as CalendarIcon, Clock, Plus, Settings, RefreshCw, Menu, FileText } from 'lucide-react'; import NewLeaveRequestModal from '../components/NewLeaveRequestModal'; +import { useMsal } from "@azure/msal-react"; +import { loginRequest } from "../authConfig"; const Dashboard = () => { const { user } = useAuth(); + const [graphToken, setGraphToken] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(false); const [leaveCounters, setLeaveCounters] = useState({ availableCP: 0, @@ -20,54 +23,82 @@ const Dashboard = () => { const [recentRequests, setRecentRequests] = useState([]); const [allRequests, setAllRequests] = useState([]); + const { instance, accounts } = useMsal(); + + // Récupération du bon ID utilisateur (CollaborateurAD) + const userId = user?.id || user?.CollaborateurADId || user?.ID; + + useEffect(() => { - if (user?.id) { + console.log("🔎 Utilisateur chargé dans Dashboard:", user); + }, [user]); + + useEffect(() => { + if (accounts.length > 0) { + const request = { + ...loginRequest, + account: accounts[0], + }; + + instance.acquireTokenSilent(request) + .then((response) => { + console.log("✅ Token Graph récupéré (silent):", response.accessToken); + setGraphToken(response.accessToken); + }) + .catch(async (err) => { + console.warn("⚠️ Silent échoué, on tente un popup:", err); + try { + const tokenResponse = await instance.acquireTokenPopup(request); + console.log("✅ Token Graph récupéré (popup):", tokenResponse.accessToken); + setGraphToken(tokenResponse.accessToken); + } catch (popupErr) { + console.error("❌ Impossible d'obtenir un token:", popupErr); + } + }); + } + }, [accounts, instance]); + + // Vérification rapide du token + useEffect(() => { + if (graphToken) { + console.log("🔎 Token prêt ?", + graphToken.split(".").length === 3 ? "✅ JWT valide" : "❌ Token invalide: " + graphToken + ); + } + }, [graphToken]); + + // 🔄 Récupération compteurs et demandes + useEffect(() => { + if (userId) { fetchLeaveCounters(); fetchAllRequests(); } - }, [user]); + }, [userId]); const fetchLeaveCounters = async () => { try { - const url = `http://localhost/GTA/project/public/php/getLeaveCounters.php?user_id=${user.id}`; - console.log(' Dashboard - Récupération des compteurs:', url); - console.log(' Dashboard - User ID utilisé:', user.id); - + const url = `http://localhost/GTA/project/public/php/getLeaveCounters.php?user_id=${userId}`; const response = await fetch(url); - if (!response.ok) { - throw new Error(`Erreur HTTP: ${response.status}`); - } + if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`); const text = await response.text(); - console.log(' Dashboard - Réponse brute compteurs:', text); - console.log(' Dashboard - Longueur de la réponse:', text.length); - console.log(' Dashboard - Premiers 500 caractères:', text.substring(0, 500)); + console.log("🔎 Réponse brute getLeaveCounters:", text); let data; try { data = JSON.parse(text); - } catch (parseError) { - console.error(' Dashboard - Erreur parsing JSON:', parseError); - console.error(' Dashboard - Texte qui a causé l\'erreur:', text); - throw new Error('Réponse PHP invalide: ' + text.substring(0, 200)); + } catch (e) { + throw new Error("Le serveur PHP ne renvoie pas un JSON valide"); } - console.log(' Dashboard - Compteurs parsés:', data); - if (data.success) { - console.log(' Dashboard - Compteurs récupérés:', data.counters); setLeaveCounters(data.counters); } else { - console.error(' Dashboard - Erreur API compteurs:', data.message); - console.error(' Dashboard - Données complètes:', data); - throw new Error('API Error: ' + (data.message || 'Erreur inconnue')); + throw new Error(data.message || "Erreur lors de la récupération des compteurs"); } } catch (error) { - console.error('💥 Dashboard - Erreur lors de la récupération des compteurs:', error); - - // Fallback avec des données par défaut - console.log(' Dashboard - Utilisation des données par défaut'); + console.error("💥 Erreur compteurs:", error); setLeaveCounters({ availableCP: 25, availableRTT: 10, @@ -75,78 +106,63 @@ const Dashboard = () => { rttInProcess: 0, absenteism: 0 }); + } finally { + setIsLoading(false); } - - setIsLoading(false); }; const fetchAllRequests = async () => { - console.log(' Dashboard - Début fetchAllRequests pour user:', user?.id); - try { - const url = `http://localhost/GTA/project/public/php/getRequests.php?user_id=${user.id}`; - console.log(' Dashboard - URL appelée:', url); - + const url = `http://localhost/GTA/project/public/php/getRequests.php?user_id=${userId}`; const response = await fetch(url); - - if (!response.ok) { - throw new Error(`Erreur HTTP: ${response.status}`); - } + if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`); const text = await response.text(); - console.log(' Dashboard - Réponse brute:', text); + console.log("🔎 Réponse brute getRequests:", text); - const data = JSON.parse(text); - console.log(' Dashboard - Données parsées:', data); + let data; + try { + data = JSON.parse(text); + } catch { + throw new Error("Le serveur PHP ne renvoie pas un JSON valide"); + } if (data.success) { - console.log('Dashboard - Demandes récupérées:', data.requests?.length || 0); setAllRequests(data.requests || []); setRecentRequests(data.requests?.slice(0, 3) || []); } else { - throw new Error(data.message || 'Erreur lors de la récupération des demandes'); + throw new Error(data.message || "Erreur lors de la récupération des demandes"); } } catch (error) { - console.error(' Dashboard - Erreur lors de la récupération des demandes:', error); - - // En cas d'erreur, on garde des tableaux vides + console.error("💥 Erreur demandes:", error); setAllRequests([]); setRecentRequests([]); } }; const handleResetCounters = async () => { - if (!confirm(' ATTENTION !\n\nCette action va réinitialiser TOUS les compteurs de congés selon les règles de gestion :\n\n• Congés Payés : 25 jours (exercice 01/06 au 31/05)\n• RTT : 10 jours pour 2025 (exercice 01/01 au 31/12)\n• Congés Maladie : 0 jours\n\nCette action est IRRÉVERSIBLE !\n\nÊtes-vous sûr de vouloir continuer ?')) { - return; - } + if (!confirm("⚠️ Voulez-vous vraiment réinitialiser les compteurs ?")) return; try { const response = await fetch('http://localhost/GTA/project/public/php/resetLeaveCounters.php', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ manual_reset: true }), }); const data = await response.json(); - if (data.success) { - alert(` Réinitialisation réussie !\n\n• ${data.details.employees_updated} employés mis à jour\n• Exercice CP : ${data.details.leave_year}\n• Année RTT : ${data.details.rtt_year}\n• Date : ${data.details.reset_date}`); + alert("✅ Réinitialisation réussie !"); fetchLeaveCounters(); } else { - alert(` Erreur lors de la réinitialisation :\n${data.message}`); + alert(`❌ Erreur : ${data.message}`); } } catch (error) { - console.error('Erreur:', error); - alert(' Erreur de connexion au serveur'); + console.error("Erreur:", error); + alert("❌ Erreur serveur"); } }; - const openManualResetPage = () => { - window.open('http://localhost/GTA/project/public/php/manualResetCounters.php', '_blank'); - }; - const getStatusColor = (status) => { switch (status) { case 'Approuvé': @@ -392,7 +408,10 @@ const Dashboard = () => { setShowNewRequestModal(false)} availableLeaveCounters={leaveCounters} - userId={user?.id} + accessToken={graphToken} + userId={userId} + userEmail={user.email} + userName={`${user.prenom} ${user.nom}`} onRequestSubmitted={() => { fetchLeaveCounters(); fetchAllRequests(); diff --git a/project/src/pages/EmployeeDetails.jsx b/project/src/pages/EmployeeDetails.jsx new file mode 100644 index 0000000..16b38da --- /dev/null +++ b/project/src/pages/EmployeeDetails.jsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import Sidebar from '../components/Sidebar'; +import { Calendar, Clock, CheckCircle, XCircle } from 'lucide-react'; + +const EmployeeDetails = () => { + const { id } = useParams(); + const [employee, setEmployee] = useState(null); + const [requests, setRequests] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetchEmployeeData(); + }, [id]); + + const fetchEmployeeData = async () => { + try { + setIsLoading(true); + + // 1️⃣ Données employé + const resEmployee = await fetch(`http://localhost/GTA/project/public/php/getEmploye.php?id=${id}`); + const dataEmployee = await resEmployee.json(); + + if (!dataEmployee.success) { + setEmployee(null); + return; + } + setEmployee(dataEmployee.employee); + + // 2️⃣ Historique des demandes + const resRequests = await fetch(`http://localhost/GTA/project/public/php/getEmployeRequest.php?id=${id}`); + const dataRequests = await resRequests.json(); + setRequests(dataRequests.requests || []); + + // 3️⃣ Compteurs de congés et RTT + const resCounters = await fetch(`http://localhost/GTA/project/public/php/getLeaveCounters.php?user_id=${id}`); + const dataCounters = await resCounters.json(); + + if (dataCounters.success) { + setEmployee(prev => ({ + ...prev, + conges_restants: dataCounters.counters.availableCP, + rtt_restants: dataCounters.counters.availableRTT + })); + } + + } catch (err) { + console.error("Erreur récupération données collaborateur:", err); + } finally { + setIsLoading(false); + } + }; + + const getStatusIcon = (status) => { + switch (status) { + case 'Validée': + return ; + case 'Refusée': + case 'Annulée': + return ; + default: + return ; + } + }; + + if (isLoading) return

Chargement...

; + if (!employee) return

Collaborateur introuvable

; + + return ( +
+ + +
+

{employee.Prenom} {employee.Nom}

+

{employee.Email}

+ + {/* Compteurs congés/RTT */} +
+
+

Congés restants

+

{employee.conges_restants || 0} jours

+
+
+

RTT restants

+

{employee.rtt_restants || 0} jours

+
+
+ + {/* Historique des congés */} +

Historique des congés

+
+ {requests.length === 0 ? ( +

Aucune demande

+ ) : ( + requests.map((r) => ( +
+
+

{r.type} - {r.days}j

+

{r.date_display}

+
+
+ {getStatusIcon(r.status)} + {r.status} +
+
+ )) + )} +
+
+
+ ); +}; + +export default EmployeeDetails; diff --git a/project/src/pages/Login.jsx b/project/src/pages/Login.jsx index 1803ad3..747b016 100644 --- a/project/src/pages/Login.jsx +++ b/project/src/pages/Login.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useAuth } from '../context/AuthContext'; import { useNavigate } from 'react-router-dom'; -import { Building2, Mail, Lock, Eye, EyeOff } from 'lucide-react'; +import { Building2, Mail, Lock, Eye, EyeOff, AlertTriangle } from 'lucide-react'; const Login = () => { const [email, setEmail] = useState(''); @@ -9,25 +9,88 @@ const Login = () => { const [showPassword, setShowPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); + const [authMethod, setAuthMethod] = useState(''); // Pour tracker la méthode d'auth utilisée const navigate = useNavigate(); - const { login, loginWithO365 } = useAuth(); + const { login, loginWithO365, isAuthorized } = useAuth(); const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(''); + setAuthMethod('local'); - const success = await login(email, password); - if (success) { - navigate('/dashboard'); - } else { - setError('Identifiants incorrects. Veuillez réessayer.'); + try { + const success = await login(email, password); + if (success) { + navigate('/dashboard'); + } else { + setError('Identifiants incorrects. Veuillez réessayer.'); + } + } catch (error) { + setError(error.message || 'Erreur lors de la connexion'); } setIsLoading(false); }; + const handleO365Login = async () => { + setIsLoading(true); + setError(''); + setAuthMethod('o365'); + + try { + // Étape 1 : Login O365 + const success = await loginWithO365(); + + if (!success) { + setError("Erreur lors de la connexion Office 365"); + setIsLoading(false); + return; + } + + // Étape 2 : Récupération du token d’authentification (si ton context le fournit) + const token = localStorage.getItem("o365_token"); + // ⚠️ Ici j’imagine que tu stockes ton token quelque part (dans ton AuthContext ou localStorage). + // Adapte selon ton implémentation de loginWithO365 + + // Étape 3 : Appel de ton API PHP + const response = await fetch("http://localhost/GTA/project/public/php/initial-sync.php", { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + console.log("Résultat syncGroups :", data); + + if (!data.success) { + setError("Erreur de synchronisation des groupes : " + data.message); + setIsLoading(false); + return; + } + + // Étape 4 : Redirection vers le dashboard + navigate('/dashboard'); + + } catch (error) { + console.error('Erreur O365:', error); + + if (error.message?.includes('non autorisé') || error.message?.includes('Accès refusé')) { + setError('Accès refusé : Vous devez être membre d\'un groupe autorisé dans votre organisation.'); + } else if (error.message?.includes('AADSTS')) { + setError('Erreur d\'authentification Azure AD. Contactez votre administrateur.'); + } else { + setError(error.message || "Erreur lors de la connexion Office 365"); + } + } + + setIsLoading(false); + }; + + return (
{/* Image côté gauche */} @@ -50,7 +113,38 @@ const Login = () => {

Gestion de congés

- {/* Form */} + {/* Connexion Office 365 prioritaire */} +
+ +
+ + {/* Séparateur */} +
+
+
+
+
+ ou +
+
+ + {/* Formulaire classique */}
@@ -84,11 +179,13 @@ const Login = () => { className="w-full pl-9 lg:pl-10 pr-10 lg:pr-12 py-2 lg:py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm lg:text-base" placeholder="••••••••" required + disabled={isLoading} /> - - + {/* Affichage des erreurs */} {error && ( -
-

{error}

+
+
+ +
+

+ {error.includes('Accès refusé') ? 'Accès refusé' : 'Erreur de connexion'} +

+

{error}

+ {error.includes('groupe autorisé') && ( +

+ Contactez votre administrateur pour être ajouté aux groupes appropriés. +

+ )} +
+
)} + + {/* Info sur l'authentification */} +
+

+ Utilisez votre compte Office 365 pour une connexion sécurisée +

+
@@ -134,4 +235,4 @@ const Login = () => { ); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/project/src/pages/Requests.jsx b/project/src/pages/Requests.jsx index 9a7b677..9473bbb 100644 --- a/project/src/pages/Requests.jsx +++ b/project/src/pages/Requests.jsx @@ -3,6 +3,7 @@ import { useAuth } from '../context/AuthContext'; import Sidebar from '../components/Sidebar'; import { Plus, Search, Filter, Eye, Menu, X } from 'lucide-react'; import NewLeaveRequestModal from '../components/NewLeaveRequestModal'; +import { useMsal } from "@azure/msal-react"; const Requests = () => { const { user } = useAuth(); @@ -32,8 +33,30 @@ const Requests = () => { const [showFilters, setShowFilters] = useState(false); + const [graphToken, setGraphToken] = useState(null); + const { instance, accounts } = useMsal(); + const userId = user?.id || user?.CollaborateurADId || user?.ID; + useEffect(() => { - if (user?.id) { + if (accounts.length > 0) { + const request = { + scopes: ["User.Read", "Mail.Send"], + account: accounts[0], + }; + + instance.acquireTokenSilent(request) + .then((response) => { + setGraphToken(response.accessToken); + console.log("✅ Token Graph récupéré :", response.accessToken); + }) + .catch((err) => { + console.error("❌ Erreur récupération token Graph:", err); + }); + } + }, [accounts, instance]); + + useEffect(() => { + if (userId) { fetchLeaveCounters(); fetchAllRequests(); } @@ -69,7 +92,7 @@ const Requests = () => { const fetchLeaveCounters = async () => { try { - const response = await fetch(`http://localhost/GTA/project/public/php/getLeaveCounters.php?user_id=${user.id}`); + const response = await fetch(`http://localhost/GTA/project/public/php/getLeaveCounters.php?user_id=${userId}`); const text = await response.text(); let data; try { @@ -89,7 +112,7 @@ const Requests = () => { const fetchAllRequests = async () => { try { - const url = `http://localhost/GTA/project/public/php/getRequests.php?user_id=${user.id}`; + const url = `http://localhost/GTA/project/public/php/getRequests.php?user_id=${userId}`; const response = await fetch(url); const text = await response.text(); let data; @@ -426,6 +449,8 @@ const Requests = () => { onClose={() => setShowNewRequestModal(false)} availableLeaveCounters={leaveCounters} userId={user?.id} + userEmail={user.email} + userName={`${user.prenom} ${user.nom}`} onRequestSubmitted={() => { fetchLeaveCounters(); fetchAllRequests();