Files
GTA/project/src/pages/Calendar.jsx

1667 lines
86 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import Sidebar from '../components/Sidebar';
import { ChevronLeft, ChevronRight, X, Menu, RefreshCw, Search, Filter, Calendar as CalendarIcon } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
import { useMsal } from "@azure/msal-react";
const Calendar = () => {
const { user } = useAuth();
const role = user?.role?.toLowerCase();
const userId = user?.id || user?.CollaborateurADId || user?.ID;
const [sidebarOpen, setSidebarOpen] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
const [hoveredLeave, setHoveredLeave] = useState(null);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [showFilters, setShowFilters] = useState(false);
const [selectedDate, setSelectedDate] = useState(null);
const [selectedEndDate, setSelectedEndDate] = useState(null);
const [isSelectingRange, setIsSelectingRange] = useState(false);
const [contextMenu, setContextMenu] = useState({ show: false, x: 0, y: 0 });
const [preselectedDates, setPreselectedDates] = useState(null);
const [holidays, setHolidays] = useState([]);
const [detailedCounters, setDetailedCounters] = useState(null);
const [teamLeaves, setTeamLeaves] = useState([]);
const [filters, setFilters] = useState({});
const [employeeFilter, setEmployeeFilter] = useState("all");
const [selectedCampus, setSelectedCampus] = useState("all");
const [selectedSociete, setSelectedSociete] = useState("all");
const [selectedService, setSelectedService] = useState("all");
const [selectedEmployees, setSelectedEmployees] = useState([]);
const [showEmployeeList, setShowEmployeeList] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [lastRefresh, setLastRefresh] = useState(new Date());
const [graphToken, setGraphToken] = useState(null);
const { instance, accounts } = useMsal();
const [isMobile, setIsMobile] = useState(false);
const [sseConnected, setSseConnected] = useState(false);
const [toasts, setToasts] = useState([]);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const monthNames = [
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
];
const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
const dayNamesMobile = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
useEffect(() => {
if (accounts.length > 0) {
const request = {
scopes: ["User.Read", "Mail.Send"],
account: accounts[0],
};
instance.acquireTokenSilent(request)
.then((response) => {
setGraphToken(response.accessToken);
})
.catch((err) => {
console.error("❌ Erreur récupération token Graph:", err);
});
}
}, [accounts, instance]);
const fetchFrenchHolidays = async (year) => {
try {
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();
const holidayDates = Object.keys(data).map(dateStr => {
const [year, month, day] = dateStr.split('-').map(Number);
return {
date: new Date(year, month - 1, day),
name: data[dateStr]
};
});
return holidayDates;
} catch (error) {
console.error('Erreur jours fériés:', error);
return [];
}
};
useEffect(() => {
const year = currentDate.getFullYear();
fetchFrenchHolidays(year).then(setHolidays);
}, [currentDate]);
const isHoliday = (date) => {
if (!date) return false;
return holidays.some(holiday =>
holiday.date.getDate() === date.getDate() &&
holiday.date.getMonth() === date.getMonth() &&
holiday.date.getFullYear() === date.getFullYear()
);
};
const getHolidayName = (date) => {
const holiday = holidays.find(h =>
h.date.getDate() === date.getDate() &&
h.date.getMonth() === date.getMonth() &&
h.date.getFullYear() === date.getFullYear()
);
return holiday?.name || '';
};
const fetchDetailedCounters = async () => {
try {
const response = await fetch(`http://localhost:3000/getDetailedLeaveCounters?user_id=${userId}`);
const data = await response.json();
if (data.success) {
setDetailedCounters(data.data);
}
} catch (error) {
console.error('💥 Erreur compteurs:', error);
}
};
const loadTeamLeaves = async () => {
if (userId) {
try {
let url = `http://localhost:3000/getTeamLeaves?user_id=${userId}&role=${user.role}`;
// ⭐ PRESIDENT/ADMIN/RH : Envoyer le campus
if (role === 'president' || role === 'rh' || role === 'admin') {
if (selectedCampus !== 'all') {
url += `&selectedCampus=${encodeURIComponent(selectedCampus)}`;
}
if (selectedSociete !== 'all') {
url += `&selectedSociete=${encodeURIComponent(selectedSociete)}`;
}
if (selectedService !== 'all') {
url += `&selectedService=${encodeURIComponent(selectedService)}`;
}
}
// ⭐ DIRECTEUR DE CAMPUS : Envoyer société et service
if (role === 'directeur de campus' || role === 'directrice de campus') {
if (selectedSociete !== 'all') {
url += `&selectedSociete=${encodeURIComponent(selectedSociete)}`;
}
if (selectedService !== 'all') {
url += `&selectedService=${encodeURIComponent(selectedService)}`;
}
}
console.log("📡 Appel API:", url);
const response = await fetch(url);
const data = await response.json();
console.log("📡 Réponse API :", data);
if (data.success) {
setTeamLeaves(data.leaves || []);
setFilters(data.filters || {});
}
} catch (error) {
console.error('Erreur récupération congés équipe:', error);
}
}
};
const refreshAllData = useCallback(async () => {
if (!userId) return;
if (!isLoading) setIsRefreshing(true);
try {
await Promise.all([
fetchDetailedCounters(),
loadTeamLeaves()
]);
setLastRefresh(new Date());
} catch (error) {
console.error('❌ Erreur lors du rafraîchissement:', error);
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, [userId]);
const showToast = useCallback((message, type = 'info') => {
const id = Date.now();
const newToast = { id, message, type };
setToasts(prev => [...prev, newToast]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3000);
}, []);
useEffect(() => {
refreshAllData();
}, [refreshAllData]);
// ⭐ Recharger quand les filtres changent
useEffect(() => {
if ((role === 'president' || role === 'rh' || role === 'admin') && selectedCampus) {
loadTeamLeaves();
}
}, [selectedCampus, role]);
// ⭐ Recharger pour Directeur de Campus quand société OU service changent
// Recharger pour Directeur de Campus quand société OU service changent
useEffect(() => {
if (role === 'directeur de campus' || role === 'directrice de campus') {
console.log("🔄 Rechargement Directeur Campus:", {
societe: selectedSociete,
service: selectedService,
role: role
});
loadTeamLeaves();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSociete, selectedService, role]);
const getDaysInMonth = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const days = [];
for (let day = 1; day <= daysInMonth; day++) {
const currentDay = new Date(year, month, day);
const dayOfWeek = currentDay.getDay();
// ⭐ MODIFIÉ : Inclure du lundi au samedi (1 à 6)
if (dayOfWeek >= 1 && dayOfWeek <= 6) {
days.push(currentDay);
}
}
return days;
};
const getDaysInMonthMobile = () => {
const year = currentDate.getFullYear();
const month = currentDate.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;
const days = [];
for (let i = 0; i < startingDayOfWeek; i++) {
days.push(null);
}
for (let day = 1; day <= daysInMonth; day++) {
const currentDay = new Date(year, month, day);
const dayOfWeek = currentDay.getDay();
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
days.push(currentDay);
}
}
return days;
};
// ⭐ FILTRAGE DE BASE SELON LE RÔLE ET LES FILTRES CAMPUS/SOCIÉTÉ/SERVICE
const getBaseFilteredLeaves = () => {
// Pour directeur de campus - le backend a déjà tout filtré
if (role === 'directeur de campus' || role === 'directrice de campus') {
return teamLeaves;
}
// Pour collaborateur, validateur, apprenti - pas de filtres
if (['collaborateur', 'collaboratrice','validateur', 'apprenti'].includes(role)) {
return teamLeaves;
}
// Pour president, admin, rh - appliquer les filtres frontend
return teamLeaves.filter(leave => {
if (selectedCampus !== 'all' && leave.campusnom !== selectedCampus) return false;
if (selectedSociete !== 'all' && leave.societenom !== selectedSociete) return false;
if (selectedService !== 'all' && leave.servicenom !== selectedService) return false;
return true;
});
};
// ⭐ OBTENIR TOUS LES EMPLOYÉS DISPONIBLES APRÈS FILTRAGE DE BASE
// ⭐ OBTENIR TOUS LES EMPLOYÉS DISPONIBLES - déjà filtrés par le backend
const getAllEmployees = () => {
if (!filters.employees || filters.employees.length === 0) {
return [];
}
// Pour directeur de campus - le backend a déjà tout filtré
if (role === 'directeur de campus' || role === 'directrice de campus') {
const employeeList = filters.employees.map(emp => ({
name: typeof emp === 'string' ? emp : emp.name,
campus: emp.campus || '',
societe: emp.societe || '',
service: emp.service || ''
}));
// Dédupliquer par nom
const uniqueEmployees = [];
const seenNames = new Set();
for (const emp of employeeList) {
if (!seenNames.has(emp.name)) {
seenNames.add(emp.name);
uniqueEmployees.push(emp);
}
}
return uniqueEmployees.sort((a, b) => a.name.localeCompare(b.name));
}
// Pour president/admin/rh - appliquer les filtres frontend
const filteredEmployees = filters.employees.filter(emp => {
if (role === 'president' || role === 'rh' || role === 'admin') {
if (selectedCampus !== 'all' && emp.campus !== selectedCampus) return false;
if (selectedSociete !== 'all' && emp.societe !== selectedSociete) return false;
if (selectedService !== 'all' && emp.service !== selectedService) return false;
return true;
}
// Pour collaborateur/validateur/apprenti - pas de filtre
return true;
}).map(emp => ({
name: typeof emp === 'string' ? emp : emp.name,
campus: emp.campus || '',
societe: emp.societe || '',
service: emp.service || ''
}));
// Dédupliquer par nom
const uniqueEmployees = [];
const seenNames = new Set();
for (const emp of filteredEmployees) {
if (!seenNames.has(emp.name)) {
seenNames.add(emp.name);
uniqueEmployees.push(emp);
}
}
return uniqueEmployees.sort((a, b) => a.name.localeCompare(b.name));
};
// ⭐ OBTENIR LES EMPLOYÉS À AFFICHER DANS LE TABLEAU
const getDisplayedEmployees = () => {
const allEmployees = getAllEmployees();
let filteredEmployees = allEmployees;
if (selectedEmployees.length > 0) {
filteredEmployees = allEmployees.filter(emp => selectedEmployees.includes(emp.name));
}
// ⭐ TRI AUTOMATIQUE pour President/RH/Admin/Directeur de Campus
// quand "Tous les campus" ET "Toutes les sociétés" ET "Tous les services"
const shouldAutoSort = (
['president', 'rh', 'admin'].includes(role) &&
selectedCampus === 'all' &&
selectedSociete === 'all' &&
selectedService === 'all'
) || (
(role === 'directeur de campus' || role === 'directrice de campus') &&
selectedSociete === 'all' &&
selectedService === 'all'
);
if (shouldAutoSort) {
console.log("🔄 Tri automatique par société → service → nom");
return filteredEmployees.sort((a, b) => {
// 1. Tri par société
const societeCompare = (a.societe || '').localeCompare(b.societe || '');
if (societeCompare !== 0) return societeCompare;
// 2. Tri par service
const serviceCompare = (a.service || '').localeCompare(b.service || '');
if (serviceCompare !== 0) return serviceCompare;
// 3. Tri par nom
return a.name.localeCompare(b.name);
});
}
// ⭐ Sinon, tri simple par nom
return filteredEmployees.sort((a, b) => a.name.localeCompare(b.name));
};
// ⭐ OBTENIR LES CONGÉS À AFFICHER
const getDisplayedLeaves = () => {
const baseFiltered = getBaseFilteredLeaves();
const displayedEmployees = getDisplayedEmployees();
const displayedNames = displayedEmployees.map(emp => emp.name);
return baseFiltered.filter(leave => displayedNames.includes(leave.employeename));
};
// ⭐ USEMEMO POUR ÉVITER LES RECALCULS INUTILES
const allEmployeesData = useMemo(() => {
return getAllEmployees();
}, (role === 'directeur de campus' || role === 'directrice de campus')
? [filters.employees] // Directeur: seulement filters.employees (backend filtre)
: [teamLeaves, filters, selectedCampus, selectedSociete, selectedService, role] // Autres: tous les filtres
);
// ⭐ NETTOYER LA SÉLECTION SI DES EMPLOYÉS NE SONT PLUS DISPONIBLES
useEffect(() => {
const availableNames = allEmployeesData.map(emp => emp.name);
setSelectedEmployees(prev => {
const filtered = prev.filter(name => availableNames.includes(name));
// ⭐ Ne retourner que si vraiment différent pour éviter boucles
if (filtered.length !== prev.length) {
console.log("🧹 Nettoyage employés:", {
avant: prev.length,
après: filtered.length
});
return filtered;
}
return prev;
});
}, [allEmployeesData]);
// ⭐ LOG DE DÉBOGAGE
useEffect(() => {
console.log("👥 Employés disponibles (après filtres):", allEmployeesData.length);
console.log("✅ Employés sélectionnés:", selectedEmployees.length);
console.log("📊 Employés affichés dans le tableau:", getDisplayedEmployees().length);
}, [allEmployeesData, selectedEmployees]);
const toggleEmployee = (employeeName) => {
setSelectedEmployees(prev => {
if (prev.includes(employeeName)) {
return prev.filter(name => name !== employeeName);
} else {
return [...prev, employeeName];
}
});
};
const selectAllEmployees = () => {
const allNames = allEmployeesData.map(emp => emp.name);
setSelectedEmployees(allNames);
};
const deselectAllEmployees = () => {
setSelectedEmployees([]);
};
const parseLocalDate = (dateStr) => {
if (!dateStr) return null;
const [year, month, day] = dateStr.split('-').map(Number);
return new Date(year, month - 1, day);
};
const getLeaveForEmployeeOnDate = (employeeName, date) => {
if (!date) return null;
const displayedLeaves = getDisplayedLeaves();
return displayedLeaves.find(leave => {
if (leave.employeename !== employeeName) return false;
const start = parseLocalDate(leave.startdate);
const end = parseLocalDate(leave.enddate);
return date >= start && date <= end;
});
};
const hasLeave = (date) => {
if (!date) return false;
const displayedLeaves = getDisplayedLeaves();
return displayedLeaves.some(leave => {
const start = parseLocalDate(leave.startdate);
const end = parseLocalDate(leave.enddate);
return date >= start && date <= end;
});
};
const getLeaveColor = (leave) => {
if (!leave) return '';
const type = leave.type?.toLowerCase();
const status = leave.statut?.toLowerCase();
if (status === 'en attente' || status === 'pending' || status === 'en attente de validation') {
return 'bg-orange-400';
}
if (type === 'formation' || type === 'training') {
return 'bg-blue-400';
}
return 'bg-green-400';
};
const handleMouseMove = (e) => {
if (!isMobile) {
setMousePosition({ x: e.clientX, y: e.clientY });
}
};
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;
});
};
useEffect(() => {
const handleClickOutside = () => {
if (contextMenu.show) {
setContextMenu({ show: false, x: 0, y: 0 });
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [contextMenu.show]);
const isPastDate = (date) => {
if (!date) return false;
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
};
const isSelected = (date) => {
if (!date || !selectedDate) return false;
if (selectedEndDate) {
return date >= selectedDate && date <= selectedEndDate;
}
return date.toDateString() === selectedDate.toDateString();
};
const isSaturday = (date) => {
return date.getDay() === 6;
};
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 checkIfRangeIncludesSaturday = (start, end) => {
const current = new Date(start);
while (current <= end) {
if (current.getDay() === 6) return true;
current.setDate(current.getDate() + 1);
}
return false;
};
const handleDateClick = (date) => {
if (!date || isPastDate(date) || isHoliday(date)) return;
const saturday = isSaturday(date);
// ⭐ Si c'est un samedi et qu'on n'a pas encore de sélection en cours
if (saturday && !selectedDate) {
// Permettre de sélectionner un samedi seul
setSelectedDate(date);
setSelectedEndDate(null);
setIsSelectingRange(true);
showToast('📅 Samedi sélectionné - Cliquez sur une autre date ou validez pour une récupération', 'info');
return;
}
// ⭐ Si on clique sur un samedi alors qu'on a déjà une sélection en cours
if (saturday && selectedDate && isSelectingRange) {
// Vérifier que la plage contient bien un samedi
if (date >= selectedDate) {
setSelectedEndDate(date);
setIsSelectingRange(false);
// Ouvrir directement le modal avec type "Récup" pré-sélectionné
setPreselectedDates({
startDate: selectedDate,
endDate: date,
type: 'Récup'
});
setShowNewRequestModal(true);
setSelectedDate(null);
setSelectedEndDate(null);
} else {
showToast('⚠️ La date de fin doit être après la date de début', 'error');
}
return;
}
// ⭐ Logique normale pour les jours de semaine
if (!selectedDate) {
setSelectedDate(date);
setSelectedEndDate(null);
setIsSelectingRange(true);
} else if (isSelectingRange && !selectedEndDate) {
if (date >= selectedDate) {
setSelectedEndDate(date);
setIsSelectingRange(false);
const includesSaturday = checkIfRangeIncludesSaturday(selectedDate, date);
if (includesSaturday) {
// Si la plage inclut un samedi, forcer le type Récup
setPreselectedDates({
startDate: selectedDate,
endDate: date,
type: 'Récup'
});
setShowNewRequestModal(true);
setSelectedDate(null);
setSelectedEndDate(null);
} else {
// Sinon, afficher le menu contextuel
setTimeout(() => {
setContextMenu({
show: true,
x: window.innerWidth / 2,
y: window.innerHeight / 2
});
}, 100);
}
} else {
setSelectedDate(date);
setSelectedEndDate(null);
}
} else {
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) => {
if (!selectedDate) return;
const startDate = selectedDate;
const endDate = selectedEndDate || selectedDate;
const includesSaturday = checkIfRangeIncludesSaturday(startDate, endDate);
if (includesSaturday && type !== 'Récup') {
showToast('⚠️ Les samedis ne peuvent être sélectionnés que pour les récupérations', 'error');
setContextMenu({ show: false, x: 0, y: 0 });
return;
}
setPreselectedDates({
startDate: startDate,
endDate: endDate,
type: type
});
setShowNewRequestModal(true);
setContextMenu({ show: false, x: 0, y: 0 });
setSelectedDate(null);
setSelectedEndDate(null);
setIsSelectingRange(false);
};
const cancelSelection = () => {
setSelectedDate(null);
setSelectedEndDate(null);
setIsSelectingRange(false);
setContextMenu({ show: false, x: 0, y: 0 });
showToast('❌ Sélection annulée', 'info');
};
const getSelectedDays = () => {
if (!selectedDate) return 0;
if (selectedEndDate) {
return calculateWorkingDays(selectedDate, selectedEndDate);
}
return 1;
};
const formatDateToString = (date) => {
if (!date) return null;
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const days = isMobile ? getDaysInMonthMobile() : getDaysInMonth();
const employees = getDisplayedEmployees();
const allEmployees = allEmployeesData;
const totalPTO = detailedCounters
? (detailedCounters.cpN?.solde || 0) + (detailedCounters.cpN1?.solde || 0) + (detailedCounters.rttN?.solde || 0)
: 0;
const canViewAllFilters = ['president', 'rh', 'admin'].includes(role);
const canViewCampusFilters = ['president', 'rh', 'admin', 'directeur de campus','directrice de campus'].includes(role);
const activeFiltersCount = [
employeeFilter !== "all" ? employeeFilter : null,
selectedCampus !== "all" ? selectedCampus : null,
selectedSociete !== "all" ? selectedSociete : null,
selectedService !== "all" ? selectedService : null,
selectedEmployees.length > 0 ? `${selectedEmployees.length} collab` : null,
].filter(Boolean).length;
const clearAllFilters = () => {
setEmployeeFilter("all");
setSelectedCampus("all");
setSelectedSociete("all");
setSelectedService("all");
setSelectedEmployees([]);
};
const renderLeaveCell = (leave) => {
if (!leave) return null;
const color = getLeaveColor(leave);
// 🔹 CORRECTION : Parser correctement les détails
let details;
try {
if (typeof leave.detailsconges === 'string') {
details = JSON.parse(leave.detailsconges);
} else {
details = leave.detailsconges || [];
}
} catch (e) {
console.error('Erreur parsing detailsconges:', e, leave.detailsconges);
details = [];
}
// Vérifier si c'est une demi-journée
const isPeriodePartielle = Array.isArray(details) && details.some(d =>
d.periode === 'Matin' || d.periode === 'Après-midi'
);
if (isPeriodePartielle) {
const hasMatin = details?.some(d => d.periode === 'Matin');
const hasApresMidi = details?.some(d => d.periode === 'Après-midi');
return (
<div className="relative h-6 w-6 mx-auto rounded overflow-hidden">
{hasMatin && <div className={`absolute top-0 left-0 right-0 h-3 ${color}`}></div>}
{hasApresMidi && <div className={`absolute bottom-0 left-0 right-0 h-3 ${color}`}></div>}
{!hasMatin && !hasApresMidi && <div className={`h-full w-full ${color}`}></div>}
</div>
);
}
// Affichage normal pour journée entière
return <div className={`h-6 w-6 mx-auto ${color} rounded cursor-pointer`}></div>;
};
return (
<div className="flex h-screen bg-gray-50" onMouseMove={handleMouseMove}>
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
<div className="lg:hidden fixed top-0 left-0 right-0 bg-white shadow-sm z-40 px-4 py-3">
<div className="flex items-center justify-between">
<button onClick={() => setSidebarOpen(true)} className="text-gray-600">
<Menu className="w-6 h-6" />
</button>
<h1 className="text-lg font-semibold text-gray-800">Calendrier</h1>
<button onClick={() => setShowFilters(!showFilters)} className="text-gray-600">
<Filter className="w-6 h-6" />
</button>
</div>
</div>
<div className="flex-1 overflow-auto pt-16 lg:pt-0">
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6">
<div className="hidden lg:flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<h1 className="text-xl font-semibold text-gray-800">Calendrier des congés</h1>
{sseConnected && (
<span className="flex items-center gap-1 text-xs text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full animate-pulse"></div>
Connecté
</span>
)}
</div>
<div className="flex items-center gap-4">
<div className="bg-gray-100 px-4 py-1.5 rounded">
<span className="text-sm text-gray-600">PTO: </span>
<span className="text-sm font-semibold">{totalPTO.toFixed(1)}j</span>
</div>
</div>
</div>
<div className="lg:hidden mb-4 flex justify-center">
<div className="bg-blue-50 border border-blue-200 px-4 py-2 rounded-lg">
<span className="text-sm text-gray-600">PTO: </span>
<span className="text-sm font-bold text-blue-600">{totalPTO.toFixed(1)}j</span>
</div>
</div>
{isSelectingRange && (
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-3">
<CalendarIcon className="w-5 h-5 text-blue-600" />
<div>
<p className="text-sm font-medium text-blue-900">
Mode sélection activé
</p>
<p className="text-xs text-blue-700">
{selectedDate ?
`Début: ${selectedDate.toLocaleDateString('fr-FR')} - Cliquez sur la date de fin` :
'Cliquez sur une date de début'
}
</p>
</div>
</div>
<button
onClick={cancelSelection}
className="px-3 py-1.5 bg-white border border-blue-300 text-blue-700 rounded text-sm hover:bg-blue-50"
>
Annuler
</button>
</div>
)}
{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>
</div>
</div>
)}
<div className="mb-4 bg-white rounded-lg border border-gray-200 p-4">
<div className="flex items-center justify-between mb-4">
<button
onClick={() => setShowFilters(!showFilters)}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
<Filter className="w-4 h-4" />
<span className="text-sm font-medium">
Filtres {activeFiltersCount > 0 && `(${activeFiltersCount})`}
</span>
</button>
<div className="flex items-center gap-3">
{activeFiltersCount > 0 && (
<button
onClick={clearAllFilters}
className="text-sm text-blue-600 hover:text-blue-700"
>
Réinitialiser les filtres
</button>
)}
<button
onClick={refreshAllData}
disabled={isRefreshing}
className="p-2 text-gray-600 hover:bg-gray-100 rounded border border-gray-300"
>
<RefreshCw className={`w-5 h-5 mx-auto ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{showFilters && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pt-4 border-t border-gray-200">
{/* FILTRES POUR PRESIDENT/ADMIN/RH : Campus + Societe + Service */}
{canViewAllFilters && (
<>
{filters.campus && filters.campus.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Campus
</label>
<select
value={selectedCampus}
onChange={(e) => {
setSelectedCampus(e.target.value);
// Réinitialiser les filtres dépendants
setSelectedSociete('all');
setSelectedService('all');
setSelectedEmployees([]);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
>
<option value="all">Tous les campus</option>
{filters.campus?.map((campus, index) => (
<option key={index} value={campus}>
{campus}
</option>
))}
</select>
</div>
)}
{filters.societes && filters.societes.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Société
</label>
<select
value={selectedSociete}
onChange={(e) => {
setSelectedSociete(e.target.value);
setSelectedService('all');
setSelectedEmployees([]);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
>
<option value="all">Toutes les sociétés</option>
{filters.societes?.map((societe, index) => (
<option key={index} value={societe}>
{societe}
</option>
))}
</select>
</div>
)}
{filters.services && filters.services.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Service
</label>
<select
value={selectedService}
onChange={(e) => {
setSelectedService(e.target.value);
setSelectedEmployees([]);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
>
<option value="all">Tous les services</option>
{filters.services?.map((service, index) => (
<option key={index} value={service}>
{service}
</option>
))}
</select>
</div>
)}
</>
)}
{/* FILTRES POUR DIRECTEUR DE CAMPUS - Société et Service uniquement */}
{(role === 'directeur de campus' || role === 'directrice de campus') && filters.societes && filters.societes.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Société
</label>
<select
value={selectedSociete}
onChange={(e) => {
console.log("🔄 Changement société:", e.target.value);
setSelectedSociete(e.target.value);
setSelectedEmployees([]);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
>
<option value="all">Toutes les sociétés</option>
{filters.societes?.map((soc, index) => (
<option key={index} value={soc}>
{soc}
</option>
))}
</select>
</div>
)}
{(role === 'directeur de campus' || role === 'directrice de campus') && filters.services && filters.services.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Service
</label>
<select
value={selectedService}
onChange={(e) => {
console.log("🔄 Changement service:", e.target.value);
setSelectedService(e.target.value);
setSelectedEmployees([]);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
>
<option value="all">Tous les services</option>
{filters.services?.map((srv, index) => (
<option key={index} value={srv}>
{srv}
</option>
))}
</select>
</div>
)}
{/* Section Collaborateurs - pour tous les rôles */}
<div className="col-span-full">
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700">
Collaborateurs à afficher
{allEmployeesData.length > 0 && (
<span className="ml-2 text-xs text-gray-500">
({allEmployeesData.length} disponible{allEmployeesData.length > 1 ? 's' : ''})
</span>
)}
</label>
<button
onClick={() => setShowEmployeeList(!showEmployeeList)}
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
{showEmployeeList ? 'Masquer' : 'Sélectionner'}
</button>
</div>
{selectedEmployees.length > 0 && (
<div className="mb-2 flex flex-wrap gap-2">
{selectedEmployees.map((empName, idx) => (
<span
key={idx}
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs"
>
{empName}
<button
onClick={() => toggleEmployee(empName)}
className="hover:text-blue-900"
>
<X className="w-3 h-3" />
</button>
</span>
))}
</div>
)}
{showEmployeeList && (
<div className="border border-gray-300 rounded-lg p-3 bg-gray-50 max-h-60 overflow-y-auto">
{allEmployeesData.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-4">
Aucun collaborateur trouvé avec les filtres actuels
</p>
) : (
<>
<div className="flex gap-2 mb-3">
<button
onClick={selectAllEmployees}
className="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
>
Tout sélectionner
</button>
<button
onClick={deselectAllEmployees}
className="px-3 py-1 bg-gray-300 text-gray-700 text-xs rounded hover:bg-gray-400"
>
Tout désélectionner
</button>
</div>
<div className="space-y-2">
{allEmployeesData.map((employee, idx) => (
<label
key={idx}
className="flex items-center gap-2 p-2 hover:bg-white rounded cursor-pointer"
>
<input
type="checkbox"
checked={selectedEmployees.includes(employee.name)}
onChange={() => toggleEmployee(employee.name)}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<div className="flex-1">
<div className="text-sm text-gray-700">{employee.name}</div>
<div className="text-xs text-gray-500">
{employee.service}
{canViewCampusFilters && (
<>
{employee.societe && `${employee.societe}`}
{canViewAllFilters && employee.campus && `${employee.campus}`}
</>
)}
</div>
</div>
</label>
))}
</div>
</>
)}
</div>
)}
</div>
</div>
)}
</div>
{!isMobile && (
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<div className="inline-block min-w-full">
<div className="flex items-center justify-center py-4 px-4 border-b bg-gray-50">
<button
onClick={() => navigateMonth('prev')}
className="p-2 hover:bg-gray-200 rounded"
>
<ChevronLeft className="w-5 h-5 text-blue-600" />
</button>
<div className="mx-8 text-center min-w-[200px]">
<span className="text-lg font-semibold">
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
</span>
</div>
<button
onClick={() => navigateMonth('next')}
className="p-2 hover:bg-gray-200 rounded"
>
<ChevronRight className="w-5 h-5 text-blue-600" />
</button>
</div>
<table className="w-full border-collapse">
<thead>
<tr className="border-b bg-gray-50">
<th className="sticky left-0 bg-gray-50 z-20 border-r px-4 py-3 text-left w-48">
<span className="text-sm font-semibold">
Collaborateurs ({employees.length}
{selectedEmployees.length > 0 && ` / ${allEmployees.length}`})
</span>
</th>
{days.map((date, index) => {
const dayName = dayNames[date.getDay() - 1];
const inRange = isSelected(date);
const past = isPastDate(date);
const holiday = isHoliday(date);
const saturday = isSaturday(date);
return (
<th
key={index}
className={`border-r px-2 py-2 text-center min-w-[40px] ${holiday ? 'bg-gray-600 text-white' :
saturday ? 'bg-purple-100' :
inRange ? 'bg-blue-100' :
past ? 'bg-gray-100' : 'bg-gray-50'
}`}
title={holiday ? getHolidayName(date) : saturday ? 'Samedi - Récupération uniquement' : ''}
>
<div className={`text-xs font-normal ${holiday ? 'text-white' :
saturday ? 'text-purple-700' :
past ? 'text-gray-400' : 'text-gray-600'
}`}>{dayName}</div>
<div className={`text-sm font-medium ${holiday ? 'text-white' :
saturday ? 'text-purple-700' :
inRange ? 'text-blue-700' :
past ? 'text-gray-400' : 'text-gray-900'
}`}>{date.getDate()}</div>
</th>
);
})}
</tr>
</thead>
<tbody>
{employees.length === 0 ? (
<tr>
<td colSpan={days.length + 1} className="text-center py-12 text-gray-500">
{allEmployees.length === 0 ? 'Aucun collaborateur trouvé' : 'Aucun collaborateur sélectionné - Cliquez sur "Sélectionner" pour choisir'}
</td>
</tr>
) : (
employees.map((employee, empIndex) => (
<tr key={empIndex} className="border-b hover:bg-gray-50">
<td className="sticky left-0 bg-white z-10 border-r px-4 py-3">
<div className="font-medium text-sm">
{employee.name}
</div>
<div className="text-xs text-gray-500 mt-0.5">
{employee.service}
{canViewCampusFilters && (
<>
{employee.societe && `${employee.societe}`}
{(role === "president" || role === "rh" || role === "admin") && employee.campus && `${employee.campus}`}
{(role === "directeur de campus" || role === 'directrice de campus') && employee.societe === "Ensup Solution & Support" && employee.campus !== "N/A" && `${employee.campus}`}
</>
)}
</div>
</td>
{days.map((date, dayIndex) => {
const leave = getLeaveForEmployeeOnDate(employee.name, date);
const inRange = isSelected(date);
const past = isPastDate(date);
const holiday = isHoliday(date);
const saturday = isSaturday(date);
const isToday = date.toDateString() === new Date().toDateString();
return (
<td
key={dayIndex}
className={`border-r px-1 py-3 text-center ${isToday ? 'bg-cyan-50' : ''
} ${inRange ? 'bg-blue-100' : ''} ${past ? 'bg-gray-50 opacity-60' : ''
} ${holiday ? 'bg-gray-600' : ''} ${saturday ? 'bg-purple-50' : ''
}`}
onClick={() => {
// ⭐ Permettre le clic sur les samedis aussi (retirer le check !saturday)
if (!leave && employee.name === `${user.prenom} ${user.nom}` && !past && !holiday) {
handleDateClick(date);
}
}}
onContextMenu={(e) => handleContextMenu(e, date)}
onMouseEnter={() => !isMobile && leave && setHoveredLeave({ employee, leave, date })}
onMouseLeave={() => setHoveredLeave(null)}
>
{/* ⭐ REMPLACER CETTE SECTION */}
{holiday ? (
<div className="h-6 w-6 mx-auto bg-gray-700 rounded" title={getHolidayName(date)}></div>
) : leave ? (
renderLeaveCell(leave)
) : employee.name === `${user.prenom} ${user.nom}` && !past && !holiday ? (
<div className={`h-6 w-6 mx-auto rounded cursor-pointer transition-all ${inRange
? 'bg-blue-500 scale-110'
: saturday
? 'bg-purple-200 hover:bg-purple-300'
: 'bg-gray-100 hover:bg-blue-200 hover:scale-105'
}`}></div>
) : (
<div className="h-6 w-6 mx-auto bg-gray-100 rounded"></div>
)}
</td>
);
})}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
<div className="flex items-center gap-3 lg:gap-6 p-4 border-t border-gray-100 flex-wrap text-xs lg:text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-cyan-50 border border-cyan-200 rounded"></div>
<span className="text-gray-600">Aujourd'hui</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-400 rounded"></div>
<span className="text-gray-600">Validé</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-orange-400 rounded"></div>
<span className="text-gray-600">En attente</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-400 rounded"></div>
<span className="text-gray-600">Formation</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-purple-200 rounded"></div>
<span className="text-gray-600">Samedi (Récup)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gray-700 rounded"></div>
<span className="text-gray-600">Jours fériés</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-500 rounded"></div>
<span className="text-gray-600">Sélection</span>
</div>
</div>
</div>
)}
{isMobile && (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-gray-900">
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
</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>
<div className="grid grid-cols-5 gap-2 mb-2">
{dayNamesMobile.map(day => (
<div key={day} className="p-2 text-center text-xs font-medium text-gray-500">
{day}
</div>
))}
</div>
<div className="grid grid-cols-5 gap-1">
{days.map((date, index) => (
<div
key={index}
className={`
min-h-[60px] p-1 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-gray-700 text-white cursor-not-allowed border border-gray-800' :
isSelected(date) ? 'bg-blue-600 text-white' :
hasLeave(date) ? 'bg-green-100' : 'hover:bg-gray-50'
}
`}
onClick={() => handleDateClick(date)}
onContextMenu={(e) => handleContextMenu(e, date)}
title={isHoliday(date) ? getHolidayName(date) : ''}
>
{date && (
<div className="flex flex-col items-center justify-between h-full">
<span className="text-xs">{date.getDate()}</span>
{isHoliday(date) && getHolidayName(date) && (
<div className="text-[10px] text-white font-medium mt-1 text-center leading-tight break-words">
{getHolidayName(date)}
</div>
)}
{hasLeave(date) && (
<div className="mt-1 flex flex-col items-center space-y-0.5 text-[10px] text-center leading-tight">
{getDisplayedLeaves()
.filter(leave => {
const start = parseLocalDate(leave.startdate);
const end = parseLocalDate(leave.enddate);
return date >= start && date <= end;
})
.map((leave, i) => (
<div
key={i}
className="flex flex-col items-center gap-1"
>
<span className={`text-[10px] leading-tight break-words text-center ${leave.type === 'Formation' ? 'text-blue-800' :
leave.statut === 'En attente' ? 'text-orange-800' :
'text-green-800'
}`}>
{leave.employeename}
</span>
</div>
))}
</div>
)}
</div>
)}
</div>
))}
</div>
<div className="flex items-center gap-4 mt-6 pt-6 border-t border-gray-100 flex-wrap text-xs">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-100 rounded"></div>
<span className="text-gray-600">Validé</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-orange-100 rounded"></div>
<span className="text-gray-600">En attente</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-100 rounded"></div>
<span className="text-gray-600">Formation</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-gray-700 rounded"></div>
<span className="text-gray-600">Férié</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-600 rounded"></div>
<span className="text-gray-600">Sélection</span>
</div>
</div>
</div>
)}
{contextMenu.show && (
<div
className="fixed bg-white rounded-lg shadow-xl border border-gray-200 py-2 z-50 min-w-[200px]"
style={{ left: contextMenu.x, top: contextMenu.y }}
>
<div className="px-3 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>
<button
onClick={() => handleTypeSelection('CP')}
className="w-full text-left px-3 py-2 hover:bg-green-50 rounded text-sm flex items-center gap-2"
>
<div className="w-3 h-3 bg-green-400 rounded"></div>
Congé payé (CP)
</button>
<button
onClick={() => handleTypeSelection('RTT')}
className="w-full text-left px-3 py-2 hover:bg-blue-50 rounded text-sm flex items-center gap-2"
>
<div className="w-3 h-3 bg-blue-300 rounded"></div>
RTT
</button>
<button
onClick={() => handleTypeSelection('Formation')}
className="w-full text-left px-3 py-2 hover:bg-purple-50 rounded text-sm flex items-center gap-2"
>
<div className="w-3 h-3 bg-purple-400 rounded"></div>
Formation
</button>
<button
onClick={() => handleTypeSelection('Récup')}
className="w-full text-left px-3 py-2 hover:bg-purple-50 rounded text-sm flex items-center gap-2"
>
<div className="w-3 h-3 bg-purple-300 rounded"></div>
Récupération (samedi)
</button>
<button
onClick={() => handleTypeSelection('Autre')}
className="w-full text-left px-3 py-2 hover:bg-gray-50 rounded text-sm flex items-center gap-2"
>
<div className="w-3 h-3 bg-gray-400 rounded"></div>
Autre
</button>
<div className="border-t border-gray-200 mt-2 pt-2">
<button
onClick={cancelSelection}
className="w-full text-center px-3 py-2 text-red-600 hover:bg-red-50 rounded text-sm"
>
Annuler
</button>
</div>
</div>
)}
{hoveredLeave && !isMobile && (
<div
className="fixed bg-white rounded-lg shadow-xl border p-4 z-50 min-w-[250px]"
style={{
left: Math.min(mousePosition.x + 10, window.innerWidth - 280),
top: mousePosition.y + 10,
}}
>
<div className="flex items-start gap-3">
{/* Emoji selon le type de journée */}
<div className="text-2xl">
{(() => {
const details = typeof hoveredLeave.leave.details_conges === 'string'
? JSON.parse(hoveredLeave.leave.details_conges)
: hoveredLeave.leave.details_conges;
const isPeriodePartielle = details?.some(
d => d.periode === 'Matin' || d.periode === 'Après-midi'
);
return isPeriodePartielle ? '🌗' : '📅';
})()}
</div>
<div className="flex-1">
<div className="font-semibold text-sm mb-1">
{hoveredLeave.employee.name}
</div>
{/* Afficher les types avec leurs périodes */}
<div className="space-y-1 mb-2">
{(() => {
const details = typeof hoveredLeave.leave.details_conges === 'string'
? JSON.parse(hoveredLeave.leave.details_conges)
: hoveredLeave.leave.details_conges;
if (details && Array.isArray(details)) {
return details.map((detail, idx) => (
<div key={idx} className="text-xs">
<span className="font-medium">{detail.type}</span>
{detail.periode !== 'Journée entière' && (
<span className="text-gray-600 ml-1">
({detail.periode === 'Matin' ? '🌅 Matin' : ' Après-midi'})
</span>
)}
<span className="text-gray-500 ml-1">
- {detail.jours}j
</span>
</div>
));
} else {
// Fallback pour ancien format
return (
<div className="text-xs">
<span className="font-medium">{hoveredLeave.leave.type || 'Congé'}</span>
</div>
);
}
})()}
</div>
<div className={`text-xs px-2 py-0.5 rounded inline-block ${hoveredLeave.leave.statut === 'Validée' ? 'bg-green-100 text-green-700' :
hoveredLeave.leave.statut === 'En attente' ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
{hoveredLeave.leave.statut}
</div>
<div className="text-xs text-gray-500 mt-2">
Du {parseLocalDate(hoveredLeave.leave.startdate)?.toLocaleDateString('fr-FR')}
au {parseLocalDate(hoveredLeave.leave.enddate)?.toLocaleDateString('fr-FR')}
</div>
{hoveredLeave.leave.nombrejoursouvres && (
<div className="text-xs text-gray-500">
Durée totale: <span className="font-medium">
{hoveredLeave.leave.nombrejoursouvres} jour(s)
</span>
</div>
)}
</div>
</div>
</div>
)}
{hoveredLeave && isMobile && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-end" onClick={() => setHoveredLeave(null)}>
<div className="bg-white rounded-t-2xl w-full p-6 animate-slide-up" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="font-semibold text-lg">{hoveredLeave.employee.name}</h3>
<p className="text-sm text-gray-600">{hoveredLeave.leave.type || 'Congé'}</p>
</div>
<button onClick={() => setHoveredLeave(null)} className="text-gray-400">
<X className="w-6 h-6" />
</button>
</div>
<button
onClick={() => setHoveredLeave(null)}
className="w-full mt-6 py-3 bg-blue-600 text-white rounded-lg font-medium"
>
Fermer
</button>
</div>
</div>
)}
</div>
</div>
<div className="fixed top-20 right-4 z-50 space-y-2">
{toasts.map(toast => (
<div
key={toast.id}
className={`px-4 py-3 rounded-lg shadow-lg animate-slideInRight ${toast.type === 'success' ? 'bg-green-500 text-white' :
toast.type === 'error' ? 'bg-red-500 text-white' :
'bg-blue-500 text-white'
}`}
>
{toast.message}
</div>
))}
</div>
{showNewRequestModal && detailedCounters && (
<NewLeaveRequestModal
onClose={() => {
setShowNewRequestModal(false);
setPreselectedDates(null);
}}
availableLeaveCounters={{
availableCP_N: detailedCounters.cpN?.solde || 0,
totalCP_N: detailedCounters.cpN?.acquis || 0,
availableCP_N1: detailedCounters.cpN1?.solde || 0,
availableRTT_N: detailedCounters.rttN?.solde || 0,
totalRTT_N: detailedCounters.rttN?.acquis || 0,
availableRTT_N1: 0,
availableABS: 0,
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
availableRTT: detailedCounters.rttN?.solde || 0
}}
accessToken={graphToken}
userId={userId}
userRole={user.role}
userEmail={user.email}
userName={`${user.prenom} ${user.nom}`}
onRequestSubmitted={refreshAllData}
preselectedStartDate={preselectedDates ? formatDateToString(preselectedDates.startDate) : null}
preselectedEndDate={preselectedDates ? formatDateToString(preselectedDates.endDate) : null}
preselectedType={preselectedDates?.type}
/>
)}
<style>{`
@keyframes slide-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.animate-slide-up {
animation: slide-up 0.3s ease-out;
}
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animate-slideInRight {
animation: slideInRight 0.3s ease-out;
}
`}</style>
</div>
);
};
export default Calendar;