Ajoutez des fichiers projet.
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user