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:
@@ -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">
|
||||
|
||||
@@ -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)} />
|
||||
@@ -426,15 +509,45 @@ const Calendar = () => {
|
||||
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>
|
||||
)}
|
||||
@@ -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 && (
|
||||
|
||||
@@ -52,8 +52,13 @@ const Requests = () => {
|
||||
|
||||
// Filtre par type
|
||||
if (typeFilter !== 'all') {
|
||||
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);
|
||||
setCurrentPage(1); // Reset à la première page lors du filtrage
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user