Ajout du champ "Autre" dans le formulaire de demande de congé. Mise en place de l’export du calendrier aux formats ICS et CSV.
670 lines
30 KiB
JavaScript
670 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/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>
|
|
|
|
{/* Ton rendu de calendrier ici */}
|
|
</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; |