Revert "V1_Sans_Congé_Anticipéfemini collaboratrice"

This reverts commit 0eb4dbb99b.
This commit is contained in:
2025-11-17 10:34:50 +01:00
parent 0eb4dbb99b
commit 7f15e380e3
41 changed files with 2740 additions and 30354 deletions

View File

@@ -1,515 +0,0 @@
import React, { useState, useEffect } from 'react';
import { X, Calendar, AlertCircle, Upload } from 'lucide-react';
const EditLeaveRequestModal = ({
onClose,
request,
availableLeaveCounters,
accessToken,
userId,
userEmail,
userRole,
userName,
onRequestUpdated
}) => {
const [leaveType, setLeaveType] = useState(request.typeId || '');
const [startDate, setStartDate] = useState(request.startDate || '');
const [endDate, setEndDate] = useState(request.endDate || '');
const [reason, setReason] = useState(request.reason || '');
const [businessDays, setBusinessDays] = useState(request.days || 0);
const [saturdayCount, setSaturdayCount] = useState(0);
const [medicalDocuments, setMedicalDocuments] = useState([]);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitMessage, setSubmitMessage] = useState({ type: '', text: '' });
// ⭐ Types de congés disponibles selon le rôle
const getLeaveTypes = () => {
const baseTypes = [
{ id: 1, name: 'Congé payé', key: 'CP', counter: availableLeaveCounters.availableCP },
];
// Ajouter RTT sauf pour les apprentis
if (userRole !== 'Apprenti') {
baseTypes.push({
id: 2,
name: 'RTT',
key: 'RTT',
counter: availableLeaveCounters.availableRTT
});
}
// Ajouter les types sans compteur
baseTypes.push(
{ id: 3, name: 'Arrêt maladie', key: 'ABS', counter: null },
{ id: 5, name: 'Récupération (samedi)', key: 'Récup', counter: null }
);
// Ajouter Formation pour les apprentis
if (userRole === 'Apprenti') {
baseTypes.push({ id: 4, name: 'Formation', key: 'Formation', counter: null });
}
return baseTypes;
};
const leaveTypes = getLeaveTypes();
// ⭐ Calcul des jours ouvrés ET des samedis
useEffect(() => {
if (startDate && endDate) {
const result = calculateBusinessDaysAndSaturdays(startDate, endDate);
setBusinessDays(result.workingDays);
setSaturdayCount(result.saturdays);
}
}, [startDate, endDate]);
const calculateBusinessDaysAndSaturdays = (start, end) => {
const startD = new Date(start);
const endD = new Date(end);
let workingDays = 0;
let saturdays = 0;
const current = new Date(startD);
while (current <= endD) {
const dayOfWeek = current.getDay();
if (dayOfWeek === 6) {
saturdays++;
} else if (dayOfWeek !== 0) { // Pas dimanche
workingDays++;
}
current.setDate(current.getDate() + 1);
}
return { workingDays, saturdays };
};
const validateForm = () => {
const newErrors = {};
if (!leaveType) {
newErrors.leaveType = 'Veuillez sélectionner un type de congé';
}
if (!startDate) {
newErrors.startDate = 'La date de début est requise';
}
if (!endDate) {
newErrors.endDate = 'La date de fin est requise';
}
if (startDate && endDate && new Date(startDate) > new Date(endDate)) {
newErrors.endDate = 'La date de fin doit être après la date de début';
}
// ⭐ Validation spécifique pour Récupération
const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType));
if (selectedType?.key === 'Récup') {
if (saturdayCount === 0) {
newErrors.days = 'Une récupération nécessite au moins un samedi dans la période sélectionnée';
}
}
// ⭐ Validation spécifique pour Arrêt maladie
if (selectedType?.key === 'ABS' && medicalDocuments.length === 0) {
newErrors.medical = 'Un justificatif médical est obligatoire pour un arrêt maladie';
}
// Vérification du solde disponible (CP et RTT uniquement)
if (selectedType && selectedType.counter !== null && businessDays > selectedType.counter) {
newErrors.days = `Solde insuffisant. Disponible : ${selectedType.counter} jour(s)`;
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleFileUpload = (e) => {
const files = Array.from(e.target.files);
const validFiles = [];
const maxSize = 5 * 1024 * 1024; // 5MB
for (const file of files) {
const validTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
if (!validTypes.includes(file.type)) {
setSubmitMessage({
type: 'error',
text: `Le fichier "${file.name}" n'est pas un format valide.`
});
continue;
}
if (file.size > maxSize) {
setSubmitMessage({
type: 'error',
text: `Le fichier "${file.name}" est trop volumineux (max 5MB).`
});
continue;
}
validFiles.push(file);
}
setMedicalDocuments(prev => [...prev, ...validFiles]);
e.target.value = '';
};
const removeDocument = (index) => {
setMedicalDocuments(prev => prev.filter((_, i) => i !== index));
};
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setSubmitMessage({ type: '', text: '' });
try {
const formDataToSend = new FormData();
formDataToSend.append('requestId', request.id);
formDataToSend.append('leaveType', parseInt(leaveType));
formDataToSend.append('startDate', startDate);
formDataToSend.append('endDate', endDate);
formDataToSend.append('reason', reason);
formDataToSend.append('userId', userId);
formDataToSend.append('userEmail', userEmail);
formDataToSend.append('userName', userName);
formDataToSend.append('accessToken', accessToken);
// ⭐ Calcul des jours selon le type
const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType));
const daysToSend = selectedType?.key === 'Récup' ? saturdayCount : businessDays;
formDataToSend.append('businessDays', daysToSend);
// ⭐ Documents médicaux
medicalDocuments.forEach((file) => {
formDataToSend.append('medicalDocuments', file);
});
const response = await fetch('http://localhost:3000/updateRequest', {
method: 'POST',
body: formDataToSend
});
const data = await response.json();
if (data.success) {
setSubmitMessage({
type: 'success',
text: '✅ Demande modifiée avec succès ! Le manager a été informé par email.'
});
setTimeout(() => {
onRequestUpdated();
onClose();
}, 2000);
} else {
setSubmitMessage({
type: 'error',
text: `${data.message || 'Erreur lors de la modification'}`
});
}
} catch (error) {
console.error('Erreur:', error);
setSubmitMessage({
type: 'error',
text: '❌ Une erreur est survenue. Veuillez réessayer.'
});
} finally {
setIsSubmitting(false);
}
};
const getMinDate = () => {
const today = new Date();
return today.toISOString().split('T')[0];
};
const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType));
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Overlay */}
<div
className="absolute inset-0 bg-black bg-opacity-50"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center rounded-t-2xl">
<h2 className="text-2xl font-bold text-gray-900">Modifier la demande</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Body */}
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Message de statut */}
{submitMessage.text && (
<div className={`p-4 rounded-lg ${submitMessage.type === 'success'
? 'bg-green-50 text-green-800 border border-green-200'
: 'bg-red-50 text-red-800 border border-red-200'
}`}>
{submitMessage.text}
</div>
)}
{/* Info - Demande originale */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="font-semibold text-blue-900 mb-2 flex items-center gap-2">
<AlertCircle className="w-5 h-5" />
Demande actuelle
</h3>
<div className="text-sm text-blue-800 space-y-1">
<p><strong>Type :</strong> {request.type}</p>
<p><strong>Dates :</strong> {request.dateDisplay}</p>
<p><strong>Jours :</strong> {request.days}</p>
</div>
</div>
{/* Type de congé */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Type de congé *
</label>
<select
value={leaveType}
onChange={(e) => setLeaveType(e.target.value)}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.leaveType ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Sélectionnez un type</option>
{leaveTypes.map(type => (
<option key={type.id} value={type.id}>
{type.name}
{type.counter !== null && ` (${type.counter.toFixed(1)} jours disponibles)`}
</option>
))}
</select>
{errors.leaveType && (
<p className="mt-1 text-sm text-red-600">{errors.leaveType}</p>
)}
</div>
{/* Dates */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Date de début *
</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
min={getMinDate()}
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.startDate ? 'border-red-500' : 'border-gray-300'
}`}
/>
</div>
{errors.startDate && (
<p className="mt-1 text-sm text-red-600">{errors.startDate}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Date de fin *
</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
min={startDate || getMinDate()}
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.endDate ? 'border-red-500' : 'border-gray-300'
}`}
/>
</div>
{errors.endDate && (
<p className="mt-1 text-sm text-red-600">{errors.endDate}</p>
)}
</div>
</div>
{/* ⭐ Résumé de la période */}
{startDate && endDate && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-sm font-medium text-blue-900 mb-1">Résumé de la période :</p>
<div className="text-xs text-blue-700 space-y-1">
<p> <strong>{businessDays} jour(s) ouvré(s)</strong> (lundi-vendredi)</p>
{saturdayCount > 0 && (
<>
<p className="text-purple-700"> {saturdayCount} samedi(s) détecté(s)</p>
{selectedType?.key !== 'Récup' && (
<p className="text-orange-700 font-medium">
Les samedis seront ignorés (sélectionnez "Récupération" pour les inclure)
</p>
)}
{selectedType?.key === 'Récup' && (
<p className="text-green-700 font-medium">
Récupération : {saturdayCount} samedi(s) seront comptabilisés
</p>
)}
</>
)}
</div>
</div>
)}
{/* Nombre de jours */}
{(businessDays > 0 || saturdayCount > 0) && (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<p className="text-sm text-gray-700">
<strong>Nombre de jours {selectedType?.key === 'Récup' ? '(samedis)' : 'ouvrés'} :</strong>{' '}
{selectedType?.key === 'Récup' ? saturdayCount : businessDays}
</p>
{errors.days && (
<p className="mt-1 text-sm text-red-600">{errors.days}</p>
)}
</div>
)}
{/* ⭐ Upload documents médicaux pour Arrêt maladie */}
{selectedType?.key === 'ABS' && (
<div>
<label className="block text-sm font-medium text-gray-900 mb-3">
Justificatif médical <span className="text-red-600">*</span>
</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors">
<div className="w-12 h-12 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<Upload className="w-6 h-6 text-gray-400" />
</div>
<p className="text-gray-700 text-sm mb-2">
Glissez vos documents ici ou cliquez pour sélectionner
</p>
<p className="text-gray-500 text-xs mb-4">
Formats acceptés : PDF, JPG, PNG (max 5MB par fichier)
</p>
<input
type="file"
multiple
accept=".pdf,.jpg,.jpeg,.png"
onChange={handleFileUpload}
className="hidden"
id="medical-documents"
/>
<label
htmlFor="medical-documents"
className="inline-block px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors text-sm font-medium text-gray-700"
>
Sélectionner des fichiers
</label>
</div>
{medicalDocuments.length > 0 && (
<div className="mt-4 space-y-2">
<p className="text-sm font-medium text-gray-900 mb-2">
Fichiers sélectionnés ({medicalDocuments.length}) :
</p>
{medicalDocuments.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-blue-100 rounded flex items-center justify-center flex-shrink-0">
{file.type === 'application/pdf' ? (
<svg className="w-4 h-4 text-red-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clipRule="evenodd" />
</svg>
) : (
<svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900 truncate">{file.name}</p>
<p className="text-xs text-gray-500">{formatFileSize(file.size)}</p>
</div>
</div>
<button
type="button"
onClick={() => removeDocument(index)}
className="text-gray-400 hover:text-red-600 ml-2 flex-shrink-0"
>
<X className="w-5 h-5" />
</button>
</div>
))}
</div>
)}
{errors.medical && (
<p className="mt-2 text-sm text-red-600">{errors.medical}</p>
)}
</div>
)}
{/* Motif */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Motif (optionnel)
</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={3}
placeholder="Précisez le motif de votre demande..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
/>
</div>
{/* Info importante */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<p className="text-sm text-yellow-800">
<strong>Important :</strong> Votre manager sera automatiquement informé par email de cette modification.
</p>
</div>
{/* Actions */}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium transition-colors"
>
Annuler
</button>
<button
type="submit"
disabled={isSubmitting}
className={`flex-1 px-6 py-3 bg-blue-600 text-white rounded-lg font-medium transition-colors ${isSubmitting
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-blue-700'
}`}
>
{isSubmitting ? 'Modification...' : 'Modifier la demande'}
</button>
</div>
</form>
</div>
</div>
);
};
export default EditLeaveRequestModal;

View File

@@ -1 +0,0 @@

View File

@@ -1,133 +0,0 @@
import React, { useState, useEffect } from 'react';
import { FileText, Download, Eye, Loader } from 'lucide-react';
const MedicalDocuments = ({ demandeId }) => {
const [documents, setDocuments] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchDocuments = async () => {
if (!demandeId) {
setLoading(false);
return;
}
try {
setLoading(true);
const response = await fetch(`http://localhost:3000/medical-documents/${demandeId}`);
const data = await response.json();
if (data.success) {
setDocuments(data.documents || []);
} else {
setError(data.message);
}
} catch (err) {
console.error('Erreur récupération documents:', err);
setError('Impossible de charger les documents');
} finally {
setLoading(false);
}
};
fetchDocuments();
}, [demandeId]);
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const getFileIcon = (type) => {
if (type === 'application/pdf') {
return (
<svg className="w-5 h-5 text-red-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clipRule="evenodd" />
</svg>
);
}
return (
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
);
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
if (loading) {
return (
<div className="flex items-center justify-center py-4">
<Loader className="w-5 h-5 animate-spin text-gray-400" />
<span className="ml-2 text-sm text-gray-500">Chargement...</span>
</div>
);
}
if (error) {
return (
<div className="text-sm text-red-600 py-2">
Erreur : {error}
</div>
);
}
if (documents.length === 0) {
return null; // Ne rien afficher s'il n'y a pas de documents
}
return (
<div>
<p className="text-gray-500 mb-2">
Justificatifs médicaux ({documents.length})
</p>
<div className="space-y-2">
{documents.map((doc) => (
<div
key={doc.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200 hover:bg-gray-100 transition-colors"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex-shrink-0">
{getFileIcon(doc.type)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900 truncate">
{doc.nom}
</p>
<div className="flex items-center gap-2 text-xs text-gray-500">
<span>{formatFileSize(doc.taille)}</span>
<span></span>
<span>{formatDate(doc.date)}</span>
</div>
</div>
</div>
<a
href={`http://localhost:3000${doc.downloadUrl}`}
download
className="flex-shrink-0 p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Télécharger"
>
<Download className="w-4 h-4" />
</a>
</div>
))}
</div>
</div>
);
};
export default MedicalDocuments;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { LogOut, Calendar, Home, FileText, X, Users, Clock } from 'lucide-react';
import { LogOut, Calendar, Home, FileText, Building2, X, Users } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
const Sidebar = ({ isOpen, onToggle }) => {
@@ -15,40 +15,13 @@ const Sidebar = ({ isOpen, onToggle }) => {
return 'bg-red-100 text-red-800';
case 'Validateur':
return 'bg-green-100 text-green-800';
case 'Validatrice':
return 'bg-green-100 text-green-800';
case 'Directeur de campus':
return 'bg-purple-100 text-purple-800';
case 'Directrice de campus':
return 'bg-purple-100 text-purple-800';
case 'President':
return 'bg-indigo-100 text-indigo-800';
case 'Collaborateur':
return 'bg-cyan-600 text-white';
case 'Collaboratrice':
return 'bg-cyan-600 text-white';
default:
return 'bg-gray-100 text-gray-800';
}
};
// Vérifier si l'utilisateur peut voir le compte-rendu d'activités
const canViewCompteRendu = () => {
const allowedRoles = [
'Validateur',
'Validatrice',
'Directeur de campus',
'Directrice de campus',
'President',
'Admin',
'RH'
];
return allowedRoles.includes(user?.role);
};
// Vérifier si l'utilisateur est en forfait jour
const isForfaitJour = user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfaitjour';
return (
<>
{isOpen && (
@@ -73,12 +46,14 @@ const Sidebar = ({ isOpen, onToggle }) => {
{/* Logo */}
<div className="p-6 border-b border-gray-100">
<div className="flex flex-col items-center gap-2">
<img
src="/assets/GA.svg"
alt="GTA Logo"
className="h-24 w-auto"
/>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-cyan-600 rounded-lg flex items-center justify-center">
<Building2 className="w-6 h-6 text-white" />
</div>
<div>
<h2 className="text-xl font-bold text-gray-900">GTA</h2>
<p className="text-sm text-gray-500">Gestion de congés</p>
</div>
</div>
</div>
@@ -116,8 +91,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
to="/dashboard"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/dashboard")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Home className="w-5 h-5" />
@@ -129,7 +104,7 @@ const Sidebar = ({ isOpen, onToggle }) => {
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/demandes")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<FileText className="w-5 h-5" />
@@ -141,57 +116,35 @@ const Sidebar = ({ isOpen, onToggle }) => {
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/calendrier")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Calendar className="w-5 h-5" />
<span className="font-medium">Calendrier</span>
</Link>
{/* Lien Compte-Rendu d'Activités - Visible pour validateurs et directeurs */}
{(canViewCompteRendu() || isForfaitJour) && (
<Link
to="/compte-rendu-activites"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/compte-rendu-activites")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Clock className="w-5 h-5" />
<span className="font-medium">Compte-Rendu</span>
</Link>
)}
{/* Rubrique dynamique Collaborateur / Validateur */}
{(user?.role === "Collaborateur" ||
user?.role === "Collaboratrice" ||
user?.role === "Apprenti" ||
user?.role === "Validateur" ||
user?.role === "Validatrice" ||
user?.role === "Manager" ||
user?.role === "RH" ||
user?.role === "Directeur de campus" ||
user?.role === "Directrice de campus" ||
user?.role === "President" ||
user?.role === "Admin") && (() => {
const targetPath = (user?.role === "Collaborateur" || user?.role === "Apprenti" || user?.role === "Collaboratrice")
? "/collaborateur"
: "/manager";
return (
<Link
to={targetPath}
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(targetPath)
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
user?.role === "Admin") && (
<Link
to={user?.role === "Collaborateur" ? "/collaborateur" : "/manager"}
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(user?.role === "Collaborateur" ? "/collaborateur" : "/manager")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Users className="w-5 h-5" />
<span className="font-medium">Mon équipe</span>
</Link>
);
})()}
}`}
>
<Users className="w-5 h-5" />
<span className="font-medium">
{user?.role === "Collaborateur"
? "Mon équipe"
: "Mon équipe"}
</span>
</Link>
)}
</nav>
{/* Bouton déconnexion */}