résumé de la demande

This commit is contained in:
2025-08-08 14:37:37 +02:00
parent b066dcd136
commit 011620fb39
5 changed files with 227 additions and 493 deletions

View File

@@ -4,7 +4,6 @@ header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, OPTIONS"); header("Access-Control-Allow-Methods: GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type"); header("Access-Control-Allow-Headers: Content-Type");
// Gère la requête OPTIONS (pré-vol CORS)
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
http_response_code(200); http_response_code(200);
exit(); exit();
@@ -12,7 +11,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
header("Content-Type: application/json"); header("Content-Type: application/json");
// Log des erreurs pour debug
ini_set('display_errors', 1); ini_set('display_errors', 1);
ini_set('display_startup_errors', 1); ini_set('display_startup_errors', 1);
error_reporting(E_ALL); error_reporting(E_ALL);
@@ -22,17 +20,14 @@ $dbname = "DemandeConge";
$username = "wpuser"; $username = "wpuser";
$password = "-2b/)ru5/Bi8P[7_"; $password = "-2b/)ru5/Bi8P[7_";
// Crée une nouvelle connexion à la base de données
$conn = new mysqli($host, $username, $password, $dbname); $conn = new mysqli($host, $username, $password, $dbname);
// Vérifie la connexion
if ($conn->connect_error) { if ($conn->connect_error) {
error_log("Erreur connexion DB getRequests: " . $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]); echo json_encode(["success" => false, "message" => "Erreur de connexion à la base de données : " . $conn->connect_error]);
exit(); exit();
} }
// Récupère l'ID utilisateur depuis les paramètres de requête GET
$userId = $_GET['user_id'] ?? null; $userId = $_GET['user_id'] ?? null;
error_log("=== DEBUT getRequests.php ==="); error_log("=== DEBUT getRequests.php ===");
@@ -45,9 +40,6 @@ if ($userId === null) {
exit(); 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 = ?"; $checkUserQuery = "SELECT ID, Nom, Prenom FROM Users WHERE ID = ?";
$checkUserStmt = $conn->prepare($checkUserQuery); $checkUserStmt = $conn->prepare($checkUserQuery);
if ($checkUserStmt) { if ($checkUserStmt) {
@@ -62,15 +54,13 @@ if ($checkUserStmt) {
$checkUserStmt->close(); $checkUserStmt->close();
} }
// Fonction pour calculer les jours ouvrés (hors week-ends)
function getWorkingDays($startDate, $endDate) { function getWorkingDays($startDate, $endDate) {
$workingDays = 0; $workingDays = 0;
$current = new DateTime($startDate); $current = new DateTime($startDate);
$end = new DateTime($endDate); $end = new DateTime($endDate);
while ($current <= $end) { while ($current <= $end) {
$dayOfWeek = (int)$current->format('N'); // 1 (Lundi) à 7 (Dimanche) $dayOfWeek = (int)$current->format('N');
if ($dayOfWeek < 6) { // Si ce n'est ni Samedi (6) ni Dimanche (7) if ($dayOfWeek < 6) {
$workingDays++; $workingDays++;
} }
$current->modify('+1 day'); $current->modify('+1 day');
@@ -79,7 +69,6 @@ function getWorkingDays($startDate, $endDate) {
} }
try { try {
// Requête pour récupérer les demandes de l'utilisateur avec les informations du type de congé
$query = " $query = "
SELECT SELECT
dc.Id, dc.Id,
@@ -89,6 +78,7 @@ try {
dc.DateDemande, dc.DateDemande,
dc.Commentaire, dc.Commentaire,
dc.Validateur, dc.Validateur,
dc.DocumentJoint, -- 👈 CHAMP AJOUTÉ ICI
tc.Nom as TypeConge tc.Nom as TypeConge
FROM DemandeConge dc FROM DemandeConge dc
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
@@ -96,8 +86,6 @@ try {
ORDER BY dc.DateDemande DESC ORDER BY dc.DateDemande DESC
"; ";
error_log("getRequests - Requête SQL: $query");
$stmt = $conn->prepare($query); $stmt = $conn->prepare($query);
if ($stmt === false) { if ($stmt === false) {
throw new Exception("Erreur de préparation de la requête : " . $conn->error); throw new Exception("Erreur de préparation de la requête : " . $conn->error);
@@ -107,29 +95,11 @@ try {
$stmt->execute(); $stmt->execute();
$result = $stmt->get_result(); $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 = []; $requests = [];
while ($row = $result->fetch_assoc()) { while ($row = $result->fetch_assoc()) {
error_log("getRequests - Traitement demande ID: " . $row['Id']);
// Calcul des jours ouvrés
$workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']); $workingDays = getWorkingDays($row['DateDebut'], $row['DateFin']);
// Mapping des types de congés pour l'affichage
$displayType = $row['TypeConge']; $displayType = $row['TypeConge'];
switch ($row['TypeConge']) { switch ($row['TypeConge']) {
case 'Congé payé': case 'Congé payé':
@@ -143,18 +113,23 @@ try {
break; break;
} }
// Formatage des dates pour l'affichage
$startDate = new DateTime($row['DateDebut']); $startDate = new DateTime($row['DateDebut']);
$endDate = new DateTime($row['DateFin']); $endDate = new DateTime($row['DateFin']);
$submittedDate = new DateTime($row['DateDemande']); $submittedDate = new DateTime($row['DateDemande']);
// Format d'affichage des dates
if ($row['DateDebut'] === $row['DateFin']) { if ($row['DateDebut'] === $row['DateFin']) {
$dateDisplay = $startDate->format('d/m/Y'); $dateDisplay = $startDate->format('d/m/Y');
} else { } else {
$dateDisplay = $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'); $dateDisplay = $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y');
} }
// 👇 GÉNÉRATION DU LIEN VERS LE FICHIER
$fileUrl = null;
if ($row['TypeConge'] === 'Congé maladie' && !empty($row['DocumentJoint'])) {
$fileName = basename($row['DocumentJoint']);
$fileUrl = 'http://localhost/GTA/project/uploads/'. $fileName;
}
$requests[] = [ $requests[] = [
'id' => (int)$row['Id'], 'id' => (int)$row['Id'],
'type' => $displayType, 'type' => $displayType,
@@ -166,16 +141,13 @@ try {
'reason' => $row['Commentaire'] ?: 'Aucun commentaire', 'reason' => $row['Commentaire'] ?: 'Aucun commentaire',
'submittedAt' => $row['DateDemande'], 'submittedAt' => $row['DateDemande'],
'submittedDisplay' => $submittedDate->format('d/m/Y'), 'submittedDisplay' => $submittedDate->format('d/m/Y'),
'validator' => $row['Validateur'] ?: null 'validator' => $row['Validateur'] ?: null,
'fileUrl' => $fileUrl
]; ];
} }
$stmt->close(); $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([ echo json_encode([
"success" => true, "success" => true,
"message" => "Demandes récupérées avec succès.", "message" => "Demandes récupérées avec succès.",

View File

@@ -255,52 +255,64 @@ const NewLeaveRequestModal = ({
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!validateForm()) return; if (!validateForm()) return;
setIsSubmitting(true); setIsSubmitting(true);
setError(''); setError('');
try { try {
// Créer une demande pour chaque type de congé const finalTypes = formData.types.map(type =>
const requests = formData.types.map(type => { type === 'Autres' ? otherLeaveType : type
const days = formData.types.length > 1 ? (typeDistribution[type] || 0) : calculatedDays;
// Utiliser le type sélectionné dans la liste déroulante si "Autre" est coché
const finalType = type === 'Autres' ? otherLeaveType : type;
return {
EmployeeId: userId,
TypeConge: type,
DateDebut: formData.startDate,
DateFin: formData.endDate,
Commentaire: formData.reason + (formData.types.length > 1 ? ` (${days} jours ${getTypeLabel(type)})` : ''),
NumDays: days
};
});
// Soumettre toutes les demandes
const responses = await Promise.all(
requests.map(requestData =>
fetch('http://localhost/GTA/project/public/submitLeaveRequest.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
})
)
); );
const results = await Promise.all(responses.map(r => r.json())); const repartition = finalTypes.map(type => ({
TypeConge: type,
NombreJours: formData.types.length > 1
? (typeDistribution[type] || 0)
: calculatedDays
}));
const allSuccessful = results.every(result => result.success); const requestData = {
EmployeeId: userId,
DateDebut: formData.startDate,
DateFin: formData.endDate,
Commentaire: formData.reason,
NombreJours: calculatedDays,
Repartition: repartition
};
console.log("Payload envoyé au backend :", JSON.stringify(requestData, null, 2));
if (allSuccessful) { const formDataToSend = new FormData();
formDataToSend.append('data', JSON.stringify(requestData));
// Ajouter les fichiers
formData.medicalDocuments.forEach((file, index) => {
formDataToSend.append(`medicalDocuments[]`, file);
});
const response = await fetch('http://localhost/GTA/project/public/submitLeaveRequest.php', {
method: 'POST',
body: formDataToSend
});
const text = await response.text();
let result;
try {
result = JSON.parse(text);
} catch (err) {
console.error("Réponse non JSON:", text);
setError("Erreur serveur : réponse invalide.");
return;
}
if (result.success) {
onRequestSubmitted?.(); onRequestSubmitted?.();
onClose(); onClose();
} else { } else {
const failedResults = results.filter(r => !r.success); setError(result.message || 'Erreur lors de la soumission');
setError(`Erreur lors de la soumission : ${failedResults.map(r => r.message).join(', ')}`);
} }
} catch (error) { } catch (error) {
console.error('Erreur:', error); console.error('Erreur:', error);
setError('Erreur de connexion au serveur'); setError('Erreur de connexion au serveur');
@@ -309,6 +321,7 @@ const NewLeaveRequestModal = ({
} }
}; };
const getTypeLabel = (type) => { const getTypeLabel = (type) => {
switch (type) { switch (type) {
case 'CP': return 'Congés payés'; case 'CP': return 'Congés payés';

View File

@@ -89,14 +89,11 @@ const Login = () => {
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-900"
aria-label={showPassword ? "Masquer le mot de passe" : "Afficher le mot de passe"} title={showPassword ? "Masquer le mot de passe" : "Afficher le mot de passe"}
> >
{showPassword ? ( {/* L'icône est choisie en fonction de l'état de showPassword */}
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import Sidebar from '../components/Sidebar'; import Sidebar from '../components/Sidebar';
import { Calendar as CalendarIcon, Clock, Users, TrendingUp, Plus, Settings, RefreshCw, Search, Filter, Eye, Edit, Trash2, Menu } from 'lucide-react'; import { Calendar as CalendarIcon, Clock, Plus, Settings, RefreshCw, Search, Filter, Eye, Edit, Trash2, Menu, X } from 'lucide-react';
import NewLeaveRequestModal from '../components/NewLeaveRequestModal'; import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
const Requests = () => { const Requests = () => {
const { user } = useAuth(); const { user } = useAuth();
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const [leaveCounters, setLeaveCounters] = useState({ const [leaveCounters, setLeaveCounters] = useState({
availableCP: 0, availableCP: 0,
@@ -14,14 +15,19 @@ const Requests = () => {
rttInProcess: 0, rttInProcess: 0,
absenteism: 0 absenteism: 0
}); });
const [showNewRequestModal, setShowNewRequestModal] = useState(false); const [showNewRequestModal, setShowNewRequestModal] = useState(false);
const [showAdminPanel, setShowAdminPanel] = useState(false); const [showAdminPanel, setShowAdminPanel] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [allRequests, setAllRequests] = useState([]); const [allRequests, setAllRequests] = useState([]);
const [filteredRequests, setFilteredRequests] = useState([]); const [filteredRequests, setFilteredRequests] = useState([]);
const [selectedRequest, setSelectedRequest] = useState(null); // 👈 Nouveau
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all'); const [typeFilter, setTypeFilter] = useState('all');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [requestsPerPage] = useState(10); const [requestsPerPage] = useState(10);
@@ -32,11 +38,9 @@ const Requests = () => {
} }
}, [user]); }, [user]);
// Filtrage des demandes
useEffect(() => { useEffect(() => {
let filtered = allRequests; let filtered = allRequests;
// Filtre par terme de recherche
if (searchTerm) { if (searchTerm) {
filtered = filtered.filter(request => filtered = filtered.filter(request =>
request.type.toLowerCase().includes(searchTerm.toLowerCase()) || request.type.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -45,12 +49,10 @@ const Requests = () => {
); );
} }
// Filtre par statut
if (statusFilter !== 'all') { if (statusFilter !== 'all') {
filtered = filtered.filter(request => request.status === statusFilter); filtered = filtered.filter(request => request.status === statusFilter);
} }
// Filtre par type
if (typeFilter !== 'all') { if (typeFilter !== 'all') {
if (typeFilter === 'Autres') { if (typeFilter === 'Autres') {
const otherTypes = ['Récup', 'Congés sans solde', 'Congés pour évènement familial', 'Congé maternité', 'Congé paternité', 'Congé parental', 'Congé parental à temps partiel']; const otherTypes = ['Récup', 'Congés sans solde', 'Congés pour évènement familial', 'Congé maternité', 'Congé paternité', 'Congé parental', 'Congé parental à temps partiel'];
@@ -61,20 +63,18 @@ const Requests = () => {
} }
setFilteredRequests(filtered); setFilteredRequests(filtered);
setCurrentPage(1); // Reset à la première page lors du filtrage setCurrentPage(1);
}, [allRequests, searchTerm, statusFilter, typeFilter]); }, [allRequests, searchTerm, statusFilter, typeFilter]);
const fetchLeaveCounters = async () => { const fetchLeaveCounters = async () => {
try { try {
const response = await fetch(`http://localhost/GTA/project/public/getLeaveCounters.php?user_id=${user.id}`); const response = await fetch(`http://localhost/GTA/project/public/getLeaveCounters.php?user_id=${user.id}`);
const text = await response.text(); const text = await response.text();
console.log(' Requests - Réponse brute compteurs:', text);
let data; let data;
try { try {
data = JSON.parse(text); data = JSON.parse(text);
} catch (parseError) { } catch {
console.error(' Requests - Réponse non-JSON:', text.substring(0, 200));
throw new Error('Le serveur PHP ne répond pas correctement'); throw new Error('Le serveur PHP ne répond pas correctement');
} }
@@ -84,62 +84,49 @@ const Requests = () => {
throw new Error(data.message || 'Erreur lors de la récupération des compteurs'); throw new Error(data.message || 'Erreur lors de la récupération des compteurs');
} }
} catch (error) { } catch (error) {
console.error('Erreur lors de la récupération des compteurs:', error); console.error('Erreur compteurs:', error);
} }
}; };
const fetchAllRequests = async () => { const fetchAllRequests = async () => {
console.log('Requests - Début fetchAllRequests pour user:', user?.id);
try { try {
const url = `http://localhost/GTA/project/public/getRequests.php?user_id=${user.id}`; 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 response = await fetch(url);
const text = await response.text(); const text = await response.text();
console.log(' Requests - Réponse brute:', text);
const data = JSON.parse(text); const data = JSON.parse(text);
console.log(' Requests - Données parsées:', data);
if (data.success) { if (data.success) {
console.log(' Requests - Demandes récupérées:', data.requests?.length);
setAllRequests(data.requests || []); setAllRequests(data.requests || []);
} else { } else {
throw new Error(data.message || 'Erreur lors de la récupération des demandes'); throw new Error(data.message || 'Erreur lors de la récupération des demandes');
} }
} catch (error) { } catch (error) {
console.error(' Requests - Erreur:', error); console.error('Erreur requêtes:', error);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
const handleResetCounters = async () => { 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 ?')) { if (!confirm('Réinitialiser les compteurs ? Cette action est irréversible.')) return;
return;
}
try { try {
const response = await fetch('http://localhost/GTA/project/public/resetLeaveCounters.php', { const response = await fetch('http://localhost/GTA/project/public/resetLeaveCounters.php', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
},
body: JSON.stringify({ manual_reset: true }), body: JSON.stringify({ manual_reset: true }),
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
alert(` Réinitialisation réussie !\n\n• ${data.details.employees_updated} employés mis à jour\n• Exercice CP : ${data.details.leave_year}\n• Année RTT : ${data.details.rtt_year}\n• Date : ${data.details.reset_date}`); alert('Réinitialisation réussie.');
fetchLeaveCounters(); fetchLeaveCounters();
} else { } else {
alert(` Erreur lors de la réinitialisation :\n${data.message}`); alert(`Erreur : ${data.message}`);
} }
} catch (error) { } catch {
console.error('Erreur:', error); alert('Erreur de connexion au serveur');
alert(' Erreur de connexion au serveur');
} }
}; };
@@ -152,41 +139,26 @@ const Requests = () => {
case 'Approuvé': case 'Approuvé':
case 'Validée': return 'bg-green-100 text-green-800'; case 'Validée': return 'bg-green-100 text-green-800';
case 'En attente': return 'bg-yellow-100 text-yellow-800'; case 'En attente': return 'bg-yellow-100 text-yellow-800';
case 'Refusé': return 'bg-red-100 text-red-800'; case 'Refusé':
case 'Refusée': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800'; default: return 'bg-gray-100 text-gray-800';
} }
}; };
// Pagination const handleViewRequest = (request) => {
setSelectedRequest(request);
};
const handleCloseDetails = () => {
setSelectedRequest(null);
};
const indexOfLastRequest = currentPage * requestsPerPage; const indexOfLastRequest = currentPage * requestsPerPage;
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage; const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
const currentRequests = filteredRequests.slice(indexOfFirstRequest, indexOfLastRequest); const currentRequests = filteredRequests.slice(indexOfFirstRequest, indexOfLastRequest);
const totalPages = Math.ceil(filteredRequests.length / requestsPerPage); const totalPages = Math.ceil(filteredRequests.length / requestsPerPage);
const paginate = (pageNumber) => setCurrentPage(pageNumber); 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) { if (isLoading) {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
@@ -206,12 +178,9 @@ const Requests = () => {
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} /> <Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
<div className="flex-1 lg:ml-60 p-4 lg:p-8"> <div className="flex-1 lg:ml-60 p-4 lg:p-8">
{/* Mobile menu button */} {/* Bouton mobile */}
<div className="lg:hidden mb-4"> <div className="lg:hidden mb-4">
<button <button onClick={() => setSidebarOpen(true)} className="p-2 rounded-lg bg-white shadow-sm border border-gray-200">
onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg bg-white shadow-sm border border-gray-200"
>
<Menu className="w-6 h-6" /> <Menu className="w-6 h-6" />
</button> </button>
</div> </div>
@@ -222,154 +191,37 @@ const Requests = () => {
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2"> <h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
Mes Demandes de Congés Mes Demandes de Congés
</h1> </h1>
<p className="text-sm lg:text-base text-gray-600"> <p className="text-sm lg:text-base text-gray-600">Gérez toutes vos demandes de congés</p>
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>
<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 */} {/* Filtres */}
{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="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"> <div className="flex flex-col lg:flex-row gap-4">
{/* Barre de recherche */} <div className="flex-1 relative">
<div className="flex-1"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<div className="relative"> <input
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> type="text"
<input placeholder="Rechercher par type, motif ou date..."
type="text" value={searchTerm}
placeholder="Rechercher par type, motif ou date..." onChange={(e) => setSearchTerm(e.target.value)}
value={searchTerm} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
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> </div>
{/* Filtres */}
<div className="flex gap-3"> <div className="flex gap-3">
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)} onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
> >
<option value="all">Tous les statuts</option> <option value="all">Tous les statuts</option>
<option value="En attente">En attente</option> <option value="En attente">En attente</option>
@@ -380,237 +232,137 @@ const Requests = () => {
<select <select
value={typeFilter} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)} onChange={(e) => setTypeFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
> >
<option value="all">Tous les types</option> <option value="all">Tous les types</option>
<option value="Congés payés">Congés payés</option> <option value="Congés payés">Congés payés</option>
<option value="RTT">RTT</option> <option value="RTT">RTT</option>
<option value="Congé maladie">Congé maladie</option> <option value="Congé maladie">Congé maladie</option>
<option value="Autres">Autres types de congés</option> {/* Nouvelle option */} <option value="Autres">Autres types</option>
</select> </select>
</div> </div>
</div> </div>
{/* Statistiques des résultats */}
<div className="mt-4 text-sm text-gray-600"> <div className="mt-4 text-sm text-gray-600">
{filteredRequests.length} demande{filteredRequests.length > 1 ? 's' : ''} trouvée{filteredRequests.length > 1 ? 's' : ''} {filteredRequests.length} demande{filteredRequests.length > 1 ? 's' : ''} trouvée{filteredRequests.length > 1 ? 's' : ''}
{allRequests.length !== filteredRequests.length && ` sur ${allRequests.length} au total`} {allRequests.length !== filteredRequests.length && ` sur ${allRequests.length} au total`}
</div> </div>
</div> </div>
{/* Liste des Demandes */} {/* Tableau + Détails */}
<div className="bg-white rounded-xl shadow-sm border border-gray-100"> <div className="flex flex-col lg:flex-row gap-6">
<div className="p-6 border-b border-gray-100"> {/* Tableau des demandes */}
<div className="flex items-center justify-between"> <div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-100">
<h2 className="text-xl font-semibold text-gray-900"> <div className="p-6 border-b border-gray-100">
Toutes mes demandes <div className="flex items-center justify-between">
</h2> <h2 className="text-xl font-semibold text-gray-900">Toutes mes demandes</h2>
<div className="flex items-center gap-2 text-sm text-gray-500"> <div className="flex items-center gap-2 text-sm text-gray-500">
<Filter className="w-4 h-4" /> <Filter className="w-4 h-4" />
Page {currentPage} sur {totalPages || 1} Page {currentPage} sur {totalPages || 1}
</div>
</div> </div>
</div> </div>
<div className="p-6">
{currentRequests.length === 0 ? (
<div className="text-center py-8 text-gray-600">Aucune demande à afficher.</div>
) : (
<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 text-gray-700">Type</th>
<th className="text-left py-3 px-4 text-gray-700">Dates</th>
<th className="text-left py-3 px-4 text-gray-700">Jours</th>
<th className="text-left py-3 px-4 text-gray-700">Statut</th>
<th className="text-left py-3 px-4 text-gray-700">Soumis</th>
<th className="text-left py-3 px-4 text-gray-700">Actions</th>
</tr>
</thead>
<tbody>
{currentRequests.map(request => (
<tr key={request.id} className="border-b hover:bg-gray-50">
<td className="py-4 px-4">{request.type}</td>
<td className="py-4 px-4">{request.dateDisplay}</td>
<td className="py-4 px-4">{request.days}</td>
<td className="py-4 px-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(request.status)}`}>
{request.status}
</span>
</td>
<td className="py-4 px-4">{request.submittedDisplay}</td>
<td className="py-4 px-4">
<button onClick={() => handleViewRequest(request)} className="text-blue-600 hover:underline text-sm flex items-center gap-1">
<Eye className="w-4 h-4" /> Voir
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div> </div>
<div className="p-6"> {/* Détails */}
{currentRequests.length === 0 ? ( {selectedRequest && (
<div className="text-center py-8"> <div className="w-full lg:max-w-sm bg-white rounded-xl shadow-md border border-gray-100 p-6 h-fit sticky top-20 self-start">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3"> <div className="flex justify-between items-start mb-6">
<CalendarIcon className="w-6 h-6 text-gray-400" /> <h3 className="text-lg font-semibold">Détails de la demande</h3>
</div> <button onClick={handleCloseDetails} className="text-gray-500 hover:text-gray-800 p-2">
<p className="text-gray-600 mb-4"> <X className="w-4 h-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> </button>
</div> </div>
) : (
<> <div className="space-y-4 text-sm text-gray-700">
{/* Version Desktop */} <div>
<div className="hidden lg:block"> <p className="text-gray-500">Type</p>
<div className="overflow-x-auto"> <p className="text-base font-medium text-gray-900">{selectedRequest.type}</p>
<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> </div>
<div>
{/* Version Mobile */} <p className="text-gray-500">Dates</p>
<div className="lg:hidden space-y-4"> <p className="text-base font-medium text-gray-900">{selectedRequest.dateDisplay}</p>
{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> </div>
<div>
{/* Pagination */} <p className="text-gray-500">Nombre de jours</p>
{totalPages > 1 && ( <p className="text-base font-medium text-gray-900">{selectedRequest.days}</p>
<div className="flex items-center justify-between mt-6 pt-6 border-t border-gray-100"> </div>
<div className="text-sm text-gray-600"> <div>
Affichage de {indexOfFirstRequest + 1} à {Math.min(indexOfLastRequest, filteredRequests.length)} sur {filteredRequests.length} demandes <p className="text-gray-500">Statut</p>
</div> <span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(selectedRequest.status)}`}>
{selectedRequest.status}
<div className="flex items-center gap-2"> </span>
<button </div>
onClick={() => paginate(currentPage - 1)} {selectedRequest.reason && (
disabled={currentPage === 1} <div>
className="px-3 py-1 border border-gray-300 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50" <p className="text-gray-500">Motif</p>
> <p className="italic">{selectedRequest.reason}</p>
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>
)} )}
</>
)} {selectedRequest.fileUrl && (
</div>
<div>
<p className="text-gray-500">Arrêt maladie</p>
<a
href={selectedRequest.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-2"
>
<Eye className="w-4 h-4" />
Voir le fichier
</a>
</div>
)}
</div>
</div>
)}
</div> </div>
{/* Modal nouvelle demande */} {/* Modal */}
{showNewRequestModal && ( {showNewRequestModal && (
<NewLeaveRequestModal <NewLeaveRequestModal
onClose={() => setShowNewRequestModal(false)} onClose={() => setShowNewRequestModal(false)}

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB