673 lines
30 KiB
JavaScript
673 lines
30 KiB
JavaScript
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, 5, 1), name: 'Fête du Travail' },
|
|
{ date: new Date(year, 6, 14), name: 'Fête Nationale' },
|
|
{ date: new Date(year, 12, 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/php/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);
|
|
|
|
// Export CSV
|
|
const exportTeamLeavesToGoogleCSV = (teamLeaves) => {
|
|
const csvRows = [
|
|
['Subject', 'Start Date', 'Start Time', 'End Date', 'End Time', 'All Day Event', 'Description', 'Location', 'Private'],
|
|
...teamLeaves.map(leave => {
|
|
// Conversion date YYYY-MM-DD -> MM/DD/YYYY
|
|
const formatDate = dateStr => {
|
|
const [year, month, day] = dateStr.split('-');
|
|
return `${month}/${day}/${year}`;
|
|
};
|
|
|
|
return [
|
|
`Congé - ${leave.employee_name}`, // Subject
|
|
formatDate(leave.start_date), // Start Date
|
|
'', // Start Time (vide car congé toute la journée)
|
|
formatDate(leave.end_date), // End Date
|
|
'', // End Time
|
|
'TRUE', // All Day Event
|
|
`Type de congé: ${leave.type}`, // Description
|
|
'', // Location vide
|
|
'TRUE' // Privé
|
|
];
|
|
})
|
|
];
|
|
|
|
const csvContent = csvRows.map(row => row.join(',')).join('\n');
|
|
|
|
// Ajouter BOM UTF-8 pour Excel/Google Sheets
|
|
const BOM = '\uFEFF';
|
|
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const link = document.createElement('a');
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', 'conges_equipe_googleagenda.csv');
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
};
|
|
|
|
|
|
|
|
// Export ICS
|
|
const exportTeamLeavesToICS = (teamLeaves) => {
|
|
const ICS_HEADER =
|
|
`BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//TonEntreprise//CongesEquipe//FR
|
|
CALSCALE:GREGORIAN`;
|
|
|
|
const ICS_FOOTER = 'END:VCALENDAR';
|
|
|
|
const events = teamLeaves.map(leave => {
|
|
// Format date YYYYMMDD
|
|
const startDate = leave.start_date.replace(/-/g, '');
|
|
const endDate = leave.end_date.replace(/-/g, '');
|
|
|
|
return `BEGIN:VEVENT
|
|
SUMMARY:Congé - ${leave.employee_name}
|
|
DTSTART;VALUE=DATE:${startDate}
|
|
DTEND;VALUE=DATE:${endDate}
|
|
DESCRIPTION:Type de congé: ${leave.type}
|
|
STATUS:CONFIRMED
|
|
END:VEVENT`;
|
|
}).join('\n');
|
|
|
|
const icsContent = `${ICS_HEADER}\n${events}\n${ICS_FOOTER}`;
|
|
|
|
// Blob pour téléchargement
|
|
const blob = new Blob([icsContent], { type: 'text/calendar;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = 'conges_equipe.ics';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
};
|
|
|
|
|
|
|
|
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-between h-full">
|
|
{/* Date number */}
|
|
<span className="text-xs lg:text-sm">{date.getDate()}</span>
|
|
|
|
|
|
{isHoliday(date) && getHolidayName(date) && (
|
|
<div className="text-[10px] text-red-700 font-medium mt-1 text-center leading-tight break-words">
|
|
{getHolidayName(date)}
|
|
</div>
|
|
)}
|
|
|
|
|
|
{/* Leave info: names of employees with leave */}
|
|
{hasLeave(date) && (
|
|
<div className="mt-1 flex flex-col items-center space-y-0.5 text-[10px] lg:text-xs text-green-700 text-center leading-tight">
|
|
{teamLeaves
|
|
.filter(leave => {
|
|
const start = new Date(leave.start_date);
|
|
const end = new Date(leave.end_date);
|
|
return date >= start && date <= end;
|
|
})
|
|
.map((leave, i) => (
|
|
<div
|
|
key={i}
|
|
title={`${leave.employee_name} - ${leave.type}`}
|
|
className="flex items-center gap-1"
|
|
>
|
|
{/* Optional colored dot for leave type */}
|
|
<span
|
|
className="inline-block w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: leave.color || '#3B82F6' }}
|
|
></span>
|
|
<span className="text-[10px] lg:text-xs text-green-800 leading-tight break-words text-center">
|
|
{leave.employee_name}
|
|
</span>
|
|
|
|
</div>
|
|
))}
|
|
</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>
|
|
)}
|
|
{/* button csv */}
|
|
<div className="p-4">
|
|
<div className="flex justify-end gap-2 mb-4">
|
|
<button onClick={() => exportTeamLeavesToGoogleCSV(teamLeaves)} className="bg-blue-600 text-white px-3 py-2 rounded hover:bg-blue-700">
|
|
Export CSV
|
|
</button>
|
|
<button onClick={() => exportTeamLeavesToICS(teamLeaves)} className="bg-green-600 text-white px-3 py-2 rounded hover:bg-green-700">
|
|
Export ICS
|
|
</button>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
{/* Modal nouvelle demande */}
|
|
{showNewRequestModal && (
|
|
<NewLeaveRequestModal
|
|
onClose={() => {
|
|
setShowNewRequestModal(false);
|
|
setPreselectedType(null);
|
|
}}
|
|
availableLeaveCounters={leaveCounters}
|
|
userId={user?.id}
|
|
|
|
onRequestSubmitted={() => {
|
|
resetSelection();
|
|
}}
|
|
userEmail={user.email}
|
|
userName={`${user.prenom} ${user.nom}`}
|
|
preselectedStartDate={selectedDate}
|
|
preselectedEndDate={selectedEndDate}
|
|
preselectedType={preselectedType}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Calendar; |