Affichage des noms des collaborateurs en congé dans le calendrier.

Ajout du champ "Autre" dans le formulaire de demande de congé.
Mise en place de l’export du calendrier aux formats ICS et CSV.
This commit is contained in:
2025-08-06 16:49:31 +02:00
parent 238a7a31b0
commit fbcd80fb6f
3 changed files with 230 additions and 15 deletions

View File

@@ -24,6 +24,8 @@ const NewLeaveRequestModal = ({
const [error, setError] = useState('');
const [calculatedDays, setCalculatedDays] = useState(0);
const [isPreselected, setIsPreselected] = useState(false);
const [isOtherChecked, setIsOtherChecked] = useState(false);
const [otherLeaveType, setOtherLeaveType] = useState('');
// Vérifier si des valeurs sont pré-sélectionnées
useEffect(() => {
@@ -130,8 +132,36 @@ const NewLeaveRequestModal = ({
return { ...prev, types: newTypes };
});
setError('');
// Désélectionner le type "Autre" si un autre type est sélectionné
if (type !== 'Autres') {
setIsOtherChecked(false);
setOtherLeaveType('');
}
};
const handleOtherCheckboxChange = (e) => {
const isChecked = e.target.checked;
setIsOtherChecked(isChecked);
// Si la case est cochée, désélectionner les autres types
if (isChecked) {
setFormData(prev => ({ ...prev, types: ['Autres'] }));
} else {
// Si elle est décochée, vider le type et les documents
setOtherLeaveType('');
setFormData(prev => ({ ...prev, types: [] }));
}
setError('');
};
const handleOtherTypeChange = (e) => {
setOtherLeaveType(e.target.value);
setFormData(prev => ({ ...prev, types: [e.target.value] }));
};
const handleDistributionChange = (type, days) => {
setTypeDistribution(prev => ({
...prev,
@@ -183,6 +213,12 @@ const NewLeaveRequestModal = ({
setError('Impossible de faire une demande pour une date passée');
return false;
}
// Vérification de la sélection d'un type "autre"
if (isOtherChecked && !otherLeaveType) {
setError('Veuillez sélectionner un type de congé dans la liste "Autre"');
return false;
}
// Vérification de la distribution des jours
if (formData.types.length > 1) {
@@ -197,6 +233,12 @@ const NewLeaveRequestModal = ({
for (const type of formData.types) {
const requiredDays = formData.types.length > 1 ? (typeDistribution[type] || 0) : calculatedDays;
// Les types "autres" n'ont pas de solde à vérifier
const nonSoldeTypes = ['Récup', 'Congés sans solde', 'Congés pour évènement familial', 'Congé maternité', 'Congé paternité', 'Congé parental', 'Congé parental à temps partiel', 'Autres'];
if (nonSoldeTypes.includes(type)) {
continue;
}
if (type === 'CP' && requiredDays > availableLeaveCounters.availableCP) {
setError(`Solde CP insuffisant. Vous avez ${availableLeaveCounters.availableCP} jours disponibles`);
return false;
@@ -223,6 +265,8 @@ const NewLeaveRequestModal = ({
// Créer une demande pour chaque type de congé
const requests = formData.types.map(type => {
const days = formData.types.length > 1 ? (typeDistribution[type] || 0) : calculatedDays;
// Utiliser le type sélectionné dans la liste déroulante si "Autre" est coché
const finalType = type === 'Autres' ? otherLeaveType : type;
return {
EmployeeId: userId,
TypeConge: type,
@@ -325,6 +369,7 @@ const NewLeaveRequestModal = ({
)}
{/* Type de congé */}
<div>
<label className="block text-sm lg:text-base font-medium text-gray-700 mb-2">
Types de congé * {formData.types.length > 0 && `(${formData.types.length} sélectionné${formData.types.length > 1 ? 's' : ''})`}
@@ -341,7 +386,7 @@ const NewLeaveRequestModal = ({
type="checkbox"
checked={formData.types.includes(type.key)}
onChange={() => handleTypeToggle(type.key)}
disabled={isPreselected && preselectedType && preselectedType !== type.key}
disabled={isPreselected && preselectedType && preselectedType !== type.key || isOtherChecked}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className={`px-2 lg:px-3 py-1 rounded-full text-xs lg:text-sm font-medium border ${getTypeColor(type.key)}`}>
@@ -350,9 +395,47 @@ const NewLeaveRequestModal = ({
</label>
</div>
))}
{/* Nouvelle case à cocher pour "Autre" */}
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 cursor-pointer flex-1">
<input
type="checkbox"
checked={isOtherChecked}
onChange={handleOtherCheckboxChange}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className={`px-2 lg:px-3 py-1 rounded-full text-xs lg:text-sm font-medium border ${getTypeColor('Autres')}`}>
Autre
</span>
</label>
</div>
{/* Liste déroulante "Autre" conditionnelle */}
{isOtherChecked && (
<div className="mt-2 pl-6">
<label className="block text-sm font-medium text-gray-700 mb-1">
Sélectionnez le type de congé
</label>
<select
value={otherLeaveType}
onChange={handleOtherTypeChange}
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"
required
>
<option value="">-- Choisir un type --</option>
<option value="Récup">Récup</option>
<option value="Congés sans solde">Congés sans solde</option>
<option value="Congés pour évènement familial">Congés pour évènement familial</option>
<option value="Congé maternité">Congé maternité</option>
<option value="Congé paternité">Congé paternité</option>
<option value="Congé parental">Congé parental</option>
<option value="Congé parental à temps partiel">Congé parental à temps partiel</option>
</select>
</div>
)}
</div>
</div>
{/* Distribution des jours si plusieurs types sélectionnés */}
{formData.types.length > 1 && calculatedDays > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">

View File

@@ -57,9 +57,9 @@ const Calendar = () => {
// 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, 5, 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' }
{ date: new Date(year, 12, 25), name: 'Noël' }
];
} finally {
setIsLoadingHolidays(false);
@@ -298,6 +298,89 @@ const Calendar = () => {
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)} />
@@ -410,8 +493,8 @@ const Calendar = () => {
<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 ? '' :
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' :
@@ -419,28 +502,58 @@ const Calendar = () => {
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' : ''}
`}
${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">
<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) && (
<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>
<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="w-1.5 h-1.5 lg:w-2 lg:h-2 bg-green-500 rounded-full mt-1"></div>
<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>
</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">
@@ -518,6 +631,19 @@ const Calendar = () => {
</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 && (

View File

@@ -52,7 +52,12 @@ const Requests = () => {
// Filtre par type
if (typeFilter !== 'all') {
filtered = filtered.filter(request => request.type === typeFilter);
if (typeFilter === 'Autres') {
const otherTypes = ['Récup', 'Congés sans solde', 'Congés pour évènement familial', 'Congé maternité', 'Congé paternité', 'Congé parental', 'Congé parental à temps partiel'];
filtered = filtered.filter(request => otherTypes.includes(request.type));
} else {
filtered = filtered.filter(request => request.type === typeFilter);
}
}
setFilteredRequests(filtered);
@@ -84,7 +89,7 @@ const Requests = () => {
};
const fetchAllRequests = async () => {
console.log('🔍 Requests - Début fetchAllRequests pour user:', user?.id);
console.log('Requests - Début fetchAllRequests pour user:', user?.id);
try {
const url = `http://localhost/GTA/project/public/getRequests.php?user_id=${user.id}`;
@@ -381,6 +386,7 @@ const Requests = () => {
<option value="Congés payés">Congés payés</option>
<option value="RTT">RTT</option>
<option value="Congé maladie">Congé maladie</option>
<option value="Autres">Autres types de congés</option> {/* Nouvelle option */}
</select>
</div>
</div>