Ajoutez des fichiers projet.
This commit is contained in:
25
project/.gitignore
vendored
Normal file
25
project/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.env
|
||||||
13
project/index.html
Normal file
13
project/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GTA</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2913
project/package-lock.json
generated
Normal file
2913
project/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
project/package.json
Normal file
24
project/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "gestion-conges-jsx",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.344.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^7.7.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.18",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"vite": "^5.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
project/postcss.config.js
Normal file
6
project/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
project/public/assets/ImageEnsup.png
Normal file
BIN
project/public/assets/ImageEnsup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
project/public/assets/Logo.png
Normal file
BIN
project/public/assets/Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
project/public/assets/Logo_Ensitech.png
Normal file
BIN
project/public/assets/Logo_Ensitech.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
project/public/assets/utilisateur.png
Normal file
BIN
project/public/assets/utilisateur.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
156
project/public/getAllTeamRequests.php
Normal file
156
project/public/getAllTeamRequests.php
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<?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();
|
||||||
|
?>
|
||||||
278
project/public/getLeaveCounters.php
Normal file
278
project/public/getLeaveCounters.php
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<?php
|
||||||
|
// Récupération des compteurs de congés avec gestion des exercices
|
||||||
|
// Exercice CP: 01/06 au 31/05 | Exercice RTT: 01/01 au 31/12
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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_";
|
||||||
|
|
||||||
|
// IMPORTANT: Changer ces paramètres pour votre configuration locale
|
||||||
|
// $host = "localhost";
|
||||||
|
// $username = "root";
|
||||||
|
// $password = "";
|
||||||
|
|
||||||
|
// 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 getLeaveCounters: " . $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 getLeaveCounters.php ===");
|
||||||
|
error_log("getLeaveCounters - user_id reçu: " . ($userId ?? 'NULL'));
|
||||||
|
error_log("getLeaveCounters - Toutes les variables GET: " . print_r($_GET, true));
|
||||||
|
|
||||||
|
if ($userId === null) {
|
||||||
|
error_log("getLeaveCounters - user_id manquant");
|
||||||
|
echo json_encode(["success" => false, "message" => "ID utilisateur manquant."]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
// Fonction pour déterminer l'exercice des congés payés (01/06 au 31/05)
|
||||||
|
function getLeaveYear($date = null) {
|
||||||
|
if ($date === null) {
|
||||||
|
$date = new DateTime();
|
||||||
|
} else {
|
||||||
|
$date = new DateTime($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentYear = (int)$date->format('Y');
|
||||||
|
$currentMonth = (int)$date->format('m');
|
||||||
|
|
||||||
|
// Si on est avant le 1er juin, l'exercice a commencé l'année précédente
|
||||||
|
if ($currentMonth < 6) {
|
||||||
|
return $currentYear - 1;
|
||||||
|
}
|
||||||
|
// Si on est le 1er juin ou après, l'exercice a commencé cette année
|
||||||
|
return $currentYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour déterminer l'année RTT (01/01 au 31/12)
|
||||||
|
function getRTTYear($date = null) {
|
||||||
|
if ($date === null) {
|
||||||
|
$date = new DateTime();
|
||||||
|
} else {
|
||||||
|
$date = new DateTime($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)$date->format('Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère l'ID utilisateur depuis les paramètres de requête GET
|
||||||
|
$userId = $_GET['user_id'] ?? null;
|
||||||
|
|
||||||
|
if ($userId === null) {
|
||||||
|
echo json_encode(["success" => false, "message" => "ID utilisateur manquant."]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcul des exercices selon les règles de gestion
|
||||||
|
$leaveYear = getLeaveYear(); // Exercice CP (01/06 au 31/05)
|
||||||
|
$rttYear = getRTTYear(); // Exercice RTT (01/01 au 31/12)
|
||||||
|
$currentDate = date('Y-m-d'); // Date actuelle pour les filtres de demandes
|
||||||
|
|
||||||
|
// Variables pour les soldes disponibles
|
||||||
|
$cpSolde = 0;
|
||||||
|
$rttSolde = 0;
|
||||||
|
$absSolde = 0;
|
||||||
|
|
||||||
|
// Variables pour les demandes en cours/validées
|
||||||
|
$cpInProcess = 0;
|
||||||
|
$rttInProcess = 0;
|
||||||
|
$absenteism = 0;
|
||||||
|
|
||||||
|
// --- FONCTION UTILITAIRE 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 (pour Lundi) à 7 (pour Dimanche)
|
||||||
|
if ($dayOfWeek < 6) { // Si ce n'est ni Samedi (6) ni Dimanche (7)
|
||||||
|
$workingDays++;
|
||||||
|
}
|
||||||
|
$current->modify('+1 day');
|
||||||
|
}
|
||||||
|
return $workingDays;
|
||||||
|
}
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
// --- Récupération du Solde de Congé Payé (CP) ---
|
||||||
|
$queryCPSolde = "SELECT cc.Solde FROM CompteurConges cc
|
||||||
|
JOIN TypeConge tc ON cc.TypeCongeId = tc.Id
|
||||||
|
WHERE cc.EmployeeId = ? AND tc.Nom = 'Congé payé' AND cc.Annee = ?";
|
||||||
|
$stmtCPSolde = $conn->prepare($queryCPSolde);
|
||||||
|
if ($stmtCPSolde === false) {
|
||||||
|
error_log("Erreur de préparation de la requête CP Solde : " . $conn->error);
|
||||||
|
} else {
|
||||||
|
$stmtCPSolde->bind_param("ii", $userId, $leaveYear);
|
||||||
|
$stmtCPSolde->execute();
|
||||||
|
$resultCPSolde = $stmtCPSolde->get_result();
|
||||||
|
if ($rowCPSolde = $resultCPSolde->fetch_assoc()) {
|
||||||
|
$cpSolde = $rowCPSolde['Solde'];
|
||||||
|
}
|
||||||
|
$stmtCPSolde->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Récupération du Solde de RTT ---
|
||||||
|
$queryRTTSolde = "SELECT cc.Solde FROM CompteurConges cc
|
||||||
|
JOIN TypeConge tc ON tc.Id = cc.TypeCongeId
|
||||||
|
WHERE cc.EmployeeId = ? AND tc.Nom = 'RTT' AND cc.Annee = ?";
|
||||||
|
$stmtRTTSolde = $conn->prepare($queryRTTSolde);
|
||||||
|
if ($stmtRTTSolde === false) {
|
||||||
|
error_log("Erreur de préparation de la requête RTT Solde : " . $conn->error);
|
||||||
|
} else {
|
||||||
|
$stmtRTTSolde->bind_param("ii", $userId, $rttYear);
|
||||||
|
$stmtRTTSolde->execute();
|
||||||
|
$resultRTTSolde = $stmtRTTSolde->get_result();
|
||||||
|
if ($rowRTTSolde = $resultRTTSolde->fetch_assoc()) {
|
||||||
|
$rttSolde = $rowRTTSolde['Solde'];
|
||||||
|
}
|
||||||
|
$stmtRTTSolde->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Récupération du Solde de Congé Maladie (ABS) ---
|
||||||
|
$queryABSSolde = "SELECT cc.Solde FROM CompteurConges cc
|
||||||
|
JOIN TypeConge tc ON tc.Id = cc.TypeCongeId
|
||||||
|
WHERE cc.EmployeeId = ? AND tc.Nom = 'Congé maladie' AND cc.Annee = ?";
|
||||||
|
$stmtABSSolde = $conn->prepare($queryABSSolde);
|
||||||
|
if ($stmtABSSolde === false) {
|
||||||
|
error_log("Erreur de préparation de la requête ABS Solde : " . $conn->error);
|
||||||
|
} else {
|
||||||
|
$stmtABSSolde->bind_param("ii", $userId, $rttYear);
|
||||||
|
$stmtABSSolde->execute();
|
||||||
|
$resultABSSolde = $stmtABSSolde->get_result();
|
||||||
|
if ($rowABSSolde = $resultABSSolde->fetch_assoc()) {
|
||||||
|
$absSolde = $rowABSSolde['Solde'];
|
||||||
|
}
|
||||||
|
$stmtABSSolde->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Calcul des Congés Payés (CP) en cours (demandes 'En attente' ou 'Validée' dont la fin est >= date actuelle) ---
|
||||||
|
// Cette requête sélectionne les dates pour le calcul en PHP
|
||||||
|
$queryCPInProcessDates = "SELECT dc.DateDebut, dc.DateFin FROM DemandeConge dc
|
||||||
|
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||||||
|
WHERE dc.EmployeeId = ?
|
||||||
|
AND tc.Nom = 'Congé payé'
|
||||||
|
AND dc.Statut IN ('En attente', 'Validée')
|
||||||
|
AND dc.DateFin >= ?";
|
||||||
|
$stmtCPInProcessDates = $conn->prepare($queryCPInProcessDates);
|
||||||
|
if ($stmtCPInProcessDates === false) {
|
||||||
|
error_log("Erreur de préparation de la requête CP en cours dates : " . $conn->error);
|
||||||
|
} else {
|
||||||
|
$stmtCPInProcessDates->bind_param("is", $userId, $currentDate);
|
||||||
|
$stmtCPInProcessDates->execute();
|
||||||
|
$resultCPInProcessDates = $stmtCPInProcessDates->get_result();
|
||||||
|
while ($row = $resultCPInProcessDates->fetch_assoc()) {
|
||||||
|
$cpInProcess += getWorkingDays($row['DateDebut'], $row['DateFin']);
|
||||||
|
}
|
||||||
|
$stmtCPInProcessDates->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Calcul des RTT en cours (mêmes critères que CP, mais pour RTT) ---
|
||||||
|
$queryRTTInProcessDates = "SELECT dc.DateDebut, dc.DateFin FROM DemandeConge dc
|
||||||
|
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||||||
|
WHERE dc.EmployeeId = ?
|
||||||
|
AND tc.Nom = 'RTT'
|
||||||
|
AND dc.Statut IN ('En attente', 'Validée')
|
||||||
|
AND dc.DateFin >= ?";
|
||||||
|
$stmtRTTInProcessDates = $conn->prepare($queryRTTInProcessDates);
|
||||||
|
if ($stmtRTTInProcessDates === false) {
|
||||||
|
error_log("Erreur de préparation de la requête RTT en cours dates : " . $conn->error);
|
||||||
|
} else {
|
||||||
|
$stmtRTTInProcessDates->bind_param("is", $userId, $currentDate);
|
||||||
|
$stmtRTTInProcessDates->execute();
|
||||||
|
$resultRTTInProcessDates = $stmtRTTInProcessDates->get_result();
|
||||||
|
while ($row = $resultRTTInProcessDates->fetch_assoc()) {
|
||||||
|
$rttInProcess += getWorkingDays($row['DateDebut'], $row['DateFin']);
|
||||||
|
}
|
||||||
|
$stmtRTTInProcessDates->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Calcul des jours d'absence (ABS) (somme des jours DATEDIFF à partir de DemandeConge) ---
|
||||||
|
// Note: Ici, on ne modifie pas le calcul, car l'absentéisme maladie est souvent compté sur tous les jours, y compris week-ends, pour le suivi global.
|
||||||
|
// Si vous devez exclure les week-ends pour les ABS, appliquez getWorkingDays ici aussi.
|
||||||
|
$queryABSInProcess = "SELECT SUM(DATEDIFF(dc.DateFin, dc.DateDebut) + 1) AS total_abs FROM DemandeConge dc
|
||||||
|
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||||||
|
WHERE dc.EmployeeId = ?
|
||||||
|
AND tc.Nom = 'Congé maladie'
|
||||||
|
AND dc.Statut = 'Validée'";
|
||||||
|
$stmtABSInProcess = $conn->prepare($queryABSInProcess);
|
||||||
|
if ($stmtABSInProcess === false) {
|
||||||
|
error_log("Erreur de préparation de la requête ABS en cours : " . $conn->error);
|
||||||
|
} else {
|
||||||
|
$stmtABSInProcess->bind_param("i", $userId);
|
||||||
|
$stmtABSInProcess->execute();
|
||||||
|
$resultABSInProcess = $stmtABSInProcess->get_result();
|
||||||
|
if ($rowABSInProcess = $resultABSInProcess->fetch_assoc()) {
|
||||||
|
$absenteism = $rowABSInProcess['total_abs'] ?? 0;
|
||||||
|
}
|
||||||
|
$stmtABSInProcess->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Calcul des soldes disponibles réels (déduction "douce" pour l'affichage/validation frontend) ---
|
||||||
|
$availableCPCalculated = $cpSolde - $cpInProcess;
|
||||||
|
if ($availableCPCalculated < 0) {
|
||||||
|
$availableCPCalculated = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$availableRTTCalculated = $rttSolde - $rttInProcess;
|
||||||
|
if ($availableRTTCalculated < 0) {
|
||||||
|
$availableRTTCalculated = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Renvoie les compteurs sous format JSON
|
||||||
|
echo json_encode([
|
||||||
|
"success" => true,
|
||||||
|
"message" => "Compteurs récupérés avec succès.",
|
||||||
|
"counters" => [
|
||||||
|
"availableCP" => (int)$availableCPCalculated, // CP: Solde brut - jours ouvrés en cours/validés futurs
|
||||||
|
"availableRTT" => (int)$availableRTTCalculated, // RTT: Solde brut - jours ouvrés en cours/validés futurs
|
||||||
|
"availableABS" => (int)$absSolde, // ABS: Solde brut (sans déduction des jours en cours)
|
||||||
|
"rttInProcess" => (int)$rttInProcess, // RTT: Jours ouvrés en attente/validés futurs (pour information)
|
||||||
|
"absenteism" => (int)$absenteism // ABS: Jours d'absence maladie validés/pris (pour information)
|
||||||
|
],
|
||||||
|
"debug_values" => [
|
||||||
|
"initial_cp_solde" => (int)$cpSolde,
|
||||||
|
"cp_en_cours" => (int)$cpInProcess,
|
||||||
|
"calculated_available_cp" => (int)$availableCPCalculated,
|
||||||
|
"initial_rtt_solde" => (int)$rttSolde,
|
||||||
|
"rtt_en_cours" => (int)$rttInProcess,
|
||||||
|
"calculated_available_rtt" => (int)$availableRTTCalculated,
|
||||||
|
"leave_year" => $leaveYear,
|
||||||
|
"rtt_year" => $rttYear,
|
||||||
|
"current_date_php" => $currentDate,
|
||||||
|
"user_id_php" => (int)$userId
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
?>
|
||||||
156
project/public/getPendingRequests.php
Normal file
156
project/public/getPendingRequests.php
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<?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();
|
||||||
|
?>
|
||||||
195
project/public/getRequests.php
Normal file
195
project/public/getRequests.php
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
// Récupération des demandes de congés avec gestion des exercices
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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_";
|
||||||
|
|
||||||
|
// 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 ===");
|
||||||
|
error_log("getRequests - user_id reçu: " . ($userId ?? 'NULL'));
|
||||||
|
error_log("getRequests - Toutes les variables GET: " . print_r($_GET, true));
|
||||||
|
|
||||||
|
if ($userId === null) {
|
||||||
|
error_log("getRequests - user_id manquant");
|
||||||
|
echo json_encode(["success" => false, "message" => "ID utilisateur manquant."]);
|
||||||
|
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) {
|
||||||
|
$checkUserStmt->bind_param("i", $userId);
|
||||||
|
$checkUserStmt->execute();
|
||||||
|
$userResult = $checkUserStmt->get_result();
|
||||||
|
if ($userRow = $userResult->fetch_assoc()) {
|
||||||
|
error_log("getRequests - Utilisateur trouvé: " . $userRow['Prenom'] . " " . $userRow['Nom']);
|
||||||
|
} else {
|
||||||
|
error_log("getRequests - ATTENTION: Utilisateur ID $userId non trouvé dans la table Users");
|
||||||
|
}
|
||||||
|
$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)
|
||||||
|
$workingDays++;
|
||||||
|
}
|
||||||
|
$current->modify('+1 day');
|
||||||
|
}
|
||||||
|
return $workingDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Requête pour récupérer les demandes de l'utilisateur avec les informations du type de congé
|
||||||
|
$query = "
|
||||||
|
SELECT
|
||||||
|
dc.Id,
|
||||||
|
dc.DateDebut,
|
||||||
|
dc.DateFin,
|
||||||
|
dc.Statut,
|
||||||
|
dc.DateDemande,
|
||||||
|
dc.Commentaire,
|
||||||
|
dc.Validateur,
|
||||||
|
tc.Nom as TypeConge
|
||||||
|
FROM DemandeConge dc
|
||||||
|
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
|
||||||
|
WHERE dc.EmployeeId = ?
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->bind_param("i", $userId);
|
||||||
|
$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é':
|
||||||
|
$displayType = 'Congés payés';
|
||||||
|
break;
|
||||||
|
case 'RTT':
|
||||||
|
$displayType = 'RTT';
|
||||||
|
break;
|
||||||
|
case 'Congé maladie':
|
||||||
|
$displayType = 'Congé maladie';
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
$requests[] = [
|
||||||
|
'id' => (int)$row['Id'],
|
||||||
|
'type' => $displayType,
|
||||||
|
'startDate' => $row['DateDebut'],
|
||||||
|
'endDate' => $row['DateFin'],
|
||||||
|
'dateDisplay' => $dateDisplay,
|
||||||
|
'days' => $workingDays,
|
||||||
|
'status' => $row['Statut'],
|
||||||
|
'reason' => $row['Commentaire'] ?: 'Aucun commentaire',
|
||||||
|
'submittedAt' => $row['DateDemande'],
|
||||||
|
'submittedDisplay' => $submittedDate->format('d/m/Y'),
|
||||||
|
'validator' => $row['Validateur'] ?: null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$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.",
|
||||||
|
"requests" => $requests,
|
||||||
|
"total" => count($requests)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Erreur récupération demandes : " . $e->getMessage());
|
||||||
|
echo json_encode([
|
||||||
|
"success" => false,
|
||||||
|
"message" => "Erreur lors de la récupération des demandes : " . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
?>
|
||||||
115
project/public/getTeamLeaves.php
Normal file
115
project/public/getTeamLeaves.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
// Récupération des congés de l'équipe pour affichage dans le calendrier
|
||||||
|
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 getTeamLeaves: " . $conn->connect_error);
|
||||||
|
echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données"]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $_GET['user_id'] ?? null;
|
||||||
|
|
||||||
|
if ($userId === null) {
|
||||||
|
echo json_encode(["success" => false, "message" => "ID utilisateur manquant"]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("getTeamLeaves - User ID: $userId");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Récupérer le service de l'utilisateur
|
||||||
|
$queryUserService = "SELECT ServiceId FROM Users WHERE ID = ?";
|
||||||
|
$stmtUser = $conn->prepare($queryUserService);
|
||||||
|
$stmtUser->bind_param("i", $userId);
|
||||||
|
$stmtUser->execute();
|
||||||
|
$resultUser = $stmtUser->get_result();
|
||||||
|
|
||||||
|
if ($userRow = $resultUser->fetch_assoc()) {
|
||||||
|
$serviceId = $userRow['ServiceId'];
|
||||||
|
error_log("getTeamLeaves - Service ID: $serviceId");
|
||||||
|
|
||||||
|
// Récupérer les congés validés de l'équipe (même service)
|
||||||
|
$queryLeaves = "
|
||||||
|
SELECT
|
||||||
|
dc.DateDebut as start_date,
|
||||||
|
dc.DateFin as end_date,
|
||||||
|
CONCAT(u.Prenom, ' ', u.Nom) as employee_name,
|
||||||
|
tc.Nom as type,
|
||||||
|
tc.CouleurHex as color
|
||||||
|
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 = 'Validée'
|
||||||
|
AND dc.DateFin >= CURDATE() - INTERVAL 30 DAY
|
||||||
|
ORDER BY dc.DateDebut ASC
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmtLeaves = $conn->prepare($queryLeaves);
|
||||||
|
$stmtLeaves->bind_param("i", $serviceId);
|
||||||
|
$stmtLeaves->execute();
|
||||||
|
$resultLeaves = $stmtLeaves->get_result();
|
||||||
|
|
||||||
|
$leaves = [];
|
||||||
|
while ($row = $resultLeaves->fetch_assoc()) {
|
||||||
|
$leaves[] = [
|
||||||
|
'start_date' => $row['start_date'],
|
||||||
|
'end_date' => $row['end_date'],
|
||||||
|
'employee_name' => $row['employee_name'],
|
||||||
|
'type' => $row['type'],
|
||||||
|
'color' => $row['color'] ?? '#3B82F6'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("getTeamLeaves - Congés trouvés: " . count($leaves));
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
"success" => true,
|
||||||
|
"message" => "Congés de l'équipe récupérés avec succès",
|
||||||
|
"leaves" => $leaves,
|
||||||
|
"service_id" => $serviceId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stmtLeaves->close();
|
||||||
|
} else {
|
||||||
|
error_log("getTeamLeaves - Utilisateur non trouvé: $userId");
|
||||||
|
echo json_encode([
|
||||||
|
"success" => false,
|
||||||
|
"message" => "Utilisateur non trouvé"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtUser->close();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Erreur getTeamLeaves: " . $e->getMessage());
|
||||||
|
echo json_encode([
|
||||||
|
"success" => false,
|
||||||
|
"message" => "Erreur lors de la récupération des congés: " . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
?>
|
||||||
116
project/public/getTeamMembers.php
Normal file
116
project/public/getTeamMembers.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
// Récupération des membres de l'équipe 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 getTeamMembers: " . $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("getTeamMembers - Manager ID: $managerId");
|
||||||
|
|
||||||
|
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("getTeamMembers - Service ID du manager: $serviceId");
|
||||||
|
|
||||||
|
// Récupérer tous les membres du même service (sauf le manager lui-même)
|
||||||
|
$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,
|
||||||
|
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
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmtTeam = $conn->prepare($queryTeam);
|
||||||
|
$stmtTeam->bind_param("ii", $serviceId, $managerId);
|
||||||
|
$stmtTeam->execute();
|
||||||
|
$resultTeam = $stmtTeam->get_result();
|
||||||
|
|
||||||
|
$teamMembers = [];
|
||||||
|
while ($row = $resultTeam->fetch_assoc()) {
|
||||||
|
$teamMembers[] = [
|
||||||
|
'id' => (int)$row['id'],
|
||||||
|
'nom' => $row['nom'],
|
||||||
|
'prenom' => $row['prenom'],
|
||||||
|
'email' => $row['email'],
|
||||||
|
'role' => $row['role'],
|
||||||
|
'date_embauche' => $row['date_embauche'],
|
||||||
|
'service_name' => $row['service_name']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("getTeamMembers - Membres trouvés: " . count($teamMembers));
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
"success" => true,
|
||||||
|
"message" => "Équipe récupérée avec succès",
|
||||||
|
"team_members" => $teamMembers,
|
||||||
|
"service_id" => $serviceId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stmtTeam->close();
|
||||||
|
} else {
|
||||||
|
error_log("getTeamMembers - Manager non trouvé: $managerId");
|
||||||
|
echo json_encode([
|
||||||
|
"success" => false,
|
||||||
|
"message" => "Manager non trouvé"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtManager->close();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Erreur getTeamMembers: " . $e->getMessage());
|
||||||
|
echo json_encode([
|
||||||
|
"success" => false,
|
||||||
|
"message" => "Erreur lors de la récupération de l'équipe: " . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
?>
|
||||||
74
project/public/login.php
Normal file
74
project/public/login.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
header("Access-Control-Allow-Methods: POST, 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
|
$host = "192.168.0.4";
|
||||||
|
$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) {
|
||||||
|
// En cas d'erreur de connexion, renvoie un JSON d'échec
|
||||||
|
die(json_encode(["success" => false, "message" => "Erreur de connexion à la base de données : " . $conn->connect_error]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère les données JSON envoyées via la requête POST
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$email = $data['email'] ?? '';
|
||||||
|
$mot_de_passe = $data['mot_de_passe'] ?? '';
|
||||||
|
|
||||||
|
|
||||||
|
$query = "SELECT ID, Prenom, Nom, Email, Role FROM Users WHERE Email = ? AND MDP = ?";
|
||||||
|
$stmt = $conn->prepare($query);
|
||||||
|
|
||||||
|
// Vérifie si la préparation de la requête a réussi
|
||||||
|
if ($stmt === false) {
|
||||||
|
die(json_encode(["success" => false, "message" => "Erreur de préparation de la requête : " . $conn->error]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lie les paramètres (ss = string, string pour email et mot_de_passe)
|
||||||
|
$stmt->bind_param("ss", $email, $mot_de_passe);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
// Récupère le résultat de la requête
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
// Vérifie si un utilisateur correspondant a été trouvé
|
||||||
|
if ($result->num_rows === 1) {
|
||||||
|
// Récupère la ligne de l'utilisateur sous forme de tableau associatif
|
||||||
|
$user = $result->fetch_assoc();
|
||||||
|
|
||||||
|
// Renvoie une réponse JSON de succès avec les données de l'utilisateur
|
||||||
|
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']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Renvoie une réponse JSON d'échec si les identifiants sont incorrects
|
||||||
|
echo json_encode(["success" => false, "message" => "Identifiants incorrects."]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ferme la connexion à la base de données
|
||||||
|
$stmt->close();
|
||||||
|
$conn->close();
|
||||||
|
?>
|
||||||
116
project/public/manualResetCounters.php
Normal file
116
project/public/manualResetCounters.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
// Script manuel pour réinitialiser les compteurs
|
||||||
|
// Accès direct via navigateur pour les administrateurs
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Réinitialisation des Compteurs</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
|
||||||
|
.container { background: #f5f5f5; padding: 30px; border-radius: 10px; }
|
||||||
|
.warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||||
|
.success { background: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||||
|
.error { background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||||
|
button { background: #007bff; color: white; padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; }
|
||||||
|
button:hover { background: #0056b3; }
|
||||||
|
.danger { background: #dc3545; }
|
||||||
|
.danger:hover { background: #c82333; }
|
||||||
|
pre { background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔄 Réinitialisation des Compteurs de Congés</h1>
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
<h3>⚠️ ATTENTION</h3>
|
||||||
|
<p>Cette opération va réinitialiser TOUS les compteurs de congés selon les règles suivantes :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Congés Payés :</strong> 25 jours (exercice du 01/06 au 31/05)</li>
|
||||||
|
<li><strong>RTT :</strong> 10 jours pour 2025 (exercice du 01/01 au 31/12)</li>
|
||||||
|
<li><strong>Congés Maladie :</strong> 0 jours (remise à zéro)</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Cette action est irréversible !</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_reset'])) {
|
||||||
|
// Appel du script de réinitialisation
|
||||||
|
$resetUrl = 'http://localhost/project/public/resetLeaveCounters.php';
|
||||||
|
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => 'Content-Type: application/json',
|
||||||
|
'content' => json_encode(['manual_reset' => true])
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = file_get_contents($resetUrl, false, $context);
|
||||||
|
$data = json_decode($result, true);
|
||||||
|
|
||||||
|
if ($data && $data['success']) {
|
||||||
|
echo '<div class="success">';
|
||||||
|
echo '<h3>✅ Réinitialisation réussie !</h3>';
|
||||||
|
echo '<p>Employés mis à jour : ' . $data['details']['employees_updated'] . '</p>';
|
||||||
|
echo '<p>Exercice CP : ' . $data['details']['leave_year'] . '</p>';
|
||||||
|
echo '<p>Année RTT : ' . $data['details']['rtt_year'] . '</p>';
|
||||||
|
echo '<p>Date de réinitialisation : ' . $data['details']['reset_date'] . '</p>';
|
||||||
|
|
||||||
|
if (!empty($data['log'])) {
|
||||||
|
echo '<details><summary>Voir le détail</summary><pre>';
|
||||||
|
foreach ($data['log'] as $logLine) {
|
||||||
|
echo htmlspecialchars($logLine) . "\n";
|
||||||
|
}
|
||||||
|
echo '</pre></details>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
} else {
|
||||||
|
echo '<div class="error">';
|
||||||
|
echo '<h3>❌ Erreur lors de la réinitialisation</h3>';
|
||||||
|
echo '<p>' . ($data['message'] ?? 'Erreur inconnue') . '</p>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<form method="POST" onsubmit="return confirm('Êtes-vous sûr de vouloir réinitialiser TOUS les compteurs ? Cette action est irréversible.');">
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="confirm_reset" value="1" required>
|
||||||
|
Je confirme vouloir réinitialiser tous les compteurs de congés
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<button type="submit" class="danger">🔄 RÉINITIALISER LES COMPTEURS</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr style="margin: 40px 0;">
|
||||||
|
|
||||||
|
<h3>📋 Informations sur les exercices</h3>
|
||||||
|
<?php
|
||||||
|
$currentDate = new DateTime();
|
||||||
|
$currentYear = (int)$currentDate->format('Y');
|
||||||
|
$currentMonth = (int)$currentDate->format('m');
|
||||||
|
|
||||||
|
// Calcul exercice CP
|
||||||
|
$leaveYear = ($currentMonth < 6) ? $currentYear - 1 : $currentYear;
|
||||||
|
$leaveYearEnd = $leaveYear + 1;
|
||||||
|
|
||||||
|
echo "<p><strong>Exercice Congés Payés actuel :</strong> du 01/06/$leaveYear au 31/05/$leaveYearEnd</p>";
|
||||||
|
echo "<p><strong>Exercice RTT actuel :</strong> du 01/01/$currentYear au 31/12/$currentYear</p>";
|
||||||
|
echo "<p><strong>Date actuelle :</strong> " . $currentDate->format('d/m/Y H:i:s') . "</p>";
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h3>🔗 Actions rapides</h3>
|
||||||
|
<p>
|
||||||
|
<a href="getLeaveCounters.php?user_id=1" target="_blank">
|
||||||
|
<button type="button">Voir les compteurs (User ID 1)</button>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
228
project/public/resetLeaveCounters.php
Normal file
228
project/public/resetLeaveCounters.php
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<?php
|
||||||
|
// Script de réinitialisation des compteurs de congés
|
||||||
|
// À exécuter manuellement ou via cron job
|
||||||
|
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
header("Access-Control-Allow-Methods: POST, OPTIONS");
|
||||||
|
header("Access-Control-Allow-Headers: Content-Type");
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
|
// Gère la requête OPTIONS (pré-vol CORS)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
||||||
|
http_response_code(200);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log des erreurs pour debug
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
$host = "192.168.0.4";
|
||||||
|
$dbname = "DemandeConge";
|
||||||
|
$username = "wpuser";
|
||||||
|
$password = "-2b/)ru5/Bi8P[7_";
|
||||||
|
|
||||||
|
// Connexion à la base de données
|
||||||
|
$conn = new mysqli($host, $username, $password, $dbname);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
error_log("Erreur connexion DB reset: " . $conn->connect_error);
|
||||||
|
echo json_encode([
|
||||||
|
"success" => false,
|
||||||
|
"message" => "Erreur de connexion à la base de données : " . $conn->connect_error
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log de debug
|
||||||
|
error_log("Reset counters - Début du script");
|
||||||
|
|
||||||
|
// Fonction pour déterminer l'exercice des congés payés (01/06 au 31/05)
|
||||||
|
function getLeaveYear($date = null) {
|
||||||
|
if ($date === null) {
|
||||||
|
$date = new DateTime();
|
||||||
|
} else {
|
||||||
|
$date = new DateTime($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentYear = (int)$date->format('Y');
|
||||||
|
$currentMonth = (int)$date->format('m');
|
||||||
|
|
||||||
|
// Si on est avant le 1er juin, l'exercice a commencé l'année précédente
|
||||||
|
if ($currentMonth < 6) {
|
||||||
|
return $currentYear - 1;
|
||||||
|
}
|
||||||
|
// Si on est le 1er juin ou après, l'exercice a commencé cette année
|
||||||
|
return $currentYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour déterminer l'année RTT (01/01 au 31/12)
|
||||||
|
function getRTTYear($date = null) {
|
||||||
|
if ($date === null) {
|
||||||
|
$date = new DateTime();
|
||||||
|
} else {
|
||||||
|
$date = new DateTime($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)$date->format('Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn->begin_transaction();
|
||||||
|
|
||||||
|
$currentDate = new DateTime();
|
||||||
|
$leaveYear = getLeaveYear();
|
||||||
|
$rttYear = getRTTYear();
|
||||||
|
|
||||||
|
error_log("Reset counters - Exercice CP: $leaveYear, RTT: $rttYear");
|
||||||
|
|
||||||
|
$resetLog = [];
|
||||||
|
|
||||||
|
// 1. Récupérer tous les employés depuis la table Users
|
||||||
|
$queryEmployees = "SELECT ID FROM Users";
|
||||||
|
$resultEmployees = $conn->query($queryEmployees);
|
||||||
|
|
||||||
|
if (!$resultEmployees) {
|
||||||
|
throw new Exception("Erreur lors de la récupération des employés : " . $conn->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("Reset counters - Nombre d'employés trouvés: " . $resultEmployees->num_rows);
|
||||||
|
|
||||||
|
// 2. Récupérer les IDs des types de congés
|
||||||
|
$queryTypes = "SELECT Id, Nom FROM TypeConge WHERE Nom IN ('Congé payé', 'RTT', 'Congé maladie')";
|
||||||
|
$resultTypes = $conn->query($queryTypes);
|
||||||
|
|
||||||
|
$typeIds = [];
|
||||||
|
while ($row = $resultTypes->fetch_assoc()) {
|
||||||
|
$typeIds[$row['Nom']] = $row['Id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("Reset counters - Types trouvés: " . print_r($typeIds, true));
|
||||||
|
|
||||||
|
if (count($typeIds) < 3) {
|
||||||
|
throw new Exception("Types de congés manquants dans la base de données");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Pour chaque employé, réinitialiser les compteurs
|
||||||
|
$employeesUpdated = 0;
|
||||||
|
while ($employee = $resultEmployees->fetch_assoc()) {
|
||||||
|
$employeeId = $employee['ID'];
|
||||||
|
|
||||||
|
error_log("Reset counters - Traitement employé: $employeeId");
|
||||||
|
|
||||||
|
// CONGÉS PAYÉS - Exercice du 01/06 au 31/05 (25 jours)
|
||||||
|
$queryUpdateCP = "
|
||||||
|
INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total)
|
||||||
|
VALUES (?, ?, ?, 25, 25)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
Solde = 25,
|
||||||
|
Total = 25
|
||||||
|
";
|
||||||
|
$stmtCP = $conn->prepare($queryUpdateCP);
|
||||||
|
if (!$stmtCP) {
|
||||||
|
throw new Exception("Erreur préparation CP: " . $conn->error);
|
||||||
|
}
|
||||||
|
$stmtCP->bind_param("iii", $employeeId, $typeIds['Congé payé'], $leaveYear);
|
||||||
|
|
||||||
|
if (!$stmtCP->execute()) {
|
||||||
|
throw new Exception("Erreur lors de la mise à jour des CP pour l'employé $employeeId : " . $stmtCP->error);
|
||||||
|
}
|
||||||
|
$stmtCP->close();
|
||||||
|
|
||||||
|
// RTT - Année civile du 01/01 au 31/12
|
||||||
|
// Calcul du nombre de RTT selon l'année
|
||||||
|
$rttCount = 10; // Par défaut 10 pour 2025
|
||||||
|
if ($rttYear == 2024) {
|
||||||
|
$rttCount = 8; // Exemple pour 2024
|
||||||
|
} elseif ($rttYear >= 2025) {
|
||||||
|
$rttCount = 10; // 10 pour 2025 et après
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryUpdateRTT = "
|
||||||
|
INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
Solde = ?,
|
||||||
|
Total = ?
|
||||||
|
";
|
||||||
|
$stmtRTT = $conn->prepare($queryUpdateRTT);
|
||||||
|
if (!$stmtRTT) {
|
||||||
|
throw new Exception("Erreur préparation RTT: " . $conn->error);
|
||||||
|
}
|
||||||
|
$stmtRTT->bind_param("iiiiiii", $employeeId, $typeIds['RTT'], $rttYear, $rttCount, $rttCount, $rttCount, $rttCount);
|
||||||
|
|
||||||
|
if (!$stmtRTT->execute()) {
|
||||||
|
throw new Exception("Erreur lors de la mise à jour des RTT pour l'employé $employeeId : " . $stmtRTT->error);
|
||||||
|
}
|
||||||
|
$stmtRTT->close();
|
||||||
|
|
||||||
|
// CONGÉ MALADIE - Réinitialiser à 0 (pas de limite)
|
||||||
|
$queryUpdateABS = "
|
||||||
|
INSERT INTO CompteurConges (EmployeeId, TypeCongeId, Annee, Solde, Total)
|
||||||
|
VALUES (?, ?, ?, 0, 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
Solde = 0,
|
||||||
|
Total = 0
|
||||||
|
";
|
||||||
|
$stmtABS = $conn->prepare($queryUpdateABS);
|
||||||
|
if (!$stmtABS) {
|
||||||
|
throw new Exception("Erreur préparation ABS: " . $conn->error);
|
||||||
|
}
|
||||||
|
$stmtABS->bind_param("iii", $employeeId, $typeIds['Congé maladie'], $rttYear);
|
||||||
|
|
||||||
|
if (!$stmtABS->execute()) {
|
||||||
|
throw new Exception("Erreur lors de la mise à jour des ABS pour l'employé $employeeId : " . $stmtABS->error);
|
||||||
|
}
|
||||||
|
$stmtABS->close();
|
||||||
|
|
||||||
|
$resetLog[] = "Employé $employeeId : CP=$leaveYear (25j), RTT=$rttYear ({$rttCount}j), ABS=$rttYear (0j)";
|
||||||
|
$employeesUpdated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("Reset counters - Employés mis à jour: $employeesUpdated");
|
||||||
|
|
||||||
|
// 4. Log de la réinitialisation
|
||||||
|
$logEntry = "
|
||||||
|
=== RÉINITIALISATION DES COMPTEURS ===
|
||||||
|
Date: " . $currentDate->format('Y-m-d H:i:s') . "
|
||||||
|
Exercice CP: $leaveYear (01/06/$leaveYear au 31/05/" . ($leaveYear + 1) . ")
|
||||||
|
Année RTT: $rttYear (01/01/$rttYear au 31/12/$rttYear)
|
||||||
|
Employés traités: $employeesUpdated
|
||||||
|
|
||||||
|
Détails:
|
||||||
|
" . implode("\n ", $resetLog) . "
|
||||||
|
";
|
||||||
|
|
||||||
|
// Sauvegarder le log (optionnel - créer une table de logs si nécessaire)
|
||||||
|
error_log($logEntry, 3, "reset_counters.log");
|
||||||
|
|
||||||
|
$conn->commit();
|
||||||
|
error_log("Reset counters - Transaction commitée avec succès");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
"success" => true,
|
||||||
|
"message" => "Compteurs réinitialisés avec succès",
|
||||||
|
"details" => [
|
||||||
|
"employees_updated" => $employeesUpdated,
|
||||||
|
"leave_year" => $leaveYear,
|
||||||
|
"rtt_year" => $rttYear,
|
||||||
|
"cp_days" => 25,
|
||||||
|
"rtt_days" => $rttCount,
|
||||||
|
"reset_date" => $currentDate->format('Y-m-d H:i:s')
|
||||||
|
],
|
||||||
|
"log" => $resetLog
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$conn->rollback();
|
||||||
|
error_log("Erreur réinitialisation compteurs : " . $e->getMessage());
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
"success" => false,
|
||||||
|
"message" => "Erreur lors de la réinitialisation : " . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
?>
|
||||||
181
project/public/submitLeaveRequest.php
Normal file
181
project/public/submitLeaveRequest.php
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<?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é");
|
||||||
|
|
||||||
|
?>
|
||||||
14
project/public/test_db.php
Normal file
14
project/public/test_db.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
$host = "192.168.0.4";
|
||||||
|
$dbname = "DemandeConge";
|
||||||
|
$username = "wpuser";
|
||||||
|
$password = "-2b/)ru5/Bi8P[7_";
|
||||||
|
|
||||||
|
$conn = new mysqli($host, $username, $password, $dbname);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
die("❌ Connexion échouée : " . $conn->connect_error);
|
||||||
|
}
|
||||||
|
echo "✅ Connexion réussie à la base de données !";
|
||||||
|
|
||||||
|
?>
|
||||||
197
project/public/validateRequest.php
Normal file
197
project/public/validateRequest.php
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<?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();
|
||||||
|
?>
|
||||||
44
project/src/App.jsx
Normal file
44
project/src/App.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import { AuthProvider } from './context/AuthContext';
|
||||||
|
import Dashboard from './pages/Dashboard';
|
||||||
|
import Login from './pages/Login';
|
||||||
|
import Requests from './pages/Requests';
|
||||||
|
import Calendar from './pages/Calendar';
|
||||||
|
import Manager from './pages/Manager';
|
||||||
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/dashboard" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Dashboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/demandes" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Requests />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/calendrier" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Calendar />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/manager" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Manager />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
579
project/src/components/NewLeaveRequestModal.jsx
Normal file
579
project/src/components/NewLeaveRequestModal.jsx
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { X, Calendar, Clock, AlertCircle, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
|
const NewLeaveRequestModal = ({
|
||||||
|
onClose,
|
||||||
|
availableLeaveCounters,
|
||||||
|
userId,
|
||||||
|
onRequestSubmitted,
|
||||||
|
preselectedStartDate = null,
|
||||||
|
preselectedEndDate = null,
|
||||||
|
preselectedType = null
|
||||||
|
}) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
types: preselectedType ? [preselectedType] : [],
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
reason: '',
|
||||||
|
medicalDocuments: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const [typeDistribution, setTypeDistribution] = useState({});
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [calculatedDays, setCalculatedDays] = useState(0);
|
||||||
|
const [isPreselected, setIsPreselected] = useState(false);
|
||||||
|
|
||||||
|
// Vérifier si des valeurs sont pré-sélectionnées
|
||||||
|
useEffect(() => {
|
||||||
|
if (preselectedStartDate || preselectedEndDate || preselectedType) {
|
||||||
|
setIsPreselected(true);
|
||||||
|
|
||||||
|
// Pré-remplir automatiquement les dates
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
types: preselectedType ? [preselectedType] : prev.types,
|
||||||
|
startDate: preselectedStartDate ? preselectedStartDate.toISOString().split('T')[0] : prev.startDate,
|
||||||
|
endDate: preselectedEndDate ? preselectedEndDate.toISOString().split('T')[0] :
|
||||||
|
preselectedStartDate ? preselectedStartDate.toISOString().split('T')[0] : prev.endDate
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [preselectedStartDate, preselectedEndDate, preselectedType]);
|
||||||
|
|
||||||
|
// Calculer le nombre de jours ouvrés
|
||||||
|
const calculateWorkingDays = (start, end) => {
|
||||||
|
if (!start || !end) return 0;
|
||||||
|
|
||||||
|
const startDate = new Date(start);
|
||||||
|
const endDate = new Date(end);
|
||||||
|
let workingDays = 0;
|
||||||
|
|
||||||
|
const current = new Date(startDate);
|
||||||
|
while (current <= endDate) {
|
||||||
|
const dayOfWeek = current.getDay();
|
||||||
|
if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Pas dimanche (0) ni samedi (6)
|
||||||
|
workingDays++;
|
||||||
|
}
|
||||||
|
current.setDate(current.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return workingDays;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recalculer les jours quand les dates changent
|
||||||
|
useEffect(() => {
|
||||||
|
const days = calculateWorkingDays(formData.startDate, formData.endDate);
|
||||||
|
setCalculatedDays(days);
|
||||||
|
}, [formData.startDate, formData.endDate]);
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = (e) => {
|
||||||
|
const files = Array.from(e.target.files);
|
||||||
|
const validFiles = [];
|
||||||
|
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Vérifier le type de fichier
|
||||||
|
const validTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
|
||||||
|
if (!validTypes.includes(file.type)) {
|
||||||
|
setError(`Le fichier "${file.name}" n'est pas un format valide. Formats acceptés : PDF, JPG, PNG`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier la taille
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
setError(`Le fichier "${file.name}" est trop volumineux. Taille maximum : 5MB`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validFiles.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
medicalDocuments: [...prev.medicalDocuments, ...validFiles]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Reset input pour permettre de re-sélectionner le même fichier
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDocument = (index) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
medicalDocuments: prev.medicalDocuments.filter((_, i) => i !== index)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes) => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeToggle = (type) => {
|
||||||
|
setFormData(prev => {
|
||||||
|
const newTypes = prev.types.includes(type)
|
||||||
|
? prev.types.filter(t => t !== type)
|
||||||
|
: [...prev.types, type];
|
||||||
|
return { ...prev, types: newTypes };
|
||||||
|
});
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDistributionChange = (type, days) => {
|
||||||
|
setTypeDistribution(prev => ({
|
||||||
|
...prev,
|
||||||
|
[type]: Math.max(0, Math.min(days, calculatedDays))
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalDistributedDays = () => {
|
||||||
|
return Object.values(typeDistribution).reduce((sum, days) => sum + (days || 0), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
types: [],
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
reason: '',
|
||||||
|
medicalDocuments: []
|
||||||
|
});
|
||||||
|
setTypeDistribution({});
|
||||||
|
setIsPreselected(false);
|
||||||
|
setError('');
|
||||||
|
setCalculatedDays(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
if (formData.types.length === 0) {
|
||||||
|
setError('Veuillez sélectionner au moins un type de congé');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification des documents pour congé maladie
|
||||||
|
if (formData.types.includes('ABS') && formData.medicalDocuments.length === 0) {
|
||||||
|
setError('Un justificatif médical est obligatoire pour les arrêts maladie');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.startDate || !formData.endDate) {
|
||||||
|
setError('Veuillez sélectionner les dates de début et de fin');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date(formData.startDate) > new Date(formData.endDate)) {
|
||||||
|
setError('La date de fin doit être postérieure à la date de début');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date(formData.startDate) < new Date()) {
|
||||||
|
setError('Impossible de faire une demande pour une date passée');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification de la distribution des jours
|
||||||
|
if (formData.types.length > 1) {
|
||||||
|
const totalDistributed = getTotalDistributedDays();
|
||||||
|
if (totalDistributed !== calculatedDays) {
|
||||||
|
setError(`Vous devez distribuer exactement ${calculatedDays} jours entre les types sélectionnés`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification des soldes pour chaque type
|
||||||
|
for (const type of formData.types) {
|
||||||
|
const requiredDays = formData.types.length > 1 ? (typeDistribution[type] || 0) : calculatedDays;
|
||||||
|
|
||||||
|
if (type === 'CP' && requiredDays > availableLeaveCounters.availableCP) {
|
||||||
|
setError(`Solde CP insuffisant. Vous avez ${availableLeaveCounters.availableCP} jours disponibles`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'RTT' && requiredDays > availableLeaveCounters.availableRTT) {
|
||||||
|
setError(`Solde RTT insuffisant. Vous avez ${availableLeaveCounters.availableRTT} jours disponibles`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 results = await Promise.all(responses.map(r => r.json()));
|
||||||
|
|
||||||
|
const allSuccessful = results.every(result => result.success);
|
||||||
|
|
||||||
|
if (allSuccessful) {
|
||||||
|
onRequestSubmitted?.();
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
const failedResults = results.filter(r => !r.success);
|
||||||
|
setError(`Erreur lors de la soumission : ${failedResults.map(r => r.message).join(', ')}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur:', error);
|
||||||
|
setError('Erreur de connexion au serveur');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeLabel = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'CP': return 'Congés payés';
|
||||||
|
case 'RTT': return 'RTT';
|
||||||
|
case 'ABS': return 'Arrêt maladie';
|
||||||
|
default: return type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvailableDays = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'CP': return availableLeaveCounters.availableCP;
|
||||||
|
case 'RTT': return availableLeaveCounters.availableRTT;
|
||||||
|
case 'ABS': return '∞';
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'CP': return 'bg-blue-100 text-blue-800 border-blue-200';
|
||||||
|
case 'RTT': return 'bg-green-100 text-green-800 border-green-200';
|
||||||
|
case 'ABS': return 'bg-red-100 text-red-800 border-red-200';
|
||||||
|
default: return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-2 lg:p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md max-h-[95vh] lg:max-h-[90vh] overflow-y-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 lg:p-6 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg lg:text-xl font-semibold text-gray-900">
|
||||||
|
{isPreselected ? 'Confirmer la demande' : 'Nouvelle demande de congé'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="p-4 lg:p-6 space-y-4 lg:space-y-6">
|
||||||
|
{/* Pré-sélection info */}
|
||||||
|
{isPreselected && (
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Calendar className="w-4 h-4 text-blue-600" />
|
||||||
|
<span className="text-sm font-medium text-blue-800">Sélection depuis le calendrier</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-blue-700">
|
||||||
|
Les champs ont été pré-remplis selon votre sélection.
|
||||||
|
Vous pouvez les modifier ou utiliser le bouton "Réinitialiser".
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Type de congé */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm lg:text-base font-medium text-gray-700 mb-2">
|
||||||
|
Types de congé * {formData.types.length > 0 && `(${formData.types.length} sélectionné${formData.types.length > 1 ? 's' : ''})`}
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[
|
||||||
|
{ key: 'CP', label: 'Congés payés', available: availableLeaveCounters.availableCP },
|
||||||
|
{ key: 'RTT', label: 'RTT', available: availableLeaveCounters.availableRTT },
|
||||||
|
{ key: 'ABS', label: 'Arrêt maladie', available: '∞' }
|
||||||
|
].map(type => (
|
||||||
|
<div key={type.key} className="flex items-center gap-3">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer flex-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.types.includes(type.key)}
|
||||||
|
onChange={() => handleTypeToggle(type.key)}
|
||||||
|
disabled={isPreselected && preselectedType && preselectedType !== type.key}
|
||||||
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className={`px-2 lg:px-3 py-1 rounded-full text-xs lg:text-sm font-medium border ${getTypeColor(type.key)}`}>
|
||||||
|
{type.key === 'ABS' ? type.label : `${type.label} (${type.available} disponible${type.available !== 1 ? 's' : ''})`}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Distribution des jours si plusieurs types sélectionnés */}
|
||||||
|
{formData.types.length > 1 && calculatedDays > 0 && (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||||
|
<h4 className="text-sm font-medium text-yellow-800 mb-3">
|
||||||
|
Répartition des {calculatedDays} jours entre les types :
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{formData.types.map(type => (
|
||||||
|
<div key={type} className="flex items-center gap-3">
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${getTypeColor(type)} flex-shrink-0`}>
|
||||||
|
{getTypeLabel(type)}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max={calculatedDays}
|
||||||
|
value={typeDistribution[type] || 0}
|
||||||
|
onChange={(e) => handleDistributionChange(type, parseInt(e.target.value) || 0)}
|
||||||
|
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-600">jours</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="pt-2 border-t border-yellow-300">
|
||||||
|
<p className="text-xs text-yellow-700">
|
||||||
|
Total distribué : {getTotalDistributedDays()} / {calculatedDays} jours
|
||||||
|
{getTotalDistributedDays() !== calculatedDays && (
|
||||||
|
<span className="text-red-600 ml-2">
|
||||||
|
⚠️ Répartition incomplète
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dates */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Date de début *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="startDate"
|
||||||
|
value={formData.startDate}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Date de fin *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="endDate"
|
||||||
|
value={formData.endDate}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Durée calculée */}
|
||||||
|
{calculatedDays > 0 && (
|
||||||
|
<div className="bg-gray-50 rounded-lg p-3 lg:p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-sm font-medium text-gray-700">
|
||||||
|
Durée : {calculatedDays} jour{calculatedDays > 1 ? 's' : ''} ouvré{calculatedDays > 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{formData.types.length === 1 && formData.types[0] !== 'ABS' && (
|
||||||
|
<p className="text-xs text-gray-600 mt-1">
|
||||||
|
Solde {getTypeLabel(formData.types[0])} disponible : {getAvailableDays(formData.types[0])} jour{getAvailableDays(formData.types[0]) !== 1 && getAvailableDays(formData.types[0]) !== '∞' ? 's' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{formData.types.length > 1 && (
|
||||||
|
<p className="text-xs text-gray-600 mt-1">
|
||||||
|
Types sélectionnés : {formData.types.map(type => getTypeLabel(type)).join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Motif */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm lg:text-base font-medium text-gray-700 mb-2">
|
||||||
|
Motif (optionnel)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="reason"
|
||||||
|
value={formData.reason}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm"
|
||||||
|
placeholder="Précisez le motif de votre demande..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Documents médicaux pour congé maladie */}
|
||||||
|
{formData.types.includes('ABS') && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm lg:text-base font-medium text-gray-700 mb-2">
|
||||||
|
Justificatif médical * <span className="text-red-500">(obligatoire pour arrêt maladie)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Zone de téléchargement */}
|
||||||
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 hover:border-gray-400 transition-colors">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 mx-auto mb-3 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||||
|
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
|
Glissez vos documents ici ou cliquez pour sélectionner
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mb-3">
|
||||||
|
Formats acceptés : PDF, JPG, PNG (max 5MB par fichier)
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
className="hidden"
|
||||||
|
id="medical-documents"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="medical-documents"
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
Sélectionner des fichiers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste des fichiers sélectionnés */}
|
||||||
|
{formData.medicalDocuments.length > 0 && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<p className="text-sm font-medium text-gray-700">
|
||||||
|
Fichiers sélectionnés ({formData.medicalDocuments.length}) :
|
||||||
|
</p>
|
||||||
|
{formData.medicalDocuments.map((file, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 bg-blue-100 rounded flex items-center justify-center flex-shrink-0">
|
||||||
|
{file.type === 'application/pdf' ? (
|
||||||
|
<svg className="w-4 h-4 text-red-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900 truncate">{file.name}</p>
|
||||||
|
<p className="text-xs text-gray-500">{formatFileSize(file.size)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeDocument(index)}
|
||||||
|
className="text-red-600 hover:text-red-800 p-1 flex-shrink-0"
|
||||||
|
title="Supprimer ce fichier"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Erreur */}
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0" />
|
||||||
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex flex-col lg:flex-row gap-3 pt-4">
|
||||||
|
{isPreselected && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={resetForm}
|
||||||
|
className="flex items-center justify-center gap-2 px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Envoi...' : 'Soumettre'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewLeaveRequestModal;
|
||||||
22
project/src/components/ProtectedRoute.jsx
Normal file
22
project/src/components/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
|
const ProtectedRoute = ({ children }) => {
|
||||||
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">Chargement...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user ? children : <Navigate to="/login" replace />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
||||||
140
project/src/components/Sidebar.jsx
Normal file
140
project/src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { LogOut, Calendar, Home, FileText, Building2, Menu, X, Users } from 'lucide-react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
|
const Sidebar = ({ isOpen, onToggle }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
|
const isActive = (path) => location.pathname === path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile overlay */}
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
||||||
|
onClick={onToggle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className={`
|
||||||
|
fixed inset-y-0 left-0 z-50 w-60 bg-white border-r border-gray-200 min-h-screen flex flex-col transform transition-transform duration-300 ease-in-out
|
||||||
|
${isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||||
|
`}>
|
||||||
|
{/* Mobile close button */}
|
||||||
|
<div className="lg:hidden flex justify-end p-4">
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className="p-2 rounded-lg hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo Section */}
|
||||||
|
<div className="p-6 border-b border-gray-100">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||||
|
<Building2 className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">GTA</h2>
|
||||||
|
<p className="text-sm text-gray-500">Gestion de congés</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User Info */}
|
||||||
|
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<img
|
||||||
|
src={user?.avatar || "/assets/utilisateur.png"}
|
||||||
|
alt="User"
|
||||||
|
className="w-12 h-12 lg:w-16 lg:h-16 rounded-full object-cover mb-3"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-900 text-sm lg:text-base">{user?.name || "Utilisateur"}</p>
|
||||||
|
<p className="text-xs lg:text-sm text-gray-500">{user?.department || "Service"}</p>
|
||||||
|
<span className="inline-block mt-2 px-3 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
|
||||||
|
Employé
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 p-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Link
|
||||||
|
to="/dashboard"
|
||||||
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/dashboard")
|
||||||
|
? "bg-blue-50 text-blue-700 border-r-2 border-blue-700"
|
||||||
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Home className="w-5 h-5" />
|
||||||
|
<span className="font-medium">Dashboard</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/demandes"
|
||||||
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/demandes")
|
||||||
|
? "bg-blue-50 text-blue-700 border-r-2 border-blue-700"
|
||||||
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
<span className="font-medium">Demandes</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/calendrier"
|
||||||
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/calendrier")
|
||||||
|
? "bg-blue-50 text-blue-700 border-r-2 border-blue-700"
|
||||||
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Calendar className="w-5 h-5" />
|
||||||
|
<span className="font-medium">Calendrier</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{(user?.role === 'Manager' || user?.role === 'Admin' || user?.role === 'Employe') && (
|
||||||
|
<Link
|
||||||
|
to="/manager"
|
||||||
|
onClick={() => window.innerWidth < 1024 && onToggle()}
|
||||||
|
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/manager")
|
||||||
|
? "bg-blue-50 text-blue-700 border-r-2 border-blue-700"
|
||||||
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Users className="w-5 h-5" />
|
||||||
|
<span className="font-medium">
|
||||||
|
{user?.role === 'Employe' ? 'Mon équipe' : 'Équipe'}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Logout Button */}
|
||||||
|
<div className="p-4 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="flex items-center gap-3 px-4 py-3 w-full text-left text-gray-700 hover:bg-red-50 hover:text-red-600 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
<span className="font-medium">Déconnexion</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
126
project/src/context/AuthContext.jsx
Normal file
126
project/src/context/AuthContext.jsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const AuthContext = createContext();
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }) => {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Vérifier si l'utilisateur est déjà connecté
|
||||||
|
const savedUser = localStorage.getItem('user');
|
||||||
|
if (savedUser) {
|
||||||
|
try {
|
||||||
|
setUser(JSON.parse(savedUser));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du parsing de l\'utilisateur sauvegardé:', error);
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = async (email, password) => {
|
||||||
|
try {
|
||||||
|
// Tester plusieurs URLs possibles selon la configuration locale
|
||||||
|
const possibleUrls = [
|
||||||
|
'http://localhost/GTA/project/public/login.php',
|
||||||
|
'http://localhost:80/GTA/project/public/login.php',
|
||||||
|
'http://localhost/GTA/public/login.php',
|
||||||
|
'http://localhost/public/login.php'
|
||||||
|
];
|
||||||
|
|
||||||
|
let response = null;
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
for (const url of possibleUrls) {
|
||||||
|
try {
|
||||||
|
console.log(' Test URL:', url);
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
mot_de_passe: password
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log(' URL qui fonctionne:', url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
console.log(' URL échouée:', url, error.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
throw new Error('Aucune URL de connexion accessible');
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
console.log(' Réponse brute:', text);
|
||||||
|
|
||||||
|
// Vérifier si la réponse est du JSON valide
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(text);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error(' Réponse non-JSON:', text.substring(0, 200));
|
||||||
|
throw new Error('Le serveur PHP ne répond pas correctement. Vérifiez que PHP est démarré.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const userData = {
|
||||||
|
id: data.user.id,
|
||||||
|
name: data.user.prenom + ' ' + data.user.nom,
|
||||||
|
prenom: data.user.prenom,
|
||||||
|
nom: data.user.nom,
|
||||||
|
email: data.user.email,
|
||||||
|
role: data.user.role || 'Employe'
|
||||||
|
};
|
||||||
|
setUser(userData);
|
||||||
|
localStorage.setItem('user', JSON.stringify(userData));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error(' Échec connexion:', data.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur de connexion:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
setUser(null);
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
user,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthContext;
|
||||||
3
project/src/index.css
Normal file
3
project/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
10
project/src/main.jsx
Normal file
10
project/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App.jsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
544
project/src/pages/Calendar.jsx
Normal file
544
project/src/pages/Calendar.jsx
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Sidebar from '../components/Sidebar';
|
||||||
|
import { ChevronLeft, ChevronRight, Plus, X, Menu } from 'lucide-react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
||||||
|
|
||||||
|
const Calendar = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const [selectedDate, setSelectedDate] = useState(null);
|
||||||
|
const [selectedEndDate, setSelectedEndDate] = useState(null);
|
||||||
|
const [isSelectingRange, setIsSelectingRange] = useState(false);
|
||||||
|
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||||
|
const [contextMenu, setContextMenu] = useState({ show: false, x: 0, y: 0 });
|
||||||
|
const [preselectedType, setPreselectedType] = useState(null);
|
||||||
|
const [holidays, setHolidays] = useState([]);
|
||||||
|
const [isLoadingHolidays, setIsLoadingHolidays] = useState(true);
|
||||||
|
const [leaveCounters, setLeaveCounters] = useState({
|
||||||
|
availableCP: 25,
|
||||||
|
availableRTT: 8,
|
||||||
|
availableABS: 0
|
||||||
|
});
|
||||||
|
const [teamLeaves, setTeamLeaves] = useState([]);
|
||||||
|
|
||||||
|
const monthNames = [
|
||||||
|
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||||
|
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
|
||||||
|
];
|
||||||
|
|
||||||
|
const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven'];
|
||||||
|
|
||||||
|
// Récupération des jours fériés depuis l'API gouvernementale française
|
||||||
|
const fetchFrenchHolidays = async (year) => {
|
||||||
|
try {
|
||||||
|
setIsLoadingHolidays(true);
|
||||||
|
const response = await fetch(`https://calendrier.api.gouv.fr/jours-feries/metropole/${year}.json`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur API: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Convertir les dates de l'API en objets Date
|
||||||
|
const holidayDates = Object.keys(data).map(dateStr => {
|
||||||
|
const [year, month, day] = dateStr.split('-').map(Number);
|
||||||
|
return {
|
||||||
|
date: new Date(year, month - 1, day), // month - 1 car les mois JS commencent à 0
|
||||||
|
name: data[dateStr]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return holidayDates;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la récupération des jours fériés:', error);
|
||||||
|
// Fallback avec quelques jours fériés fixes si l'API échoue
|
||||||
|
return [
|
||||||
|
{ date: new Date(year, 0, 1), name: 'Jour de l\'An' },
|
||||||
|
{ date: new Date(year, 4, 1), name: 'Fête du Travail' },
|
||||||
|
{ date: new Date(year, 6, 14), name: 'Fête Nationale' },
|
||||||
|
{ date: new Date(year, 11, 25), name: 'Noël' }
|
||||||
|
];
|
||||||
|
} finally {
|
||||||
|
setIsLoadingHolidays(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Charger les jours fériés au montage du composant et lors du changement d'année
|
||||||
|
useEffect(() => {
|
||||||
|
const loadHolidays = async () => {
|
||||||
|
const currentYear = currentDate.getFullYear();
|
||||||
|
const nextYear = currentYear + 1;
|
||||||
|
const prevYear = currentYear - 1;
|
||||||
|
|
||||||
|
// Charger les jours fériés pour l'année précédente, actuelle et suivante
|
||||||
|
const [prevYearHolidays, currentYearHolidays, nextYearHolidays] = await Promise.all([
|
||||||
|
fetchFrenchHolidays(prevYear),
|
||||||
|
fetchFrenchHolidays(currentYear),
|
||||||
|
fetchFrenchHolidays(nextYear)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Combiner tous les jours fériés
|
||||||
|
const allHolidays = [...prevYearHolidays, ...currentYearHolidays, ...nextYearHolidays];
|
||||||
|
setHolidays(allHolidays);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTeamLeaves = async () => {
|
||||||
|
if (user?.id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost/GTA/project/public/getTeamLeaves.php?user_id=${user.id}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
setTeamLeaves(data.leaves || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur récupération congés équipe:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadHolidays();
|
||||||
|
loadTeamLeaves();
|
||||||
|
}, [currentDate.getFullYear()]);
|
||||||
|
|
||||||
|
// Fermer le menu contextuel quand on clique ailleurs
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = () => {
|
||||||
|
setContextMenu({ show: false, x: 0, y: 0 });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (contextMenu.show) {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('click', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [contextMenu.show]);
|
||||||
|
|
||||||
|
const getDaysInMonth = (date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const daysInMonth = lastDay.getDate();
|
||||||
|
const startingDayOfWeek = (firstDay.getDay() + 6) % 7; // Adjust for Monday start
|
||||||
|
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
// Add empty cells for days before the first day of the month
|
||||||
|
for (let i = 0; i < startingDayOfWeek; i++) {
|
||||||
|
days.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add days of the month (only weekdays)
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const currentDay = new Date(year, month, day);
|
||||||
|
const dayOfWeek = currentDay.getDay();
|
||||||
|
|
||||||
|
// Only add weekdays (Monday = 1 to Friday = 5)
|
||||||
|
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
|
||||||
|
days.push(currentDay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateMonth = (direction) => {
|
||||||
|
setCurrentDate(prev => {
|
||||||
|
const newDate = new Date(prev);
|
||||||
|
if (direction === 'prev') {
|
||||||
|
newDate.setMonth(prev.getMonth() - 1);
|
||||||
|
} else {
|
||||||
|
newDate.setMonth(prev.getMonth() + 1);
|
||||||
|
}
|
||||||
|
return newDate;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToday = (date) => {
|
||||||
|
if (!date) return false;
|
||||||
|
const today = new Date();
|
||||||
|
return date.toDateString() === today.toDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPastDate = (date) => {
|
||||||
|
if (!date) return false;
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return date < today;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isHoliday = (date) => {
|
||||||
|
if (!date) return false;
|
||||||
|
return holidays.some(holiday => holiday.date.toDateString() === date.toDateString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHolidayName = (date) => {
|
||||||
|
if (!date) return null;
|
||||||
|
const holiday = holidays.find(holiday => holiday.date.toDateString() === date.toDateString());
|
||||||
|
return holiday ? holiday.name : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelected = (date) => {
|
||||||
|
if (!date || !selectedDate) return false;
|
||||||
|
if (selectedEndDate) {
|
||||||
|
return date >= selectedDate && date <= selectedEndDate;
|
||||||
|
}
|
||||||
|
return date.toDateString() === selectedDate.toDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateWorkingDays = (start, end) => {
|
||||||
|
if (!start || !end) return 0;
|
||||||
|
|
||||||
|
let workingDays = 0;
|
||||||
|
const current = new Date(start);
|
||||||
|
|
||||||
|
while (current <= end) {
|
||||||
|
const dayOfWeek = current.getDay();
|
||||||
|
if (dayOfWeek >= 1 && dayOfWeek <= 5 && !isHoliday(current)) {
|
||||||
|
workingDays++;
|
||||||
|
}
|
||||||
|
current.setDate(current.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return workingDays;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateClick = (date) => {
|
||||||
|
if (!date || isPastDate(date) || isHoliday(date)) return;
|
||||||
|
|
||||||
|
if (!selectedDate) {
|
||||||
|
// Première sélection
|
||||||
|
setSelectedDate(date);
|
||||||
|
setSelectedEndDate(null);
|
||||||
|
setIsSelectingRange(true);
|
||||||
|
} else if (isSelectingRange && !selectedEndDate) {
|
||||||
|
// Deuxième sélection pour la plage
|
||||||
|
if (date >= selectedDate) {
|
||||||
|
setSelectedEndDate(date);
|
||||||
|
setIsSelectingRange(false);
|
||||||
|
// Ouvrir automatiquement le modal avec les dates pré-remplies
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowNewRequestModal(true);
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
// Si la date est antérieure, recommencer la sélection
|
||||||
|
setSelectedDate(date);
|
||||||
|
setSelectedEndDate(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Nouvelle sélection (reset)
|
||||||
|
setSelectedDate(date);
|
||||||
|
setSelectedEndDate(null);
|
||||||
|
setIsSelectingRange(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenu = (e, date) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!date || isPastDate(date) || isHoliday(date) || !isSelected(date)) return;
|
||||||
|
|
||||||
|
setContextMenu({
|
||||||
|
show: true,
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeSelection = (type) => {
|
||||||
|
setPreselectedType(type);
|
||||||
|
setContextMenu({ show: false, x: 0, y: 0 });
|
||||||
|
setShowNewRequestModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSelection = () => {
|
||||||
|
setSelectedDate(null);
|
||||||
|
setSelectedEndDate(null);
|
||||||
|
setIsSelectingRange(false);
|
||||||
|
setPreselectedType(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedDays = () => {
|
||||||
|
if (!selectedDate) return 0;
|
||||||
|
if (selectedEndDate) {
|
||||||
|
return calculateWorkingDays(selectedDate, selectedEndDate);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvailableTypes = () => {
|
||||||
|
const days = getSelectedDays();
|
||||||
|
const types = [];
|
||||||
|
|
||||||
|
if (leaveCounters.availableCP >= days) {
|
||||||
|
types.push({ key: 'CP', label: 'Congés payés', color: 'bg-blue-600', available: leaveCounters.availableCP });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (days <= 5 && leaveCounters.availableRTT >= days) {
|
||||||
|
types.push({ key: 'RTT', label: 'RTT', color: 'bg-green-600', available: leaveCounters.availableRTT });
|
||||||
|
}
|
||||||
|
|
||||||
|
types.push({ key: 'ABS', label: 'Congé maladie', color: 'bg-red-600', available: '∞' });
|
||||||
|
|
||||||
|
return types;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasLeave = (date) => {
|
||||||
|
if (!date) return false;
|
||||||
|
|
||||||
|
// Vérifier les congés de l'équipe
|
||||||
|
return teamLeaves.some(leave => {
|
||||||
|
const startDate = new Date(leave.start_date);
|
||||||
|
const endDate = new Date(leave.end_date);
|
||||||
|
return date >= startDate && date <= endDate;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const days = getDaysInMonth(currentDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex">
|
||||||
|
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
|
||||||
|
<div className="flex-1 lg:ml-60 p-4 lg:p-8">
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="lg:hidden mb-4">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">Calendrier</h1>
|
||||||
|
<p className="text-sm lg:text-base text-gray-600">Vue d'ensemble de vos congés</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 lg:gap-3">
|
||||||
|
{(selectedDate || selectedEndDate) && (
|
||||||
|
<button
|
||||||
|
onClick={resetSelection}
|
||||||
|
className="bg-gray-600 text-white px-3 lg:px-4 py-2 lg:py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
<span className="hidden lg:inline">Annuler sélection</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
<span className="hidden sm:inline">Nouvelle demande</span>
|
||||||
|
<span className="sm:hidden">Nouveau</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selection Info */}
|
||||||
|
{selectedDate && (
|
||||||
|
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-3 lg:p-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-blue-800 font-medium text-sm lg:text-base">
|
||||||
|
{selectedEndDate ? 'Plage sélectionnée' : 'Date sélectionnée'} :
|
||||||
|
{selectedDate.toLocaleDateString('fr-FR')}
|
||||||
|
{selectedEndDate && ` - ${selectedEndDate.toLocaleDateString('fr-FR')}`}
|
||||||
|
</p>
|
||||||
|
<p className="text-blue-600 text-xs lg:text-sm">
|
||||||
|
{getSelectedDays()} jour{getSelectedDays() > 1 ? 's' : ''} ouvré{getSelectedDays() > 1 ? 's' : ''}
|
||||||
|
{isSelectingRange && !selectedEndDate && (
|
||||||
|
<span> (cliquez sur une autre date pour créer une plage)</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!isSelectingRange && (
|
||||||
|
<p className="text-blue-600 text-xs lg:text-sm">
|
||||||
|
Clic droit pour choisir le type de congé
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Calendar */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 lg:p-6">
|
||||||
|
{/* Calendar Header */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-xl lg:text-2xl font-bold text-gray-900">
|
||||||
|
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||||
|
{isLoadingHolidays && (
|
||||||
|
<span className="ml-2 text-xs lg:text-sm text-gray-500">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
||||||
|
<span className="ml-2">Chargement des jours fériés...</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => navigateMonth('prev')}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => navigateMonth('next')}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Day Names */}
|
||||||
|
<div className="grid grid-cols-5 gap-2 mb-2">
|
||||||
|
{dayNames.map(day => (
|
||||||
|
<div key={day} className="p-2 lg:p-3 text-center text-xs lg:text-sm font-medium text-gray-500">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar Grid */}
|
||||||
|
<div className="grid grid-cols-5 gap-1 lg:gap-2">
|
||||||
|
{days.map((date, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`
|
||||||
|
min-h-[60px] lg:min-h-[80px] p-1 lg:p-2 text-center cursor-pointer rounded-lg transition-colors relative flex flex-col
|
||||||
|
${!date ? '' :
|
||||||
|
isPastDate(date) ? 'bg-gray-200 text-gray-500 cursor-not-allowed opacity-60' :
|
||||||
|
isHoliday(date) ? 'bg-red-100 text-red-800 cursor-not-allowed border border-red-200' :
|
||||||
|
isToday(date) ? 'bg-blue-100 text-blue-800 font-semibold' :
|
||||||
|
isSelected(date) ? 'bg-blue-600 text-white' :
|
||||||
|
hasLeave(date) ? 'bg-green-100 text-green-800' :
|
||||||
|
'hover:bg-gray-50'
|
||||||
|
}
|
||||||
|
${isSelectingRange && selectedDate && !selectedEndDate && date && date > selectedDate && !isPastDate(date) && !isHoliday(date) ? 'bg-blue-50' : ''}
|
||||||
|
`}
|
||||||
|
onClick={() => handleDateClick(date)}
|
||||||
|
onContextMenu={(e) => handleContextMenu(e, date)}
|
||||||
|
title={isHoliday(date) ? getHolidayName(date) : ''}
|
||||||
|
>
|
||||||
|
{date && (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<span className="text-xs lg:text-sm">{date.getDate()}</span>
|
||||||
|
{isHoliday(date) && getHolidayName(date) && (
|
||||||
|
<span className="text-xs text-red-700 font-medium mt-1 text-center leading-tight hidden lg:block">
|
||||||
|
{getHolidayName(date).length > 8 ? getHolidayName(date).substring(0, 8) + '...' : getHolidayName(date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{hasLeave(date) && (
|
||||||
|
<div className="w-1.5 h-1.5 lg:w-2 lg:h-2 bg-green-500 rounded-full mt-1"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="flex items-center gap-3 lg:gap-6 mt-6 pt-6 border-t border-gray-100 flex-wrap text-xs lg:text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 lg:w-3 lg:h-3 bg-blue-100 rounded"></div>
|
||||||
|
<span className="text-gray-600">Aujourd'hui</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 lg:w-3 lg:h-3 bg-green-100 rounded"></div>
|
||||||
|
<span className="text-gray-600 hidden lg:inline">Congés approuvés</span>
|
||||||
|
<span className="text-gray-600 lg:hidden">Approuvés</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 lg:w-3 lg:h-3 bg-yellow-100 rounded"></div>
|
||||||
|
<span className="text-gray-600 hidden lg:inline">Congés en attente</span>
|
||||||
|
<span className="text-gray-600 lg:hidden">En attente</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 lg:w-3 lg:h-3 bg-red-100 border border-red-200 rounded relative">
|
||||||
|
<div className="absolute top-0 right-0 w-1.5 h-1.5 bg-red-600 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-600 hidden lg:inline">Jours fériés</span>
|
||||||
|
<span className="text-gray-600 lg:hidden">Fériés</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 lg:w-3 lg:h-3 bg-gray-200 rounded opacity-60"></div>
|
||||||
|
<span className="text-gray-600 hidden lg:inline">Jours passés</span>
|
||||||
|
<span className="text-gray-600 lg:hidden">Passés</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 lg:w-3 lg:h-3 bg-blue-600 rounded"></div>
|
||||||
|
<span className="text-gray-600">Sélection</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Context Menu */}
|
||||||
|
{contextMenu.show && (
|
||||||
|
<div
|
||||||
|
className="fixed bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50 min-w-[200px]"
|
||||||
|
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||||
|
>
|
||||||
|
<div className="px-4 py-2 border-b border-gray-100">
|
||||||
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
{getSelectedDays()} jour{getSelectedDays() > 1 ? 's' : ''} sélectionné{getSelectedDays() > 1 ? 's' : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{getAvailableTypes().map(type => (
|
||||||
|
<button
|
||||||
|
key={type.key}
|
||||||
|
onClick={() => handleTypeSelection(type.key)}
|
||||||
|
className="w-full px-4 py-2 text-left hover:bg-gray-50 flex items-center justify-between text-sm"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${type.color}`}></div>
|
||||||
|
<span className="text-gray-900">{type.label}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{type.available} disponible{type.available !== 1 && type.available !== '∞' ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="border-t border-gray-100 mt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setContextMenu({ show: false, x: 0, y: 0 });
|
||||||
|
setShowNewRequestModal(true);
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-2 text-left hover:bg-gray-50 text-xs lg:text-sm text-gray-600"
|
||||||
|
>
|
||||||
|
Formulaire complet...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal nouvelle demande */}
|
||||||
|
{showNewRequestModal && (
|
||||||
|
<NewLeaveRequestModal
|
||||||
|
onClose={() => {
|
||||||
|
setShowNewRequestModal(false);
|
||||||
|
setPreselectedType(null);
|
||||||
|
}}
|
||||||
|
availableLeaveCounters={leaveCounters}
|
||||||
|
userId={user?.id}
|
||||||
|
onRequestSubmitted={() => {
|
||||||
|
resetSelection();
|
||||||
|
}}
|
||||||
|
preselectedStartDate={selectedDate}
|
||||||
|
preselectedEndDate={selectedEndDate}
|
||||||
|
preselectedType={preselectedType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Calendar;
|
||||||
408
project/src/pages/Dashboard.jsx
Normal file
408
project/src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
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 NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const [leaveCounters, setLeaveCounters] = useState({
|
||||||
|
availableCP: 0,
|
||||||
|
availableRTT: 0,
|
||||||
|
availableABS: 0,
|
||||||
|
rttInProcess: 0,
|
||||||
|
absenteism: 0
|
||||||
|
});
|
||||||
|
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||||
|
const [showAdminPanel, setShowAdminPanel] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [recentRequests, setRecentRequests] = useState([]);
|
||||||
|
const [allRequests, setAllRequests] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.id) {
|
||||||
|
fetchLeaveCounters();
|
||||||
|
fetchAllRequests();
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const fetchLeaveCounters = async () => {
|
||||||
|
try {
|
||||||
|
const url = `http://localhost/GTA/project/public/getLeaveCounters.php?user_id=${user.id}`;
|
||||||
|
console.log(' Dashboard - Récupération des compteurs:', url);
|
||||||
|
console.log(' Dashboard - User ID utilisé:', user.id);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
} 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');
|
||||||
|
setLeaveCounters({
|
||||||
|
availableCP: 25,
|
||||||
|
availableRTT: 10,
|
||||||
|
availableABS: 0,
|
||||||
|
rttInProcess: 0,
|
||||||
|
absenteism: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAllRequests = async () => {
|
||||||
|
console.log(' Dashboard - Début fetchAllRequests pour user:', user?.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `http://localhost/GTA/project/public/getRequests.php?user_id=${user.id}`;
|
||||||
|
console.log(' Dashboard - URL appelée:', url);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
console.log(' Dashboard - Réponse brute:', text);
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
console.log(' Dashboard - Données parsées:', data);
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(' Dashboard - Erreur lors de la récupération des demandes:', error);
|
||||||
|
|
||||||
|
// En cas d'erreur, on garde des tableaux vides
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost/GTA/project/public/resetLeaveCounters.php', {
|
||||||
|
method: 'POST',
|
||||||
|
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}`);
|
||||||
|
fetchLeaveCounters();
|
||||||
|
} else {
|
||||||
|
alert(` Erreur lors de la réinitialisation :\n${data.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur:', error);
|
||||||
|
alert(' Erreur de connexion au serveur');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openManualResetPage = () => {
|
||||||
|
window.open('http://localhost/GTA/project/public/manualResetCounters.php', '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
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';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
<div className="lg:ml-60 flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">Chargement des données...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex">
|
||||||
|
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
|
||||||
|
<div className="flex-1 lg:ml-60">
|
||||||
|
<div className="p-4 lg:p-8 w-full">
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="lg:hidden mb-4">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
Bonjour, {user?.name || user?.prenom || 'Utilisateur'} 👋
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm lg:text-base text-gray-600">
|
||||||
|
Voici un aperçu de vos congés et demandes récentes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 lg:gap-3">
|
||||||
|
{(user?.role === 'Admin' || user?.role === 'RH') && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdminPanel(!showAdminPanel)}
|
||||||
|
className="bg-gray-600 text-white px-3 lg:px-4 py-2 lg:py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors flex items-center gap-2"
|
||||||
|
title="Administration"
|
||||||
|
>
|
||||||
|
<Settings className="w-5 h-5" />
|
||||||
|
<span className="hidden lg:inline">Admin</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
<span className="hidden sm:inline">Nouvelle demande</span>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Requests Section */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Recent Requests */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg lg:text-xl font-semibold text-gray-900">Demandes récentes</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 lg:p-6">
|
||||||
|
{recentRequests.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">Aucune demande récente</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewRequestModal(true)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Faire votre première demande
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{recentRequests.map((request) => (
|
||||||
|
<div key={request.id} className="flex items-center justify-between p-3 lg:p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center gap-2 lg:gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium text-gray-900 text-sm lg:text-base truncate">{request.type}</p>
|
||||||
|
<p className="text-xs lg:text-sm text-gray-600">{request.dateDisplay}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`px-2 lg:px-3 py-1 rounded-full text-xs font-medium self-start lg:self-auto ${getStatusColor(request.status)}`}>
|
||||||
|
{request.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right ml-2">
|
||||||
|
<p className="font-medium text-gray-900 text-sm lg:text-base">{request.days}j</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* All Requests Summary */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg lg:text-xl font-semibold text-gray-900">Toutes les demandes</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 lg:p-6">
|
||||||
|
{allRequests.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">
|
||||||
|
<FileText className="w-6 h-6 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600">Aucune demande</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3 max-h-80 overflow-y-auto">
|
||||||
|
{allRequests.map((request) => (
|
||||||
|
<div key={request.id} className="flex items-center justify-between p-3 border border-gray-100 rounded-lg hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center gap-2 lg:gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium text-gray-900 text-sm truncate">{request.type}</p>
|
||||||
|
<p className="text-xs text-gray-600">{request.dateDisplay}</p>
|
||||||
|
<p className="text-xs text-gray-500">Soumis le {request.submittedDisplay}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium self-start lg:self-auto ${getStatusColor(request.status)}`}>
|
||||||
|
{request.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right ml-2">
|
||||||
|
<p className="font-medium text-gray-900 text-sm">{request.days}j</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal nouvelle demande */}
|
||||||
|
{showNewRequestModal && (
|
||||||
|
<NewLeaveRequestModal
|
||||||
|
onClose={() => setShowNewRequestModal(false)}
|
||||||
|
availableLeaveCounters={leaveCounters}
|
||||||
|
userId={user?.id}
|
||||||
|
onRequestSubmitted={() => {
|
||||||
|
fetchLeaveCounters();
|
||||||
|
fetchAllRequests();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
125
project/src/pages/Login.jsx
Normal file
125
project/src/pages/Login.jsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const { login } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
const success = await login(email, password);
|
||||||
|
if (success) {
|
||||||
|
navigate('/dashboard');
|
||||||
|
} else {
|
||||||
|
setError('Identifiants incorrects. Veuillez réessayer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex flex-col lg:flex-row">
|
||||||
|
{/* Image côté gauche */}
|
||||||
|
<div className="h-32 lg:h-auto lg:flex lg:w-1/2 bg-cover bg-center"
|
||||||
|
style={{ backgroundImage: "url('/assets/ImageEnsup.png')" }}>
|
||||||
|
<div className="w-full bg-black bg-opacity-40 flex items-center justify-center p-4">
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Formulaire côté droit */}
|
||||||
|
<div className="flex-1 lg:w-1/2 flex items-center justify-center p-4 lg:p-8">
|
||||||
|
<div className="max-w-md w-full">
|
||||||
|
<div className="bg-white rounded-2xl shadow-xl p-6 lg:p-8">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="text-center mb-6 lg:mb-8">
|
||||||
|
<div className="w-12 h-12 lg:w-16 lg:h-16 bg-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Building2 className="w-6 h-6 lg:w-8 lg:h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl lg:text-2xl font-bold text-gray-900">GTA</h1>
|
||||||
|
<p className="text-sm lg:text-base text-gray-600">Gestion de congés</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4 lg:space-y-6">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm lg:text-base font-medium text-gray-700 mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4 lg:w-5 lg:h-5" />
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full pl-9 lg:pl-10 pr-4 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="votre.email@entreprise.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm lg:text-base font-medium text-gray-700 mb-2">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4 lg:w-5 lg:h-5" />
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
<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"}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 lg:p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-red-600 text-xs lg:text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-blue-600 text-white py-2 lg:py-3 px-4 rounded-lg font-medium hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm lg:text-base"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Connexion...' : 'Se connecter'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
474
project/src/pages/Manager.jsx
Normal file
474
project/src/pages/Manager.jsx
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import Sidebar from '../components/Sidebar';
|
||||||
|
import { Users, CheckCircle, XCircle, Clock, Calendar, FileText, Menu, Eye, MessageSquare } from 'lucide-react';
|
||||||
|
|
||||||
|
const Manager = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const isEmployee = user?.role === 'Employe';
|
||||||
|
const [teamMembers, setTeamMembers] = useState([]);
|
||||||
|
const [pendingRequests, setPendingRequests] = useState([]);
|
||||||
|
const [allRequests, setAllRequests] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [selectedRequest, setSelectedRequest] = useState(null);
|
||||||
|
const [showValidationModal, setShowValidationModal] = useState(false);
|
||||||
|
const [validationComment, setValidationComment] = useState('');
|
||||||
|
const [validationAction, setValidationAction] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.id) {
|
||||||
|
fetchTeamData();
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const fetchTeamData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Récupérer les membres de l'équipe
|
||||||
|
await fetchTeamMembers();
|
||||||
|
|
||||||
|
// Récupérer les demandes en attente
|
||||||
|
await fetchPendingRequests();
|
||||||
|
|
||||||
|
// Récupérer toutes les demandes de l'équipe
|
||||||
|
await fetchAllTeamRequests();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la récupération des données équipe:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTeamMembers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost/GTA/project/public/getTeamMembers.php?manager_id=${user.id}`);
|
||||||
|
const text = await response.text();
|
||||||
|
console.log('Réponse équipe:', text);
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
if (data.success) {
|
||||||
|
setTeamMembers(data.team_members || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur récupération équipe:', error);
|
||||||
|
setTeamMembers([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPendingRequests = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost/GTA/project/public/getPendingRequests.php?manager_id=${user.id}`);
|
||||||
|
const text = await response.text();
|
||||||
|
console.log('Réponse demandes en attente:', text);
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
if (data.success) {
|
||||||
|
setPendingRequests(data.requests || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur récupération demandes en attente:', error);
|
||||||
|
setPendingRequests([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAllTeamRequests = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost/GTA/project/public/getAllTeamRequests.php?manager_id=${user.id}`);
|
||||||
|
const text = await response.text();
|
||||||
|
console.log('Réponse toutes demandes équipe:', text);
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
if (data.success) {
|
||||||
|
setAllRequests(data.requests || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur récupération toutes demandes:', error);
|
||||||
|
setAllRequests([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidateRequest = async (requestId, action, comment = '') => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost/GTA/project/public/validateRequest.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
request_id: requestId,
|
||||||
|
action: action, // 'approve' ou 'reject'
|
||||||
|
comment: comment,
|
||||||
|
validator_id: user.id
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
console.log('Réponse validation:', text);
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Rafraîchir les données
|
||||||
|
await fetchTeamData();
|
||||||
|
setShowValidationModal(false);
|
||||||
|
setSelectedRequest(null);
|
||||||
|
setValidationComment('');
|
||||||
|
|
||||||
|
alert(`Demande ${action === 'approve' ? 'approuvée' : 'refusée'} avec succès !`);
|
||||||
|
} else {
|
||||||
|
alert(`Erreur: ${data.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur validation:', error);
|
||||||
|
alert('Erreur lors de la validation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openValidationModal = (request, action) => {
|
||||||
|
setSelectedRequest(request);
|
||||||
|
setValidationAction(action);
|
||||||
|
setValidationComment('');
|
||||||
|
setShowValidationModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'En attente': return 'bg-yellow-100 text-yellow-800';
|
||||||
|
case 'Validée':
|
||||||
|
case 'Approuvé': return 'bg-green-100 text-green-800';
|
||||||
|
case 'Refusée': return 'bg-red-100 text-red-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'Congés payés':
|
||||||
|
case 'Congé payé': return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'RTT': return 'bg-green-100 text-green-800';
|
||||||
|
case 'Congé maladie': return 'bg-red-100 text-red-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
<div className="lg:ml-60 flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">Chargement des données équipe...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex">
|
||||||
|
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
|
||||||
|
<div className="flex-1 lg:ml-60">
|
||||||
|
<div className="p-4 lg:p-8 w-full">
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="lg:hidden mb-4">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
{isEmployee ? 'Mon équipe 👥' : 'Gestion d\'équipe 👥'}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm lg:text-base text-gray-600">
|
||||||
|
{isEmployee ? 'Consultez les congés de votre équipe' : 'Gérez les demandes de congés de votre équipe'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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">Équipe</p>
|
||||||
|
<p className="text-xl lg:text-2xl font-bold text-gray-900">{teamMembers.length}</p>
|
||||||
|
<p className="text-xs text-gray-500">membres</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||||
|
<Users 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">En attente</p>
|
||||||
|
<p className="text-xl lg:text-2xl font-bold text-gray-900">{pendingRequests.length}</p>
|
||||||
|
<p className="text-xs text-gray-500">demandes</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-yellow-100 rounded-lg flex items-center justify-center">
|
||||||
|
<Clock 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">Approuvées</p>
|
||||||
|
<p className="text-xl lg:text-2xl font-bold text-gray-900">
|
||||||
|
{allRequests.filter(r => r.status === 'Validée' || r.status === 'Approuvé').length}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">demandes</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||||
|
<CheckCircle className="w-4 h-4 lg:w-6 lg:h-6 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs lg:text-sm font-medium text-gray-600">Refusées</p>
|
||||||
|
<p className="text-xl lg:text-2xl font-bold text-gray-900">
|
||||||
|
{allRequests.filter(r => r.status === 'Refusée').length}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">demandes</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-red-100 rounded-lg flex items-center justify-center">
|
||||||
|
<XCircle className="w-4 h-4 lg:w-6 lg:h-6 text-red-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Demandes en attente */}
|
||||||
|
{!isEmployee && (
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg lg:text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
<Clock className="w-5 h-5 text-yellow-600" />
|
||||||
|
Demandes en attente ({pendingRequests.length})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 lg:p-6">
|
||||||
|
{pendingRequests.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Clock className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-600">Aucune demande en attente</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{pendingRequests.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 className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h3 className="font-medium text-gray-900">{request.employee_name}</h3>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(request.type)}`}>
|
||||||
|
{request.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600">{request.date_display}</p>
|
||||||
|
<p className="text-xs text-gray-500">Soumis le {request.submitted_display}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-medium text-gray-900">{request.days}j</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{request.reason && (
|
||||||
|
<div className="mb-3 p-2 bg-gray-50 rounded text-sm text-gray-700">
|
||||||
|
<strong>Motif:</strong> {request.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => openValidationModal(request, 'approve')}
|
||||||
|
className="flex-1 bg-green-600 text-white px-3 py-2 rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
Approuver
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openValidationModal(request, 'reject')}
|
||||||
|
className="flex-1 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 transition-colors flex items-center justify-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<XCircle className="w-4 h-4" />
|
||||||
|
Refuser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Équipe */}
|
||||||
|
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 ${isEmployee ? 'lg:col-span-2' : ''}`}>
|
||||||
|
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg lg:text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
<Users className="w-5 h-5 text-blue-600" />
|
||||||
|
Mon équipe ({teamMembers.length})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 lg:p-6">
|
||||||
|
{teamMembers.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Users className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-600">Aucun membre d'équipe</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{teamMembers.map((member) => (
|
||||||
|
<div key={member.id} className={`flex items-center justify-between p-3 bg-gray-50 rounded-lg ${isEmployee ? 'lg:p-4' : ''}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-blue-600 font-medium text-sm">
|
||||||
|
{member.prenom?.charAt(0)}{member.nom?.charAt(0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">{member.prenom} {member.nom}</p>
|
||||||
|
<p className="text-sm text-gray-600">{member.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isEmployee && (
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
{allRequests.filter(r => r.employee_id === member.id && r.status === 'En attente').length} en attente
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{allRequests.filter(r => r.employee_id === member.id).length} total
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Historique des demandes */}
|
||||||
|
{!isEmployee && (
|
||||||
|
<div className="mt-6 bg-white rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg lg:text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-gray-600" />
|
||||||
|
Historique des demandes ({allRequests.length})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 lg:p-6">
|
||||||
|
{allRequests.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-600">Aucune demande</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3 max-h-80 overflow-y-auto">
|
||||||
|
{allRequests.map((request) => (
|
||||||
|
<div key={request.id} className="flex items-center justify-between p-3 border border-gray-100 rounded-lg hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<p className="font-medium text-gray-900">{request.employee_name}</p>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(request.type)}`}>
|
||||||
|
{request.type}
|
||||||
|
</span>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(request.status)}`}>
|
||||||
|
{request.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600">{request.date_display}</p>
|
||||||
|
<p className="text-xs text-gray-500">Soumis le {request.submitted_display}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-medium text-gray-900">{request.days}j</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal de validation */}
|
||||||
|
{showValidationModal && selectedRequest && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl max-w-md w-full">
|
||||||
|
<div className="p-6 border-b border-gray-100">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">
|
||||||
|
{validationAction === 'approve' ? 'Approuver' : 'Refuser'} la demande
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-4 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<p className="font-medium text-gray-900">{selectedRequest.employee_name}</p>
|
||||||
|
<p className="text-sm text-gray-600">{selectedRequest.type} - {selectedRequest.date_display}</p>
|
||||||
|
<p className="text-sm text-gray-600">{selectedRequest.days} jour(s)</p>
|
||||||
|
{selectedRequest.reason && (
|
||||||
|
<p className="text-sm text-gray-600 mt-2"><strong>Motif:</strong> {selectedRequest.reason}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Commentaire {validationAction === 'reject' ? '(obligatoire)' : '(optionnel)'}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={validationComment}
|
||||||
|
onChange={(e) => setValidationComment(e.target.value)}
|
||||||
|
placeholder={validationAction === 'approve' ? 'Commentaire optionnel...' : 'Motif du refus...'}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowValidationModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleValidateRequest(selectedRequest.id, validationAction, validationComment)}
|
||||||
|
disabled={validationAction === 'reject' && !validationComment.trim()}
|
||||||
|
className={`flex-1 px-4 py-2 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${validationAction === 'approve'
|
||||||
|
? 'bg-green-600 hover:bg-green-700'
|
||||||
|
: 'bg-red-600 hover:bg-red-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{validationAction === 'approve' ? 'Approuver' : 'Refuser'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Manager;
|
||||||
624
project/src/pages/Requests.jsx
Normal file
624
project/src/pages/Requests.jsx
Normal file
@@ -0,0 +1,624 @@
|
|||||||
|
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 NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
||||||
|
|
||||||
|
const Requests = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const [leaveCounters, setLeaveCounters] = useState({
|
||||||
|
availableCP: 0,
|
||||||
|
availableRTT: 0,
|
||||||
|
availableABS: 0,
|
||||||
|
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 [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
const [typeFilter, setTypeFilter] = useState('all');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [requestsPerPage] = useState(10);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.id) {
|
||||||
|
fetchLeaveCounters();
|
||||||
|
fetchAllRequests();
|
||||||
|
}
|
||||||
|
}, [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()) ||
|
||||||
|
request.reason.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
request.dateDisplay.includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par statut
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(request => request.status === statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par type
|
||||||
|
if (typeFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(request => request.type === typeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredRequests(filtered);
|
||||||
|
setCurrentPage(1); // Reset à la première page lors du filtrage
|
||||||
|
}, [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));
|
||||||
|
throw new Error('Le serveur PHP ne répond pas correctement');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setLeaveCounters(data.counters);
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost/GTA/project/public/resetLeaveCounters.php', {
|
||||||
|
method: 'POST',
|
||||||
|
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}`);
|
||||||
|
fetchLeaveCounters();
|
||||||
|
} else {
|
||||||
|
alert(` Erreur lors de la réinitialisation :\n${data.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur:', error);
|
||||||
|
alert(' Erreur de connexion au serveur');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openManualResetPage = () => {
|
||||||
|
window.open('http://localhost/GTA/project/public/manualResetCounters.php', '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
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';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
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">
|
||||||
|
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
<div className="lg:ml-60 flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">Chargement des données...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex">
|
||||||
|
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||||
|
|
||||||
|
<div className="flex-1 lg:ml-60 p-4 lg:p-8">
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="lg:hidden mb-4">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
<span className="hidden sm:inline">Nouvelle demande</span>
|
||||||
|
<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 */}
|
||||||
|
<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">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<option value="all">Tous les statuts</option>
|
||||||
|
<option value="En attente">En attente</option>
|
||||||
|
<option value="Validée">Validée</option>
|
||||||
|
<option value="Refusée">Refusée</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<Filter className="w-4 h-4" />
|
||||||
|
Page {currentPage} sur {totalPages || 1}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 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>
|
||||||
|
</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>
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</div>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(request.status)}`}>
|
||||||
|
{request.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>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</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 */}
|
||||||
|
{showNewRequestModal && (
|
||||||
|
<NewLeaveRequestModal
|
||||||
|
onClose={() => setShowNewRequestModal(false)}
|
||||||
|
availableLeaveCounters={leaveCounters}
|
||||||
|
userId={user?.id}
|
||||||
|
onRequestSubmitted={() => {
|
||||||
|
fetchLeaveCounters();
|
||||||
|
fetchAllRequests();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Requests;
|
||||||
8
project/tailwind.config.js
Normal file
8
project/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
10
project/vite.config.js
Normal file
10
project/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['lucide-react'],
|
||||||
|
},
|
||||||
|
});
|
||||||
10
project/vite.config.ts
Normal file
10
project/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['lucide-react'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user