Compare commits
2 Commits
fbcd80fb6f
...
011620fb39
| Author | SHA1 | Date | |
|---|---|---|---|
| 011620fb39 | |||
| b066dcd136 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -361,3 +361,6 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
/project/public/submitLeaveRequest.php
|
||||
/project/public/getPendingRequests.php
|
||||
/project/public/validateRequest.php
|
||||
|
||||
@@ -1,156 +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 {
|
||||
// D'abord, récupérer le service du manager
|
||||
$queryManagerService = "SELECT ServiceId FROM Users 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 de l'équipe
|
||||
$queryRequests = "
|
||||
SELECT
|
||||
dc.Id,
|
||||
dc.DateDebut,
|
||||
dc.DateFin,
|
||||
dc.Statut,
|
||||
dc.DateDemande,
|
||||
dc.Commentaire,
|
||||
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
|
||||
WHERE u.ServiceId = ?
|
||||
AND dc.Statut = 'En attente'
|
||||
AND u.ID != ?
|
||||
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['EmployeeId'],
|
||||
'employee_name' => $row['employee_name'],
|
||||
'employee_email' => $row['employee_email'],
|
||||
'type' => $row['type'],
|
||||
'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();
|
||||
?>
|
||||
@@ -4,7 +4,6 @@ header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type");
|
||||
|
||||
// Gère la requête OPTIONS (pré-vol CORS)
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
@@ -12,7 +11,6 @@ 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);
|
||||
@@ -22,17 +20,14 @@ $dbname = "DemandeConge";
|
||||
$username = "wpuser";
|
||||
$password = "-2b/)ru5/Bi8P[7_";
|
||||
|
||||
// Crée une nouvelle connexion à la base de données
|
||||
$conn = new mysqli($host, $username, $password, $dbname);
|
||||
|
||||
// Vérifie la connexion
|
||||
if ($conn->connect_error) {
|
||||
error_log("Erreur connexion DB getRequests: " . $conn->connect_error);
|
||||
echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données : " . $conn->connect_error]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Récupère l'ID utilisateur depuis les paramètres de requête GET
|
||||
$userId = $_GET['user_id'] ?? null;
|
||||
|
||||
error_log("=== DEBUT getRequests.php ===");
|
||||
@@ -45,9 +40,6 @@ if ($userId === null) {
|
||||
exit();
|
||||
}
|
||||
|
||||
error_log("getRequests - Récupération pour user_id: $userId (type: " . gettype($userId) . ")");
|
||||
|
||||
// Vérifier si l'utilisateur existe
|
||||
$checkUserQuery = "SELECT ID, Nom, Prenom FROM Users WHERE ID = ?";
|
||||
$checkUserStmt = $conn->prepare($checkUserQuery);
|
||||
if ($checkUserStmt) {
|
||||
@@ -62,15 +54,13 @@ if ($checkUserStmt) {
|
||||
$checkUserStmt->close();
|
||||
}
|
||||
|
||||
// Fonction pour calculer les jours ouvrés (hors week-ends)
|
||||
function getWorkingDays($startDate, $endDate) {
|
||||
$workingDays = 0;
|
||||
$current = new DateTime($startDate);
|
||||
$end = new DateTime($endDate);
|
||||
|
||||
while ($current <= $end) {
|
||||
$dayOfWeek = (int)$current->format('N'); // 1 (Lundi) à 7 (Dimanche)
|
||||
if ($dayOfWeek < 6) { // Si ce n'est ni Samedi (6) ni Dimanche (7)
|
||||
$dayOfWeek = (int)$current->format('N');
|
||||
if ($dayOfWeek < 6) {
|
||||
$workingDays++;
|
||||
}
|
||||
$current->modify('+1 day');
|
||||
@@ -79,7 +69,6 @@ function getWorkingDays($startDate, $endDate) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Requête pour récupérer les demandes de l'utilisateur avec les informations du type de congé
|
||||
$query = "
|
||||
SELECT
|
||||
dc.Id,
|
||||
@@ -89,6 +78,7 @@ try {
|
||||
dc.DateDemande,
|
||||
dc.Commentaire,
|
||||
dc.Validateur,
|
||||
dc.DocumentJoint, -- 👈 CHAMP AJOUTÉ ICI
|
||||
tc.Nom as TypeConge
|
||||
FROM DemandeConge dc
|
||||
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||||
@@ -96,8 +86,6 @@ try {
|
||||
ORDER BY dc.DateDemande DESC
|
||||
";
|
||||
|
||||
error_log("getRequests - Requête SQL: $query");
|
||||
|
||||
$stmt = $conn->prepare($query);
|
||||
if ($stmt === false) {
|
||||
throw new Exception("Erreur de préparation de la requête : " . $conn->error);
|
||||
@@ -107,29 +95,11 @@ try {
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
error_log("getRequests - Nombre de résultats trouvés: " . $result->num_rows);
|
||||
|
||||
// Debug: Afficher toutes les demandes de la table pour cet utilisateur
|
||||
$debugQuery = "SELECT COUNT(*) as total FROM DemandeConge WHERE EmployeeId = ?";
|
||||
$debugStmt = $conn->prepare($debugQuery);
|
||||
if ($debugStmt) {
|
||||
$debugStmt->bind_param("i", $userId);
|
||||
$debugStmt->execute();
|
||||
$debugResult = $debugStmt->get_result();
|
||||
$debugRow = $debugResult->fetch_assoc();
|
||||
error_log("getRequests - Total demandes en DB pour user $userId: " . $debugRow['total']);
|
||||
$debugStmt->close();
|
||||
}
|
||||
|
||||
$requests = [];
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
error_log("getRequests - Traitement demande ID: " . $row['Id']);
|
||||
|
||||
// Calcul des jours ouvrés
|
||||
$workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']);
|
||||
|
||||
// Mapping des types de congés pour l'affichage
|
||||
$displayType = $row['TypeConge'];
|
||||
switch ($row['TypeConge']) {
|
||||
case 'Congé payé':
|
||||
@@ -143,18 +113,23 @@ try {
|
||||
break;
|
||||
}
|
||||
|
||||
// Formatage des dates pour l'affichage
|
||||
$startDate = new DateTime($row['DateDebut']);
|
||||
$endDate = new DateTime($row['DateFin']);
|
||||
$submittedDate = new DateTime($row['DateDemande']);
|
||||
|
||||
// Format d'affichage des dates
|
||||
if ($row['DateDebut'] === $row['DateFin']) {
|
||||
$dateDisplay = $startDate->format('d/m/Y');
|
||||
} else {
|
||||
$dateDisplay = $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y');
|
||||
}
|
||||
|
||||
// 👇 GÉNÉRATION DU LIEN VERS LE FICHIER
|
||||
$fileUrl = null;
|
||||
if ($row['TypeConge'] === 'Congé maladie' && !empty($row['DocumentJoint'])) {
|
||||
$fileName = basename($row['DocumentJoint']);
|
||||
$fileUrl = 'http://localhost/GTA/project/uploads/'. $fileName;
|
||||
}
|
||||
|
||||
$requests[] = [
|
||||
'id' => (int)$row['Id'],
|
||||
'type' => $displayType,
|
||||
@@ -166,16 +141,13 @@ try {
|
||||
'reason' => $row['Commentaire'] ?: 'Aucun commentaire',
|
||||
'submittedAt' => $row['DateDemande'],
|
||||
'submittedDisplay' => $submittedDate->format('d/m/Y'),
|
||||
'validator' => $row['Validateur'] ?: null
|
||||
'validator' => $row['Validateur'] ?: null,
|
||||
'fileUrl' => $fileUrl
|
||||
];
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
|
||||
error_log("getRequests - Demandes formatées: " . count($requests));
|
||||
error_log("getRequests - Détail des demandes: " . print_r($requests, true));
|
||||
error_log("=== FIN getRequests.php ===");
|
||||
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"message" => "Demandes récupérées avec succès.",
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
<?php
|
||||
// Active l'affichage des erreurs pour le dev
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type");
|
||||
|
||||
// Gère le pré-vol CORS
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
|
||||
header("Content-Type: application/json");
|
||||
|
||||
// --- Paramètres de connexion ---
|
||||
$host = "192.168.0.4";
|
||||
$dbname = "DemandeConge";
|
||||
$username = "wpuser";
|
||||
$password = "-2b/)ru5/Bi8P[7_";
|
||||
|
||||
// Connexion
|
||||
$conn = new mysqli($host, $username, $password, $dbname);
|
||||
if ($conn->connect_error) {
|
||||
error_log("Erreur connexion DB submitLeaveRequest: " . $conn->connect_error);
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Erreur de connexion DB : " . $conn->connect_error
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Lecture du JSON envoyé
|
||||
$input = file_get_contents('php://input');
|
||||
error_log("submitLeaveRequest - Input reçu: " . $input);
|
||||
|
||||
$data = json_decode($input, true);
|
||||
|
||||
if (!isset(
|
||||
$data['EmployeeId'],
|
||||
$data['TypeConge'],
|
||||
$data['DateDebut'],
|
||||
$data['DateFin'],
|
||||
$data['NumDays']
|
||||
)) {
|
||||
error_log("submitLeaveRequest - Données manquantes: " . print_r($data, true));
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Données manquantes pour la demande de congé."
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Récupération des champs
|
||||
$employeeId = (int) $data['EmployeeId'];
|
||||
$typeCongeNom= $data['TypeConge'];
|
||||
$dateDebut = $data['DateDebut'];
|
||||
$dateFin = $data['DateFin'];
|
||||
$commentaire = $data['Commentaire'] ?? '';
|
||||
$numDays = (int) $data['NumDays'];
|
||||
|
||||
error_log("submitLeaveRequest - Données parsées: EmployeeId=$employeeId, Type=$typeCongeNom, Début=$dateDebut, Fin=$dateFin");
|
||||
|
||||
$statut = 'En attente';
|
||||
$validateur = null;
|
||||
$currentDate= date('Y-m-d H:i:s'); // date complète pour DateDemande
|
||||
|
||||
// Mapping frontend → DB
|
||||
switch ($typeCongeNom) {
|
||||
case 'CP': $dbTypeCongeName = 'Congé payé'; break;
|
||||
case 'RTT': $dbTypeCongeName = 'RTT'; break;
|
||||
case 'ABS': $dbTypeCongeName = 'Congé maladie'; break;
|
||||
default:
|
||||
error_log("submitLeaveRequest - Type de congé inconnu: $typeCongeNom");
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Type de congé inconnu."
|
||||
]);
|
||||
$conn->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
error_log("submitLeaveRequest - Type DB mappé: $dbTypeCongeName");
|
||||
|
||||
// Récupération de l'ID du type de congé
|
||||
$stmt = $conn->prepare("SELECT Id FROM TypeConge WHERE Nom = ?");
|
||||
if (!$stmt) {
|
||||
error_log("submitLeaveRequest - Erreur préparation requête TypeConge: " . $conn->error);
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Erreur préparation requête TypeConge"
|
||||
]);
|
||||
$conn->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt->bind_param("s", $dbTypeCongeName);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$typeCongeId = (int) $row['Id'];
|
||||
error_log("submitLeaveRequest - TypeCongeId trouvé: $typeCongeId");
|
||||
} else {
|
||||
error_log("submitLeaveRequest - Type de congé non trouvé en DB: $dbTypeCongeName");
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Type de congé non trouvé en DB : $dbTypeCongeName"
|
||||
]);
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit();
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
// Requête d'insertion dans DemandeConge
|
||||
$query = "
|
||||
INSERT INTO DemandeConge
|
||||
(EmployeeId, DateDebut, DateFin, TypeCongeId, Statut, DateDemande, Commentaire, Validateur, NombreJours)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
";
|
||||
|
||||
error_log("submitLeaveRequest - Requête d'insertion: $query");
|
||||
|
||||
// Préparation de la requête
|
||||
$stmt = $conn->prepare($query);
|
||||
if (!$stmt) {
|
||||
error_log("Erreur prepare insert : " . $conn->error);
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Erreur interne lors de la préparation de la requête."
|
||||
]);
|
||||
$conn->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// Pour la colonne Validateur, on passe '' si null
|
||||
$validParam = $validateur ?? '';
|
||||
|
||||
error_log("submitLeaveRequest - Paramètres bind: $employeeId, $dateDebut, $dateFin, $typeCongeId, $statut, $currentDate, $commentaire, $validParam, $numDays");
|
||||
|
||||
// Bind des paramètres (types : i=integer, s=string, d=decimal)
|
||||
$stmt->bind_param(
|
||||
"ississssi",
|
||||
$employeeId, // i
|
||||
$dateDebut, // s
|
||||
$dateFin, // s
|
||||
$typeCongeId, // i
|
||||
$statut, // s
|
||||
$currentDate, // s - DateDemande
|
||||
$commentaire, // s
|
||||
$validParam, // s
|
||||
$numDays // i - NombreJours
|
||||
);
|
||||
|
||||
// Exécution
|
||||
if ($stmt->execute()) {
|
||||
$insertId = $conn->insert_id;
|
||||
error_log("submitLeaveRequest - Insertion réussie, ID: $insertId");
|
||||
echo json_encode([
|
||||
"success" => true,
|
||||
"message" => "Demande de congé soumise avec succès.",
|
||||
"request_id" => $insertId
|
||||
]);
|
||||
} else {
|
||||
error_log("Erreur execute insert : " . $stmt->error);
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Erreur lors de l'enregistrement : " . $stmt->error
|
||||
]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
|
||||
error_log("submitLeaveRequest - Script terminé");
|
||||
|
||||
?>
|
||||
@@ -1,197 +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");
|
||||
|
||||
// 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 validateRequest: " . $conn->connect_error);
|
||||
echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]);
|
||||
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"
|
||||
]);
|
||||
exit();
|
||||
}
|
||||
|
||||
$requestId = (int)$data['request_id'];
|
||||
$action = $data['action']; // 'approve' ou 'reject'
|
||||
$validatorId = (int)$data['validator_id'];
|
||||
$comment = $data['comment'] ?? '';
|
||||
|
||||
error_log("validateRequest - Request ID: $requestId, Action: $action, Validator: $validatorId");
|
||||
|
||||
try {
|
||||
$conn->begin_transaction();
|
||||
|
||||
// Vérifier que la demande existe et est en attente
|
||||
$queryCheck = "
|
||||
SELECT dc.Id, dc.EmployeeId, dc.TypeCongeId, dc.DateDebut, dc.DateFin, dc.NombreJours,
|
||||
u.Nom, u.Prenom, tc.Nom as TypeNom
|
||||
FROM DemandeConge dc
|
||||
JOIN Users u ON dc.EmployeeId = u.ID
|
||||
JOIN TypeConge tc ON dc.TypeCongeId = tc.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
|
||||
$queryUpdate = "
|
||||
UPDATE DemandeConge
|
||||
SET Statut = ?,
|
||||
ValidateurId = ?,
|
||||
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");
|
||||
}
|
||||
|
||||
$stmtCheck->close();
|
||||
|
||||
} catch (Exception $e) {
|
||||
$conn->rollback();
|
||||
error_log("Erreur validateRequest: " . $e->getMessage());
|
||||
echo json_encode([
|
||||
"success" => false,
|
||||
"message" => "Erreur lors de la validation: " . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -255,52 +255,64 @@ const NewLeaveRequestModal = ({
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// Créer une demande pour chaque type de congé
|
||||
const requests = formData.types.map(type => {
|
||||
const days = formData.types.length > 1 ? (typeDistribution[type] || 0) : calculatedDays;
|
||||
// Utiliser le type sélectionné dans la liste déroulante si "Autre" est coché
|
||||
const finalType = type === 'Autres' ? otherLeaveType : type;
|
||||
return {
|
||||
EmployeeId: userId,
|
||||
TypeConge: type,
|
||||
DateDebut: formData.startDate,
|
||||
DateFin: formData.endDate,
|
||||
Commentaire: formData.reason + (formData.types.length > 1 ? ` (${days} jours ${getTypeLabel(type)})` : ''),
|
||||
NumDays: days
|
||||
};
|
||||
});
|
||||
|
||||
// Soumettre toutes les demandes
|
||||
const responses = await Promise.all(
|
||||
requests.map(requestData =>
|
||||
fetch('http://localhost/GTA/project/public/submitLeaveRequest.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData),
|
||||
})
|
||||
)
|
||||
const finalTypes = formData.types.map(type =>
|
||||
type === 'Autres' ? otherLeaveType : type
|
||||
);
|
||||
|
||||
const results = await Promise.all(responses.map(r => r.json()));
|
||||
const repartition = finalTypes.map(type => ({
|
||||
TypeConge: type,
|
||||
NombreJours: formData.types.length > 1
|
||||
? (typeDistribution[type] || 0)
|
||||
: calculatedDays
|
||||
}));
|
||||
|
||||
const allSuccessful = results.every(result => result.success);
|
||||
const requestData = {
|
||||
EmployeeId: userId,
|
||||
DateDebut: formData.startDate,
|
||||
DateFin: formData.endDate,
|
||||
Commentaire: formData.reason,
|
||||
NombreJours: calculatedDays,
|
||||
Repartition: repartition
|
||||
};
|
||||
console.log("Payload envoyé au backend :", JSON.stringify(requestData, null, 2));
|
||||
|
||||
if (allSuccessful) {
|
||||
const formDataToSend = new FormData();
|
||||
formDataToSend.append('data', JSON.stringify(requestData));
|
||||
|
||||
// Ajouter les fichiers
|
||||
formData.medicalDocuments.forEach((file, index) => {
|
||||
formDataToSend.append(`medicalDocuments[]`, file);
|
||||
});
|
||||
|
||||
const response = await fetch('http://localhost/GTA/project/public/submitLeaveRequest.php', {
|
||||
method: 'POST',
|
||||
body: formDataToSend
|
||||
});
|
||||
|
||||
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
onRequestSubmitted?.();
|
||||
onClose();
|
||||
} else {
|
||||
const failedResults = results.filter(r => !r.success);
|
||||
setError(`Erreur lors de la soumission : ${failedResults.map(r => r.message).join(', ')}`);
|
||||
setError(result.message || 'Erreur lors de la soumission');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
setError('Erreur de connexion au serveur');
|
||||
@@ -309,6 +321,7 @@ const NewLeaveRequestModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getTypeLabel = (type) => {
|
||||
switch (type) {
|
||||
case 'CP': return 'Congés payés';
|
||||
|
||||
@@ -89,14 +89,11 @@ const Login = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
aria-label={showPassword ? "Masquer le mot de passe" : "Afficher le mot de passe"}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-900"
|
||||
title={showPassword ? "Masquer le mot de passe" : "Afficher le mot de passe"}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="w-5 h-5" />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" />
|
||||
)}
|
||||
{/* L'icône est choisie en fonction de l'état de showPassword */}
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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, Search, Filter, Eye, Edit, Trash2, Menu } from 'lucide-react';
|
||||
import { Calendar as CalendarIcon, Clock, Plus, Settings, RefreshCw, Search, Filter, Eye, Edit, Trash2, Menu, X } from 'lucide-react';
|
||||
import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
||||
|
||||
const Requests = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [leaveCounters, setLeaveCounters] = useState({
|
||||
availableCP: 0,
|
||||
@@ -14,14 +15,19 @@ const Requests = () => {
|
||||
rttInProcess: 0,
|
||||
absenteism: 0
|
||||
});
|
||||
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showAdminPanel, setShowAdminPanel] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [allRequests, setAllRequests] = useState([]);
|
||||
const [filteredRequests, setFilteredRequests] = useState([]);
|
||||
const [selectedRequest, setSelectedRequest] = useState(null); // 👈 Nouveau
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [typeFilter, setTypeFilter] = useState('all');
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [requestsPerPage] = useState(10);
|
||||
|
||||
@@ -32,11 +38,9 @@ const Requests = () => {
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// Filtrage des demandes
|
||||
useEffect(() => {
|
||||
let filtered = allRequests;
|
||||
|
||||
// Filtre par terme de recherche
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(request =>
|
||||
request.type.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
@@ -45,12 +49,10 @@ const Requests = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Filtre par statut
|
||||
if (statusFilter !== 'all') {
|
||||
filtered = filtered.filter(request => request.status === statusFilter);
|
||||
}
|
||||
|
||||
// Filtre par type
|
||||
if (typeFilter !== 'all') {
|
||||
if (typeFilter === 'Autres') {
|
||||
const otherTypes = ['Récup', 'Congés sans solde', 'Congés pour évènement familial', 'Congé maternité', 'Congé paternité', 'Congé parental', 'Congé parental à temps partiel'];
|
||||
@@ -61,20 +63,18 @@ const Requests = () => {
|
||||
}
|
||||
|
||||
setFilteredRequests(filtered);
|
||||
setCurrentPage(1); // Reset à la première page lors du filtrage
|
||||
setCurrentPage(1);
|
||||
}, [allRequests, searchTerm, statusFilter, typeFilter]);
|
||||
|
||||
const fetchLeaveCounters = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost/GTA/project/public/getLeaveCounters.php?user_id=${user.id}`);
|
||||
const text = await response.text();
|
||||
console.log(' Requests - Réponse brute compteurs:', text);
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (parseError) {
|
||||
console.error(' Requests - Réponse non-JSON:', text.substring(0, 200));
|
||||
} catch {
|
||||
throw new Error('Le serveur PHP ne répond pas correctement');
|
||||
}
|
||||
|
||||
@@ -84,62 +84,49 @@ const Requests = () => {
|
||||
throw new Error(data.message || 'Erreur lors de la récupération des compteurs');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des compteurs:', error);
|
||||
console.error('Erreur compteurs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAllRequests = async () => {
|
||||
console.log('Requests - Début fetchAllRequests pour user:', user?.id);
|
||||
|
||||
try {
|
||||
const url = `http://localhost/GTA/project/public/getRequests.php?user_id=${user.id}`;
|
||||
console.log(' Requests - URL appelée:', url);
|
||||
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
console.log(' Requests - Réponse brute:', text);
|
||||
|
||||
const data = JSON.parse(text);
|
||||
console.log(' Requests - Données parsées:', data);
|
||||
|
||||
if (data.success) {
|
||||
console.log(' Requests - Demandes récupérées:', data.requests?.length);
|
||||
setAllRequests(data.requests || []);
|
||||
} else {
|
||||
throw new Error(data.message || 'Erreur lors de la récupération des demandes');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' Requests - Erreur:', error);
|
||||
console.error('Erreur requêtes:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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('Réinitialiser les compteurs ? Cette action est irréversible.')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost/GTA/project/public/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');
|
||||
} catch {
|
||||
alert('Erreur de connexion au serveur');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,41 +139,26 @@ const Requests = () => {
|
||||
case 'Approuvé':
|
||||
case 'Validée': return 'bg-green-100 text-green-800';
|
||||
case 'En attente': return 'bg-yellow-100 text-yellow-800';
|
||||
case 'Refusé': return 'bg-red-100 text-red-800';
|
||||
case 'Refusé':
|
||||
case 'Refusée': return 'bg-red-100 text-red-800';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
// Pagination
|
||||
const handleViewRequest = (request) => {
|
||||
setSelectedRequest(request);
|
||||
};
|
||||
|
||||
const handleCloseDetails = () => {
|
||||
setSelectedRequest(null);
|
||||
};
|
||||
|
||||
const indexOfLastRequest = currentPage * requestsPerPage;
|
||||
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
|
||||
const currentRequests = filteredRequests.slice(indexOfFirstRequest, indexOfLastRequest);
|
||||
const totalPages = Math.ceil(filteredRequests.length / requestsPerPage);
|
||||
|
||||
const paginate = (pageNumber) => setCurrentPage(pageNumber);
|
||||
|
||||
const handleViewRequest = (request) => {
|
||||
alert(`Détails de la demande:\n\nType: ${request.type}\nDates: ${request.dateDisplay}\nJours: ${request.days}\nStatut: ${request.status}\nMotif: ${request.reason}`);
|
||||
};
|
||||
|
||||
const handleEditRequest = (request) => {
|
||||
if (request.status !== 'En attente') {
|
||||
alert('Seules les demandes en attente peuvent être modifiées.');
|
||||
return;
|
||||
}
|
||||
alert('Fonctionnalité de modification en cours de développement.');
|
||||
};
|
||||
|
||||
const handleDeleteRequest = (request) => {
|
||||
if (request.status !== 'En attente') {
|
||||
alert('Seules les demandes en attente peuvent être supprimées.');
|
||||
return;
|
||||
}
|
||||
if (confirm(`Êtes-vous sûr de vouloir supprimer cette demande de ${request.type} ?`)) {
|
||||
alert('Fonctionnalité de suppression en cours de développement.');
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
@@ -206,12 +178,9 @@ const Requests = () => {
|
||||
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||
|
||||
<div className="flex-1 lg:ml-60 p-4 lg:p-8">
|
||||
{/* Mobile menu button */}
|
||||
{/* Bouton mobile */}
|
||||
<div className="lg:hidden mb-4">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="p-2 rounded-lg bg-white shadow-sm border border-gray-200"
|
||||
>
|
||||
<button onClick={() => setSidebarOpen(true)} className="p-2 rounded-lg bg-white shadow-sm border border-gray-200">
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -222,12 +191,8 @@ const Requests = () => {
|
||||
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
||||
Mes Demandes de Congés
|
||||
</h1>
|
||||
<p className="text-sm lg:text-base text-gray-600">
|
||||
Gérez toutes vos demandes de congés
|
||||
</p>
|
||||
<p className="text-sm lg:text-base text-gray-600">Gérez toutes vos demandes de congés</p>
|
||||
</div>
|
||||
<div className="flex gap-2 lg:gap-3">
|
||||
|
||||
<button
|
||||
onClick={() => setShowNewRequestModal(true)}
|
||||
className="bg-blue-600 text-white px-3 lg:px-6 py-2 lg:py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
|
||||
@@ -237,139 +202,26 @@ const Requests = () => {
|
||||
<span className="sm:hidden">Nouveau</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin Panel */}
|
||||
{showAdminPanel && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
Administration
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowAdminPanel(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
|
||||
<h3 className="font-medium text-yellow-800 mb-2">⚠️ Zone d'administration</h3>
|
||||
<p className="text-yellow-700 text-sm">
|
||||
Ces actions affectent tous les utilisateurs du système. Utilisez avec précaution.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={handleResetCounters}
|
||||
className="flex items-center gap-3 p-4 border border-red-200 rounded-lg hover:bg-red-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
|
||||
<RefreshCw className="w-5 h-5 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">Réinitialiser les compteurs</h3>
|
||||
<p className="text-sm text-gray-600">Remet à zéro tous les compteurs selon les règles</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={openManualResetPage}
|
||||
className="flex items-center gap-3 p-4 border border-blue-200 rounded-lg hover:bg-blue-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Settings className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">Interface d'administration</h3>
|
||||
<p className="text-sm text-gray-600">Ouvre l'interface complète d'administration</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6 mb-8">
|
||||
<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">CP restants</p>
|
||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">{leaveCounters.availableCP}</p>
|
||||
<p className="text-xs text-gray-500">jours</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<CalendarIcon className="w-4 h-4 lg:w-6 lg:h-6 text-blue-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">RTT restants</p>
|
||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">{leaveCounters.availableRTT}</p>
|
||||
<p className="text-xs text-gray-500">jours</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<Clock 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">RTT en cours</p>
|
||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">{leaveCounters.rttInProcess}</p>
|
||||
<p className="text-xs text-gray-500">en cours</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-yellow-100 rounded-lg flex items-center justify-center">
|
||||
<Users className="w-4 h-4 lg:w-6 lg:h-6 text-yellow-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">Absences</p>
|
||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">{leaveCounters.absenteism}</p>
|
||||
<p className="text-xs text-gray-500">jours</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<TrendingUp className="w-4 h-4 lg:w-6 lg:h-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtres et Recherche */}
|
||||
{/* Filtres */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 lg:p-6 mb-6">
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
{/* Barre de recherche */}
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher par type, motif ou date..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<div className="flex gap-3">
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="all">Tous les statuts</option>
|
||||
<option value="En attente">En attente</option>
|
||||
@@ -380,31 +232,30 @@ const Requests = () => {
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="all">Tous les types</option>
|
||||
<option value="Congés payés">Congés payés</option>
|
||||
<option value="RTT">RTT</option>
|
||||
<option value="Congé maladie">Congé maladie</option>
|
||||
<option value="Autres">Autres types de congés</option> {/* Nouvelle option */}
|
||||
<option value="Autres">Autres types</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistiques des résultats */}
|
||||
<div className="mt-4 text-sm text-gray-600">
|
||||
{filteredRequests.length} demande{filteredRequests.length > 1 ? 's' : ''} trouvée{filteredRequests.length > 1 ? 's' : ''}
|
||||
{allRequests.length !== filteredRequests.length && ` sur ${allRequests.length} au total`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Liste des Demandes */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
{/* Tableau + Détails */}
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Tableau des demandes */}
|
||||
<div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="p-6 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
Toutes mes demandes
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Toutes mes demandes</h2>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Filter className="w-4 h-4" />
|
||||
Page {currentPage} sur {totalPages || 1}
|
||||
@@ -414,203 +265,104 @@ const Requests = () => {
|
||||
|
||||
<div className="p-6">
|
||||
{currentRequests.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<CalendarIcon className="w-6 h-6 text-gray-400" />
|
||||
</div>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{filteredRequests.length === 0 && allRequests.length > 0
|
||||
? 'Aucune demande ne correspond à vos critères'
|
||||
: 'Aucune demande trouvée'
|
||||
}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowNewRequestModal(true)}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||||
>
|
||||
{allRequests.length === 0 ? 'Faire votre première demande' : 'Créer une nouvelle demande'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-center py-8 text-gray-600">Aucune demande à afficher.</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Version Desktop */}
|
||||
<div className="hidden lg:block">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Type</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Dates</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Durée</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Statut</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Soumis le</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Actions</th>
|
||||
<th className="text-left py-3 px-4 text-gray-700">Type</th>
|
||||
<th className="text-left py-3 px-4 text-gray-700">Dates</th>
|
||||
<th className="text-left py-3 px-4 text-gray-700">Jours</th>
|
||||
<th className="text-left py-3 px-4 text-gray-700">Statut</th>
|
||||
<th className="text-left py-3 px-4 text-gray-700">Soumis</th>
|
||||
<th className="text-left py-3 px-4 text-gray-700">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentRequests.map((request) => (
|
||||
<tr key={request.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="py-4 px-4">
|
||||
<span className="font-medium text-gray-900">{request.type}</span>
|
||||
</td>
|
||||
<td className="py-4 px-4 text-gray-600">{request.dateDisplay}</td>
|
||||
<td className="py-4 px-4 text-gray-600">{request.days} jour{request.days > 1 ? 's' : ''}</td>
|
||||
{currentRequests.map(request => (
|
||||
<tr key={request.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-4 px-4">{request.type}</td>
|
||||
<td className="py-4 px-4">{request.dateDisplay}</td>
|
||||
<td className="py-4 px-4">{request.days}</td>
|
||||
<td className="py-4 px-4">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(request.status)}`}>
|
||||
{request.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-4 text-gray-600">{request.submittedDisplay}</td>
|
||||
<td className="py-4 px-4">{request.submittedDisplay}</td>
|
||||
<td className="py-4 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleViewRequest(request)}
|
||||
className="p-1 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded"
|
||||
title="Voir les détails"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
<button onClick={() => handleViewRequest(request)} className="text-blue-600 hover:underline text-sm flex items-center gap-1">
|
||||
<Eye className="w-4 h-4" /> Voir
|
||||
</button>
|
||||
{request.status === 'En attente' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleEditRequest(request)}
|
||||
className="p-1 text-green-600 hover:text-green-800 hover:bg-green-50 rounded"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteRequest(request)}
|
||||
className="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version Mobile */}
|
||||
<div className="lg:hidden space-y-4">
|
||||
{currentRequests.map((request) => (
|
||||
<div key={request.id} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{request.type}</h3>
|
||||
<p className="text-sm text-gray-600">{request.dateDisplay}</p>
|
||||
{/* Détails */}
|
||||
{selectedRequest && (
|
||||
<div className="w-full lg:max-w-sm bg-white rounded-xl shadow-md border border-gray-100 p-6 h-fit sticky top-20 self-start">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<h3 className="text-lg font-semibold">Détails de la demande</h3>
|
||||
<button onClick={handleCloseDetails} className="text-gray-500 hover:text-gray-800 p-2">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(request.status)}`}>
|
||||
{request.status}
|
||||
|
||||
<div className="space-y-4 text-sm text-gray-700">
|
||||
<div>
|
||||
<p className="text-gray-500">Type</p>
|
||||
<p className="text-base font-medium text-gray-900">{selectedRequest.type}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Dates</p>
|
||||
<p className="text-base font-medium text-gray-900">{selectedRequest.dateDisplay}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Nombre de jours</p>
|
||||
<p className="text-base font-medium text-gray-900">{selectedRequest.days}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">Statut</p>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(selectedRequest.status)}`}>
|
||||
{selectedRequest.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-600 mb-3">
|
||||
<span>{request.days} jour{request.days > 1 ? 's' : ''}</span>
|
||||
<span>Soumis le {request.submittedDisplay}</span>
|
||||
{selectedRequest.reason && (
|
||||
<div>
|
||||
<p className="text-gray-500">Motif</p>
|
||||
<p className="italic">{selectedRequest.reason}</p>
|
||||
</div>
|
||||
|
||||
{request.reason && request.reason !== 'Aucun commentaire' && (
|
||||
<p className="text-sm text-gray-600 mb-3 italic">"{request.reason}"</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 pt-3 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => handleViewRequest(request)}
|
||||
className="flex items-center gap-1 text-blue-600 hover:text-blue-800 text-sm"
|
||||
{selectedRequest.fileUrl && (
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500">Arrêt maladie</p>
|
||||
<a
|
||||
href={selectedRequest.fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline flex items-center gap-2"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
Voir
|
||||
</button>
|
||||
{request.status === 'En attente' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleEditRequest(request)}
|
||||
className="flex items-center gap-1 text-green-600 hover:text-green-800 text-sm"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteRequest(request)}
|
||||
className="flex items-center gap-1 text-red-600 hover:text-red-800 text-sm"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Supprimer
|
||||
</button>
|
||||
</>
|
||||
Voir le fichier
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-6 pt-6 border-t border-gray-100">
|
||||
<div className="text-sm text-gray-600">
|
||||
Affichage de {indexOfFirstRequest + 1} à {Math.min(indexOfLastRequest, filteredRequests.length)} sur {filteredRequests.length} demandes
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 border border-gray-300 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
||||
>
|
||||
Précédent
|
||||
</button>
|
||||
|
||||
{[...Array(totalPages)].map((_, index) => {
|
||||
const pageNumber = index + 1;
|
||||
if (
|
||||
pageNumber === 1 ||
|
||||
pageNumber === totalPages ||
|
||||
(pageNumber >= currentPage - 1 && pageNumber <= currentPage + 1)
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
key={pageNumber}
|
||||
onClick={() => paginate(pageNumber)}
|
||||
className={`px-3 py-1 border rounded text-sm ${currentPage === pageNumber
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{pageNumber}
|
||||
</button>
|
||||
);
|
||||
} else if (
|
||||
pageNumber === currentPage - 2 ||
|
||||
pageNumber === currentPage + 2
|
||||
) {
|
||||
return <span key={pageNumber} className="px-2 text-gray-400">...</span>;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
||||
<button
|
||||
onClick={() => paginate(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1 border border-gray-300 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
||||
>
|
||||
Suivant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal nouvelle demande */}
|
||||
{/* Modal */}
|
||||
{showNewRequestModal && (
|
||||
<NewLeaveRequestModal
|
||||
onClose={() => setShowNewRequestModal(false)}
|
||||
|
||||
BIN
project/uploads/doc_6895edf8d6538.jpg
Normal file
BIN
project/uploads/doc_6895edf8d6538.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 501 KiB |
Reference in New Issue
Block a user