V1_Fonctionnel_GTAV1_GTA

This commit is contained in:
2025-11-28 16:55:45 +01:00
parent f22979a44a
commit 6d244f5323
55 changed files with 7567 additions and 5819 deletions

View File

@@ -178,31 +178,45 @@ const EditLeaveRequestModal = ({
try {
const formDataToSend = new FormData();
formDataToSend.append('requestId', request.id);
formDataToSend.append('leaveType', parseInt(leaveType));
// ⭐ Ajouter tous les champs texte AVANT les fichiers
formDataToSend.append('requestId', request.id.toString());
formDataToSend.append('leaveType', leaveType.toString());
formDataToSend.append('startDate', startDate);
formDataToSend.append('endDate', endDate);
formDataToSend.append('reason', reason);
formDataToSend.append('userId', userId);
formDataToSend.append('reason', reason || '');
formDataToSend.append('userId', userId.toString());
formDataToSend.append('userEmail', userEmail);
formDataToSend.append('userName', userName);
formDataToSend.append('accessToken', accessToken);
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);
formDataToSend.append('businessDays', daysToSend.toString());
// ⭐ Documents médicaux
medicalDocuments.forEach((file) => {
formDataToSend.append('medicalDocuments', file);
});
// ⭐ Documents médicaux EN DERNIER
if (medicalDocuments.length > 0) {
medicalDocuments.forEach((file) => {
formDataToSend.append('medicalDocuments', file);
});
}
const response = await fetch('http://localhost:3000/updateRequest', {
// ⭐ DEBUG : Vérifier le contenu
console.log('📤 FormData à envoyer:');
for (let pair of formDataToSend.entries()) {
console.log(pair[0], ':', pair[1]);
}
const response = await fetch('/api/updateRequest', {
method: 'POST',
// ⭐ NE PAS mettre de Content-Type, le navigateur le fera automatiquement avec boundary
body: formDataToSend
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
@@ -222,7 +236,7 @@ const EditLeaveRequestModal = ({
});
}
} catch (error) {
console.error('Erreur:', error);
console.error('Erreur:', error);
setSubmitMessage({
type: 'error',
text: '❌ Une erreur est survenue. Veuillez réessayer.'

View File

@@ -0,0 +1,728 @@
import React, { useState, useEffect } from 'react';
import Joyride, { STATUS } from 'react-joyride';
import { useLocation } from 'react-router-dom';
const GlobalTutorial = ({ userId, userRole }) => {
const [runTour, setRunTour] = useState(false);
const [dontShowAgain, setDontShowAgain] = useState(false);
const [availableSteps, setAvailableSteps] = useState([]);
const location = useLocation();
const isEmployee = userRole === "Collaborateur" || userRole === "Apprenti";
const canViewAllFilters = ['president', 'rh', 'admin', 'directeur de campus', 'directrice de campus'].includes(userRole?.toLowerCase());
// 🎯 NOUVELLE FONCTION : Vérifier si un élément existe dans le DOM
const elementExists = (selector) => {
return document.querySelector(selector) !== null;
};
// 🎯 NOUVELLE FONCTION : Filtrer les étapes selon les éléments disponibles
const filterAvailableSteps = (steps) => {
return steps.filter(step => {
// Les étapes centrées (body) sont toujours affichées
if (step.target === 'body') return true;
// Pour les autres, vérifier si l'élément existe
const element = document.querySelector(step.target);
if (!element) {
console.log(`⚠️ Élément non trouvé, étape ignorée: ${step.target}`);
return false;
}
// Vérifier si l'élément est visible
const isVisible = element.offsetParent !== null;
if (!isVisible) {
console.log(`⚠️ Élément caché, étape ignorée: ${step.target}`);
return false;
}
return true;
});
};
// 🎯 Déclencher le tutoriel avec vérification
useEffect(() => {
if (userId) {
let tutorialKey = '';
if (location.pathname === '/dashboard') {
tutorialKey = 'dashboard';
} else if (location.pathname === '/manager') {
tutorialKey = 'manager';
} else if (location.pathname === '/calendar') {
tutorialKey = 'calendar';
}
if (tutorialKey) {
const hasSeenTutorial = localStorage.getItem(`${tutorialKey}-tutorial-completed-${userId}`);
if (!hasSeenTutorial) {
// ⭐ NOUVEAU : Attendre que le DOM soit chargé
setTimeout(() => {
const allSteps = getTourSteps();
const available = filterAvailableSteps(allSteps);
console.log(`📊 Étapes totales: ${allSteps.length}, disponibles: ${available.length}`);
if (available.length > 2) { // Au moins 3 étapes (intro + 1 élément + conclusion)
setAvailableSteps(available);
setRunTour(true);
} else {
console.log('⚠️ Pas assez d\'éléments pour le tutoriel, annulation');
}
}, 2000);
}
}
}
}, [userId, location.pathname]);
// 🎯 Obtenir les étapes selon la page actuelle
const getTourSteps = () => {
// ==================== DASHBOARD ====================
if (location.pathname === '/dashboard') {
return [
{
target: 'body',
content: (
<div>
<h2 className="text-xl font-bold mb-2">👋 Bienvenue sur votre application GTA !</h2>
<p>Découvrez toutes les fonctionnalités en quelques étapes. Ce tutoriel ne s'affichera qu'une seule fois.</p>
</div>
),
placement: 'center',
disableBeacon: true,
},
{
target: '[data-tour="dashboard"]',
content: '🏠 Accédez à votre tableau de bord pour voir vos soldes de congés.',
placement: 'right',
},
{
target: '[data-tour="demandes"]',
content: '📋 Consultez et gérez toutes vos demandes de congés ici.',
placement: 'right',
},
{
target: '[data-tour="calendrier"]',
content: '📅 Visualisez vos congés et ceux de votre équipe dans le calendrier.',
placement: 'right',
},
{
target: '[data-tour="mon-equipe"]',
content: '👥 Consultez votre équipe et leurs absences.',
placement: 'right',
},
{
target: '[data-tour="nouvelle-demande"]',
content: ' Cliquez ici pour créer une nouvelle demande de congé, RTT ou récupération.',
placement: 'left',
},
{
target: '[data-tour="notifications"]',
content: '🔔 Consultez ici vos notifications (validations, refus, modifications de vos demandes).',
placement: 'bottom',
},
{
target: '[data-tour="refresh"]',
content: '🔄 Rafraîchissez manuellement vos données. Mais pas d\'inquiétude : elles se mettent à jour automatiquement en temps réel !',
placement: 'bottom',
},
{
target: '[data-tour="demandes-recentes"]',
content: '📄 Consultez rapidement vos 5 dernières demandes et leur statut. Cliquez sur "Voir toutes les demandes" pour accéder à la page complète.',
placement: 'top',
},
{
target: '[data-tour="conges-service"]',
content: '👥 Visualisez les congés de votre service pour le mois en cours. Pratique pour planifier vos absences !',
placement: 'top',
},
{
target: 'body',
content: (
<div>
<h2 className="text-lg font-bold mb-2">📊 Vos compteurs de congés</h2>
<p>Découvrez maintenant vos différents soldes de congés disponibles.</p>
</div>
),
placement: 'center',
},
{
target: '[data-tour="cp-n-1"]',
content: '📅 Vos congés payés de l\'année précédente. ⚠️ Attention : ils doivent être soldés avant le 31 décembre de l\'année en cours !',
placement: 'top',
},
{
target: '[data-tour="cp-n"]',
content: '📈 Vos congés payés de l\'année en cours, en cours d\'acquisition. Ils se cumulent au fil des mois travaillés.',
placement: 'top',
},
{
target: '[data-tour="rtt"]',
content: '⏰ Vos RTT disponibles pour l\'année en cours. Ils sont acquis progressivement et à consommer avant le 31/12.',
placement: 'top',
},
{
target: '[data-tour="recup"]',
content: '🔄 Vos jours de récupération accumulés suite à des heures supplémentaires.',
placement: 'top',
},
{
target: 'body',
content: (
<div>
<h2 className="text-xl font-bold mb-2">🎉 Vous êtes prêt !</h2>
<p className="mb-3">Vous pouvez maintenant utiliser l'application en toute autonomie.</p>
<div className="bg-cyan-50 border border-cyan-200 rounded-lg p-3 mt-3">
<p className="text-sm text-cyan-900">
💡 <strong>Besoin d'aide ?</strong> Cliquez sur le bouton <strong>"Aide"</strong> 🆘 en bas à droite pour relancer ce tutoriel à tout moment.
</p>
</div>
</div>
),
placement: 'center',
},
];
}
// ==================== MANAGER ====================
if (location.pathname === '/manager') {
const baseSteps = [
{
target: 'body',
content: (
<div>
<h2 className="text-xl font-bold mb-2">👥 Bienvenue dans la gestion d'équipe !</h2>
<p>Découvrez comment gérer {isEmployee ? 'votre équipe' : 'les demandes de congés de votre équipe'}.</p>
</div>
),
placement: 'center',
disableBeacon: true,
}
];
if (!isEmployee) {
// Pour les managers/validateurs
return [
...baseSteps,
{
target: '[data-tour="demandes-attente"]',
content: ' Consultez ici toutes les demandes en attente de validation. Vous pouvez les approuver ou les refuser directement.',
placement: 'right',
},
{
target: '[data-tour="approuver-btn"]',
content: ' Cliquez sur "Approuver" pour valider une demande. Vous pourrez ajouter un commentaire optionnel.',
placement: 'top',
},
{
target: '[data-tour="refuser-btn"]',
content: ' Cliquez sur "Refuser" pour rejeter une demande. Un commentaire expliquant le motif sera obligatoire.',
placement: 'top',
},
{
target: '[data-tour="mon-equipe"]',
content: '👥 Consultez la liste complète de votre équipe. Cliquez sur un membre pour voir le détail de ses demandes.',
placement: 'left',
},
{
target: '[data-tour="historique-demandes"]',
content: '📋 L\'historique complet de toutes les demandes de votre équipe avec leur statut (validée, refusée, en attente).',
placement: 'top',
},
{
target: '[data-tour="document-joint"]',
content: '📎 Si un document est joint à une demande (certificat médical par exemple), vous pouvez le consulter ici.',
placement: 'left',
},
{
target: 'body',
content: (
<div>
<h2 className="text-xl font-bold mb-2">🎉 Vous êtes prêt à gérer votre équipe !</h2>
<p className="mb-3">Vous savez maintenant valider les demandes et suivre les absences de vos collaborateurs.</p>
<div className="bg-cyan-50 border border-cyan-200 rounded-lg p-3 mt-3">
<p className="text-sm text-cyan-900">
💡 <strong>Astuce :</strong> Les données se mettent à jour automatiquement en temps réel. Vous recevrez des notifications pour chaque nouvelle demande.
</p>
</div>
</div>
),
placement: 'center',
}
];
} else {
// Pour les collaborateurs/apprentis
return [
...baseSteps,
{
target: '[data-tour="mon-equipe"]',
content: '👥 Consultez ici la liste de votre équipe. Vous pouvez voir les membres de votre service.',
placement: 'left',
},
{
target: '[data-tour="membre-equipe"]',
content: '👤 Cliquez sur un membre pour voir le détail de ses informations et absences.',
placement: 'left',
},
{
target: 'body',
content: (
<div>
<h2 className="text-xl font-bold mb-2"> C'est tout pour cette section !</h2>
<p className="mb-3">Vous pouvez maintenant consulter votre équipe facilement.</p>
<div className="bg-cyan-50 border border-cyan-200 rounded-lg p-3 mt-3">
<p className="text-sm text-cyan-900">
💡 <strong>Besoin d'aide ?</strong> N'hésitez pas à contacter votre manager pour toute question.
</p>
</div>
</div>
),
placement: 'center',
}
];
}
}
// ==================== CALENDAR ====================
if (location.pathname === '/calendar') {
const baseSteps = [
{
target: 'body',
content: (
<div>
<h2 className="text-xl font-bold mb-2">📅 Bienvenue dans le calendrier !</h2>
<p>Découvrez comment visualiser et gérer les congés {canViewAllFilters ? 'de toute l\'entreprise' : 'de votre équipe'}.</p>
</div>
),
placement: 'center',
disableBeacon: true,
},
{
target: '[data-tour="pto-counter"]',
content: '📊 Votre solde PTO (Paid Time Off) total : somme de vos CP N-1, CP N et RTT disponibles.',
placement: 'bottom',
},
{
target: '[data-tour="navigation-mois"]',
content: '◀️▶️ Naviguez entre les mois pour consulter les congés passés et à venir.',
placement: 'bottom',
}
];
// Étapes pour les filtres selon le rôle
if (canViewAllFilters) {
baseSteps.push(
{
target: '[data-tour="filtres-btn"]',
content: '🔍 Accédez aux filtres pour affiner votre vue : société, campus, service, collaborateurs...',
placement: 'left',
},
{
target: '[data-tour="filtre-societe"]',
content: '🏢 Filtrez par société pour voir uniquement les congés d\'une entité spécifique.',
placement: 'bottom',
},
{
target: '[data-tour="filtre-campus"]',
content: '🏫 Filtrez par campus pour visualiser les absences par site géographique.',
placement: 'bottom',
},
{
target: '[data-tour="filtre-service"]',
content: '👔 Filtrez par service pour voir les congés d\'un département spécifique.',
placement: 'bottom',
}
);
}
// Étapes communes pour tous
baseSteps.push(
{
target: '[data-tour="selection-collaborateurs"]',
content: '👥 Sélectionnez les collaborateurs que vous souhaitez afficher dans le calendrier. Pratique pour se concentrer sur certaines personnes !',
placement: 'top',
},
{
target: '[data-tour="refresh-btn"]',
content: '🔄 Rafraîchissez manuellement les données. Mais rassurez-vous : elles se mettent à jour automatiquement en temps réel via SSE !',
placement: 'left',
},
{
target: 'body',
content: (
<div>
<h2 className="text-lg font-bold mb-2">📅 Sélectionner des dates</h2>
<p>Vous pouvez sélectionner des dates directement dans le calendrier pour créer une demande de congé rapidement.</p>
</div>
),
placement: 'center',
},
{
target: '[data-tour="calendar-grid"]',
content: '🖱️ Cliquez sur une date de début, puis sur une date de fin pour sélectionner une période. Un menu contextuel apparaîtra pour choisir le type de congé.',
placement: 'top',
},
{
target: '[data-tour="legende"]',
content: '🎨 La légende vous aide à identifier les différents types de congés : validés (vert), en attente (orange), formation (bleu), etc.',
placement: 'top',
},
{
target: 'body',
content: (
<div>
<h2 className="text-xl font-bold mb-2">🎉 Vous maîtrisez le calendrier !</h2>
<p className="mb-3">Vous savez maintenant visualiser les congés, filtrer par équipe et créer rapidement des demandes.</p>
<div className="bg-cyan-50 border border-cyan-200 rounded-lg p-3 mt-3">
<p className="text-sm text-cyan-900">
💡 <strong>Astuce :</strong> Survolez une case de congé pour voir tous les détails (employé, type, période, statut). Sur mobile, appuyez sur la case !
</p>
</div>
</div>
),
placement: 'center',
}
);
return baseSteps;
}
return [];
};
// 🎯 Obtenir la clé localStorage selon la page
const getTutorialKey = () => {
if (location.pathname === '/dashboard') return 'dashboard';
if (location.pathname === '/manager') return 'manager';
if (location.pathname === '/calendar') return 'calendar';
return '';
};
// 🎯 Gérer la fin du tutoriel
const handleJoyrideCallback = (data) => {
const { status } = data;
const finishedStatuses = [STATUS.FINISHED, STATUS.SKIPPED];
if (finishedStatuses.includes(status)) {
setRunTour(false);
setDontShowAgain(false);
}
};
// Si on n'a pas d'étapes disponibles, ne rien afficher
if (availableSteps.length === 0) return null;
return (
<Joyride
steps={availableSteps}
run={runTour}
continuous
showProgress={true}
showSkipButton={false}
scrollToFirstStep
scrollOffset={100}
callback={handleJoyrideCallback}
styles={{
options: {
primaryColor: '#0891b2',
zIndex: 10000,
},
}}
floaterProps={{
disableAnimation: true,
}}
locale={{
back: 'Retour',
close: 'Fermer',
last: 'Terminer',
next: 'Suivant',
skip: 'Passer'
}}
tooltipComponent={({
continuous,
index,
step,
backProps,
primaryProps,
skipProps,
closeProps,
tooltipProps,
size,
isLastStep
}) => {
const [showConfirmModal, setShowConfirmModal] = React.useState(false);
const tutorialKey = getTutorialKey();
const handleFinish = () => {
if (dontShowAgain) {
localStorage.setItem(`${tutorialKey}-tutorial-completed-${userId}`, 'true');
}
setRunTour(false);
setDontShowAgain(false);
};
const handleSkip = () => {
if (dontShowAgain) {
setShowConfirmModal(true);
} else {
setRunTour(false);
setDontShowAgain(false);
}
};
const confirmSkip = () => {
localStorage.setItem(`${tutorialKey}-tutorial-completed-${userId}`, 'true');
setShowConfirmModal(false);
setRunTour(false);
setDontShowAgain(false);
};
const cancelSkip = () => {
setShowConfirmModal(false);
setDontShowAgain(false);
};
return (
<>
{/* Modal de confirmation */}
{showConfirmModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10001
}}
onClick={(e) => {
if (e.target === e.currentTarget) {
cancelSkip();
}
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '16px',
padding: '24px',
maxWidth: '400px',
width: '90%',
boxShadow: '0 20px 50px rgba(0,0,0,0.3)'
}}>
<div style={{
fontSize: '48px',
marginBottom: '16px',
textAlign: 'center'
}}>
</div>
<h3 style={{
fontSize: '18px',
fontWeight: 'bold',
marginBottom: '12px',
color: '#111827',
textAlign: 'center'
}}>
Ne plus afficher le tutoriel ?
</h3>
<p style={{
fontSize: '14px',
color: '#6b7280',
marginBottom: '24px',
textAlign: 'center',
lineHeight: '1.5'
}}>
Êtes-vous sûr de vouloir désactiver définitivement ce tutoriel ?
{tutorialKey === 'dashboard' && ' Vous pourrez le réactiver plus tard en cliquant sur le bouton "Aide".'}
</p>
<div style={{
display: 'flex',
gap: '12px',
justifyContent: 'center'
}}>
<button
onClick={cancelSkip}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: '1px solid #d1d5db',
backgroundColor: 'white',
color: '#374151',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
transition: 'all 0.2s'
}}
>
Annuler
</button>
<button
onClick={confirmSkip}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#ef4444',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
transition: 'all 0.2s'
}}
>
Oui, ne plus afficher
</button>
</div>
</div>
</div>
)}
{/* Tooltip principal */}
<div {...tooltipProps} style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '20px',
maxWidth: '400px',
boxShadow: '0 10px 25px rgba(0,0,0,0.15)',
fontSize: '14px'
}}>
<div style={{ marginBottom: '15px', color: '#374151' }}>
{step.content}
</div>
{/* Case à cocher "Ne plus afficher" */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginTop: '12px',
marginBottom: '12px',
padding: '10px',
backgroundColor: '#f9fafb',
borderRadius: '8px',
border: '1px solid #e5e7eb'
}}>
<input
type="checkbox"
id={`dont-show-again-${index}`}
checked={dontShowAgain}
onChange={(e) => setDontShowAgain(e.target.checked)}
style={{
width: '18px',
height: '18px',
cursor: 'pointer',
accentColor: '#0891b2'
}}
/>
<label
htmlFor={`dont-show-again-${index}`}
style={{
fontSize: '13px',
color: '#374151',
cursor: 'pointer',
userSelect: 'none',
fontWeight: '500'
}}
>
Ne plus afficher ce tutoriel
</label>
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: '12px',
borderTop: '1px solid #e5e7eb'
}}>
<span style={{ fontSize: '13px', color: '#6b7280', fontWeight: '500' }}>
Étape {index + 1} sur {size}
</span>
<div style={{ display: 'flex', gap: '8px' }}>
{index > 0 && (
<button
{...backProps}
style={{
padding: '8px 14px',
borderRadius: '8px',
border: '1px solid #d1d5db',
backgroundColor: 'white',
color: '#6b7280',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '500',
transition: 'all 0.2s'
}}>
Retour
</button>
)}
{!isLastStep && (
<button
{...primaryProps}
style={{
padding: '8px 18px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#0891b2',
color: 'white',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '500',
transition: 'all 0.2s'
}}
>
Suivant
</button>
)}
{isLastStep && (
<button
onClick={handleFinish}
style={{
padding: '8px 18px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#0891b2',
color: 'white',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '500',
transition: 'all 0.2s'
}}
>
Terminer
</button>
)}
<button
onClick={handleSkip}
style={{
padding: '8px 14px',
borderRadius: '8px',
border: '1px solid #d1d5db',
backgroundColor: 'white',
color: '#6b7280',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '500',
transition: 'all 0.2s'
}}
>
Passer
</button>
</div>
</div>
</div>
</>
);
}}
/>
);
};
export default GlobalTutorial;

View File

@@ -1 +0,0 @@

View File

@@ -15,7 +15,7 @@ const MedicalDocuments = ({ demandeId }) => {
try {
setLoading(true);
const response = await fetch(`http://localhost:3000/medical-documents/${demandeId}`);
const response = await fetch(`/api/medical-documents/${demandeId}`);
const data = await response.json();
if (data.success) {
@@ -116,7 +116,7 @@ const MedicalDocuments = ({ demandeId }) => {
</div>
</div>
<a
href={`http://localhost:3000${doc.downloadUrl}`}
href={`${doc.downloadUrl}`}
download
className="flex-shrink-0 p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Télécharger"

View File

@@ -28,11 +28,52 @@ const NewLeaveRequestModal = ({
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const safeCounters = {
availableCP: availableLeaveCounters?.availableCP ?? 0,
availableRTT: availableLeaveCounters?.availableRTT ?? 0
// ⭐ État pour les données des compteurs
const [countersData, setCountersData] = useState(null);
const [isLoadingCounters, setIsLoadingCounters] = useState(true);
// ⭐ Charger les compteurs au montage
useEffect(() => {
const fetchCounters = async () => {
if (!userId) return;
setIsLoadingCounters(true);
try {
const response = await fetch(
`/api/getDetailedLeaveCounters?user_id=${userId}`
);
const data = await response.json();
if (data.success) {
console.log('📊 Compteurs reçus:', data);
setCountersData(data);
} else {
console.error('❌ Erreur compteurs:', data.message);
}
} catch (error) {
console.error('❌ Erreur réseau compteurs:', error);
} finally {
setIsLoadingCounters(false);
}
};
fetchCounters();
}, [userId]);
// ⭐ Utiliser les données des compteurs
const safeCounters = countersData ? {
availableCP: parseFloat(countersData.data?.totalDisponible?.cp || 0),
availableRTT: parseFloat(countersData.data?.totalDisponible?.rtt || 0),
availableRecup: parseFloat(countersData.data?.totalDisponible?.recup || 0)
} : {
availableCP: 0,
availableRTT: 0,
availableRecup: 0
};
console.log('📊 Compteurs disponibles:', safeCounters);
console.log('📊 Données complètes:', countersData);
useEffect(() => {
if (preselectedStartDate || preselectedEndDate) {
setFormData(prev => ({
@@ -44,7 +85,6 @@ const NewLeaveRequestModal = ({
}
}, [preselectedStartDate, preselectedEndDate, preselectedType]);
// 🔹 Calcul automatique - JOURS OUVRÉS UNIQUEMENT (Lun-Ven)
useEffect(() => {
if (formData.startDate && formData.endDate) {
const start = new Date(formData.startDate);
@@ -70,8 +110,7 @@ const NewLeaveRequestModal = ({
debut: formData.startDate,
fin: formData.endDate,
joursOuvres: workingDays,
samedis: saturdays,
message: 'Les samedis ne sont comptés QUE si "Récup" est coché'
samedis: saturdays
});
}
}, [formData.startDate, formData.endDate]);
@@ -105,20 +144,15 @@ const NewLeaveRequestModal = ({
const handlePeriodeChange = (type, periode) => {
setPeriodeSelection(prev => ({ ...prev, [type]: periode }));
// ⭐ CORRECTION : Ajuster automatiquement pour TOUTES les situations
if (formData.types.length === 1) {
// Si un seul type : ajuster selon la période
if (periode === 'Matin' || periode === 'Après-midi') {
setRepartition(prev => ({ ...prev, [type]: 0.5 }));
} else {
setRepartition(prev => ({ ...prev, [type]: totalDays }));
}
} else {
// Si plusieurs types : garder la répartition manuelle
// MAIS si c'est une période partielle sur une journée unique
if (formData.startDate === formData.endDate && (periode === 'Matin' || periode === 'Après-midi')) {
const currentRepartition = repartition[type] || 0;
// Suggérer 0.5 si pas encore défini
if (currentRepartition === 0 || currentRepartition === 1) {
setRepartition(prev => ({ ...prev, [type]: 0.5 }));
}
@@ -126,7 +160,6 @@ const NewLeaveRequestModal = ({
}
};
const handleFileUpload = (e) => {
const files = Array.from(e.target.files);
const validFiles = [];
@@ -170,6 +203,15 @@ const NewLeaveRequestModal = ({
const validateForm = () => {
console.log('\n🔍 === VALIDATION FORMULAIRE ===');
// 🔥 AJOUTER CES LOGS
console.log('📊 countersData:', countersData);
console.log('📊 countersData.success:', countersData?.success);
console.log('📊 countersData.data:', countersData?.data);
console.log('📊 totalDisponible:', countersData?.data?.totalDisponible);
console.log('📊 totalDisponible.cp:', countersData?.data?.totalDisponible?.cp);
console.log('📊 safeCounters:', safeCounters);
// Vérifications de base
if (formData.types.length === 0) {
setError('Veuillez sélectionner au moins un type de congé');
return false;
@@ -200,58 +242,82 @@ const NewLeaveRequestModal = ({
return false;
}
const hasRecup = formData.types.includes('Récup');
const hasABS = formData.types.includes('ABS');
// ⭐ NOUVEAU : Calculer le total attendu en tenant compte des demi-journées
let expectedTotal = totalDays;
// Si un seul type avec demi-journée sur une journée unique
if (formData.types.length === 1 && formData.startDate === formData.endDate) {
const type = formData.types[0];
const periode = periodeSelection[type];
if ((type === 'CP' || type === 'RTT') && (periode === 'Matin' || periode === 'Après-midi')) {
expectedTotal = 0.5;
console.log('📊 Demi-journée détectée, expectedTotal = 0.5');
}
}
console.log('📊 Analyse:', {
joursOuvres: totalDays,
expectedTotal: expectedTotal,
samedis: saturdayCount,
typesCoches: formData.types,
hasRecup
});
if (hasRecup && saturdayCount === 0) {
setError('Une récupération nécessite au moins un samedi dans la période. Veuillez sélectionner une période incluant un samedi ou décocher "Récupération (samedi)".');
if (hasABS && formData.types.length > 1) {
setError('Un arrêt maladie ne peut pas être mélangé avec d\'autres types de congés');
return false;
}
if (saturdayCount > 0 && !hasRecup) {
console.log(`⚠️ ${saturdayCount} samedi(s) détecté(s) mais "Récup" non coché - Les samedis seront ignorés`);
}
if (hasABS && formData.medicalDocuments.length === 0) {
setError('Un justificatif médical est obligatoire pour un arrêt maladie');
return false;
}
// VALIDATION RÉPARTITION AMÉLIORÉE
if (formData.types.length > 1) {
const sum = Object.values(repartition).reduce((a, b) => a + b, 0);
// VALIDATION DES SOLDES AVEC ANTICIPATION
// 🔥 CONDITION MODIFIÉE : Vérifier que les données sont bien chargées
if (!countersData || !countersData.data || !countersData.data.totalDisponible) {
console.error('❌ Données compteurs non disponibles pour validation !');
setError('Erreur : Les compteurs ne sont pas chargés. Veuillez réessayer.');
return false;
}
console.log('📊 Validation répartition:', {
somme: sum,
attendu: expectedTotal,
joursOuvres: totalDays,
samedisIgnores: !hasRecup ? saturdayCount : 0
// Calculer les jours demandés par type
const joursDemandesParType = {};
if (formData.types.length === 1) {
const type = formData.types[0];
const periode = periodeSelection[type] || 'Journée entière';
if (formData.startDate === formData.endDate && (periode === 'Matin' || periode === 'Après-midi')) {
joursDemandesParType[type] = 0.5;
} else {
joursDemandesParType[type] = totalDays;
}
} else {
formData.types.forEach(type => {
joursDemandesParType[type] = repartition[type] || 0;
});
}
if (Math.abs(sum - expectedTotal) > 0.01) {
setError(`La somme des jours répartis (${sum.toFixed(1)}j) doit être égale au total de jours ouvrés (${expectedTotal}j). ${saturdayCount > 0 && !hasRecup ? `Les ${saturdayCount} samedi(s) ne sont pas comptés.` : ''}`);
console.log('📊 Jours demandés:', joursDemandesParType);
console.log('📊 Soldes disponibles:', safeCounters);
// Vérifier CP
if (joursDemandesParType['CP'] > 0) {
const cpDemande = joursDemandesParType['CP'];
const cpDisponible = safeCounters.availableCP;
console.log(`🔍 CP: ${cpDemande}j demandés vs ${cpDisponible}j disponibles`);
if (cpDemande > cpDisponible) {
setError(`Solde CP insuffisant (${cpDisponible.toFixed(2)}j disponibles avec anticipation, ${cpDemande}j demandés)`);
return false;
}
}
// Vérifier RTT
if (joursDemandesParType['RTT'] > 0) {
const rttDemande = joursDemandesParType['RTT'];
const rttDisponible = safeCounters.availableRTT;
console.log(`🔍 RTT: ${rttDemande}j demandés vs ${rttDisponible}j disponibles`);
if (rttDemande > rttDisponible) {
setError(`Solde RTT insuffisant (${rttDisponible.toFixed(2)}j disponibles avec anticipation, ${rttDemande}j demandés)`);
return false;
}
}
// Vérifier Récup
if (joursDemandesParType['Récup'] > 0) {
const recupDemande = joursDemandesParType['Récup'];
const recupDisponible = safeCounters.availableRecup;
console.log(`🔍 Récup: ${recupDemande}j demandés vs ${recupDisponible}j disponibles`);
if (recupDemande > recupDisponible) {
setError(`Solde Récup insuffisant (${recupDisponible.toFixed(2)}j disponibles, ${recupDemande}j demandés)`);
return false;
}
}
@@ -261,6 +327,7 @@ const NewLeaveRequestModal = ({
};
const handleSubmit = async () => {
setError('');
@@ -281,15 +348,13 @@ const NewLeaveRequestModal = ({
formDataToSend.append('Nom', userName);
formDataToSend.append('Commentaire', formData.reason || '');
// ⭐ CORRECTION : Calculer le NombreJours total correctement
let totalJoursToSend = totalDays;
// Si un seul type avec demi-journée
if (formData.types.length === 1 && formData.startDate === formData.endDate) {
const type = formData.types[0];
const periode = periodeSelection[type];
if ((type === 'CP' || type === 'RTT') && (periode === 'Matin' || periode === 'Après-midi')) {
if ((type === 'CP' || type === 'RTT' || type === 'Récup') && (periode === 'Matin' || periode === 'Après-midi')) {
totalJoursToSend = 0.5;
}
}
@@ -297,50 +362,31 @@ const NewLeaveRequestModal = ({
formDataToSend.append('NombreJours', totalJoursToSend);
const repartitionArray = formData.types.map(type => {
if (type === 'Récup' && formData.types.length === 1) {
console.log(`📝 Récup seul: ${saturdayCount} samedi(s)`);
return {
TypeConge: type,
NombreJours: saturdayCount,
PeriodeJournee: 'Journée entière'
};
}
if (type === 'Récup' && formData.types.length > 1) {
const joursRecup = repartition[type] || saturdayCount;
console.log(`📝 Récup (répartition): ${joursRecup}j`);
return {
TypeConge: type,
NombreJours: joursRecup,
PeriodeJournee: 'Journée entière'
};
}
// ⭐ CORRECTION : Gérer demi-journées pour un seul type
let nombreJours;
let periodeJournee = 'Journée entière';
if (formData.types.length === 1) {
// Un seul type : utiliser soit 0.5 (demi-journée) soit totalDays
const periode = periodeSelection[type];
if ((type === 'CP' || type === 'RTT') &&
const periode = periodeSelection[type] || 'Journée entière';
if ((type === 'CP' || type === 'RTT' || type === 'Récup') &&
formData.startDate === formData.endDate &&
(periode === 'Matin' || periode === 'Après-midi')) {
nombreJours = 0.5;
periodeJournee = periode;
} else {
nombreJours = totalDays;
}
} else {
// Plusieurs types : utiliser la répartition manuelle
nombreJours = repartition[type] || 0;
periodeJournee = periodeSelection[type] || 'Journée entière';
}
console.log(`📝 ${type}: ${nombreJours}j (${periodeSelection[type] || 'Journée entière'})`);
console.log(`📝 ${type}: ${nombreJours}j (${periodeJournee})`);
return {
TypeConge: type,
NombreJours: nombreJours,
PeriodeJournee: ['CP', 'RTT'].includes(type)
? (periodeSelection[type] || 'Journée entière')
: 'Journée entière'
PeriodeJournee: ['CP', 'RTT', 'Récup'].includes(type) ? periodeJournee : 'Journée entière'
};
});
@@ -353,7 +399,7 @@ const NewLeaveRequestModal = ({
formDataToSend.append('medicalDocuments', file);
});
const response = await fetch('http://localhost:3000/submitLeaveRequest', {
const response = await fetch('/api/submitLeaveRequest', {
method: 'POST',
body: formDataToSend
});
@@ -376,7 +422,6 @@ const NewLeaveRequestModal = ({
}
};
const handleTypeToggle = (type) => {
setFormData(prev => ({
...prev,
@@ -386,20 +431,74 @@ const NewLeaveRequestModal = ({
}));
};
const isTypeDisabled = (typeKey) => {
const hasABS = formData.types.includes('ABS');
const hasOtherTypes = formData.types.some(t => t !== 'ABS');
if (hasABS && typeKey !== 'ABS') {
return true;
}
if (hasOtherTypes && typeKey === 'ABS') {
return true;
}
return false;
};
const getDisabledTooltip = (typeKey) => {
if (formData.types.includes('ABS') && typeKey !== 'ABS') {
return '⚠️ Un arrêt maladie ne peut pas être mélangé avec d\'autres types';
}
if (formData.types.some(t => t !== 'ABS') && typeKey === 'ABS') {
return '⚠️ Un arrêt maladie ne peut pas être mélangé avec d\'autres types';
}
return '';
};
// ⭐ Inclure les détails des compteurs dans availableTypes
// ⭐ Inclure les détails des compteurs dans availableTypes
const availableTypes = userRole === 'Apprenti'
? [
{ key: 'CP', label: 'Congés payés', available: safeCounters.availableCP },
{
key: 'CP',
label: 'Congé(s) payé(s)',
// ✅ Afficher seulement le solde actuel (sans anticipé)
available: countersData?.data?.cpN?.solde || 0,
details: countersData?.data?.cpN
},
{ key: 'ABS', label: 'Arrêt maladie' },
{ key: 'Formation', label: 'Formation' },
{ key: 'Récup', label: 'Récupération (samedi)' },
{
key: 'Récup',
label: 'Récupération(s)',
available: countersData?.data?.recupN?.solde || 0
},
]
: [
{ key: 'CP', label: 'Congés payés', available: safeCounters.availableCP },
{ key: 'RTT', label: 'RTT', available: safeCounters.availableRTT },
{ key: 'ABS', label: 'Arrêt maladie' },
{ key: 'Récup', label: 'Récupération (samedi)' },
{
key: 'CP',
label: 'Congé(s) payé(s)',
// ✅ Afficher seulement le solde actuel (sans anticipé)
available: countersData?.data?.cpN?.solde || 0,
details: countersData?.data?.cpN
},
{
key: 'RTT',
label: 'RTT',
// ✅ Afficher seulement le solde actuel (sans anticipé)
available: countersData?.data?.rttN?.solde || 0,
details: countersData?.data?.rttN
},
{
key: 'Récup',
label: 'Récupération',
available: countersData?.data?.recupN?.solde || 0
},
{ key: 'ABS', label: 'Arrêt maladie' }
];
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
@@ -411,34 +510,75 @@ const NewLeaveRequestModal = ({
</div>
<div className="p-6 space-y-5">
{/* ⭐ BLOC SOLDES DÉTAILLÉS */}
{/* Loading */}
{isLoadingCounters && (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 text-center">
<div className="animate-spin h-6 w-6 border-2 border-blue-600 border-t-transparent rounded-full mx-auto mb-2"></div>
<p className="text-sm text-gray-600">Chargement des soldes...</p>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-900 mb-3">
Types de congé *
Types d'absences *
</label>
<div className="space-y-2">
{availableTypes.map(type => (
<label key={type.key} className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={formData.types.includes(type.key)}
onChange={() => handleTypeToggle(type.key)}
className="w-4 h-4 rounded border-gray-300"
/>
<span className={`px-2.5 py-1 rounded text-sm font-medium ${type.key === 'CP' ? 'bg-blue-100 text-blue-800' :
{availableTypes.map(type => {
const disabled = isTypeDisabled(type.key);
const tooltip = getDisabledTooltip(type.key);
return (
<label
key={type.key}
className={`flex items-center gap-3 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
title={tooltip}
>
<input
type="checkbox"
checked={formData.types.includes(type.key)}
onChange={() => handleTypeToggle(type.key)}
disabled={disabled}
className={`w-4 h-4 rounded border-gray-300 ${disabled ? 'cursor-not-allowed' : ''}`}
/>
<span className={`px-2.5 py-1 rounded text-sm font-medium ${type.key === 'CP' ? 'bg-blue-100 text-blue-800' :
type.key === 'RTT' ? 'bg-green-100 text-green-800' :
type.key === 'ABS' ? 'bg-red-100 text-red-800' :
'bg-purple-100 text-purple-800'
}`}>
{type.label}
</span>
{type.available !== undefined && (
<span className="text-sm text-gray-600">
({type.available.toFixed(2)} disponibles)
type.key === 'Récup' ? 'bg-orange-100 text-orange-800' :
'bg-purple-100 text-purple-800'
}`}>
{type.label}
</span>
)}
</label>
))}
{type.available !== undefined && (
<div className="flex flex-col text-xs">
<span className={`font-semibold ${type.details?.solde < 0 || type.details?.anticipe?.depassement > 0
? 'text-red-600'
: 'text-gray-600'
}`}>
({type.available.toFixed(2)} disponibles)
</span>
{type.details?.anticipe?.depassement > 0 && (
<span className="text-red-600 text-xs italic">
⚠️ Dépassement anticipation
</span>
)}
</div>
)}
</label>
);
})}
</div>
{formData.types.includes('ABS') && (
<div className="mt-3 flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<AlertCircle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
<p className="text-amber-700 text-xs">
Un arrêt maladie ne peut pas être combiné avec d'autres types de congés.
</p>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
@@ -468,12 +608,7 @@ const NewLeaveRequestModal = ({
</div>
</div>
{/* ⭐ SECTION PÉRIODE POUR UN SEUL TYPE */}
{/* ⭐ SECTION PÉRIODE POUR UN SEUL TYPE */}
{formData.types.length === 1 && ['CP', 'RTT'].includes(formData.types[0]) && (
{formData.types.length === 1 && ['CP', 'RTT', 'Récup'].includes(formData.types[0]) && (
<div className="border-t border-gray-200 pt-4">
<h3 className="text-sm font-semibold text-gray-900 mb-2">
Période de la journée
@@ -496,7 +631,6 @@ const NewLeaveRequestModal = ({
))}
</div>
{/* 🎯 AFFICHAGE DU NOMBRE DE JOURS */}
<div className="mt-3 flex items-center justify-center">
<div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-2 inline-flex items-center gap-2">
<span className="text-sm font-medium text-blue-900">Durée sélectionnée :</span>
@@ -506,10 +640,8 @@ const NewLeaveRequestModal = ({
const periode = periodeSelection[type] || 'Journée entière';
if (formData.startDate === formData.endDate) {
// Journée unique
return (periode === 'Matin' || periode === 'Après-midi') ? '0.5 jour' : '1 jour';
} else {
// Plusieurs jours
return (periode === 'Matin' || periode === 'Après-midi')
? `${(totalDays - 0.5).toFixed(1)} jours`
: `${totalDays} jour${totalDays > 1 ? 's' : ''}`;
@@ -521,20 +653,17 @@ const NewLeaveRequestModal = ({
</div>
)}
{/* ⭐ SECTION RÉPARTITION POUR PLUSIEURS TYPES */}
{/* ⭐ SECTION RÉPARTITION POUR PLUSIEURS TYPES */}
{formData.types.length > 1 && totalDays > 0 && (
<div className="border-t border-gray-200 pt-4">
<h3 className="text-sm font-semibold text-gray-900 mb-2">
Répartition des {totalDays} jours ouvrés
</h3>
<p className="text-xs text-gray-500 mb-4">
La somme doit être égale à {totalDays} jour(s)
Indiquez la répartition souhaitée (le système vérifiera automatiquement)
</p>
<div className="space-y-3">
{formData.types.map((type) => {
const showPeriode = ['CP', 'RTT'].includes(type);
const showPeriode = ['CP', 'RTT', 'Récup'].includes(type);
const currentValue = repartition[type] || 0;
return (
@@ -547,11 +676,10 @@ const NewLeaveRequestModal = ({
type="number"
step="0.5"
min="0"
max={type === 'Récup' ? saturdayCount : totalDays}
max={totalDays}
value={repartition[type] || ''}
onChange={(e) => handleRepartitionChange(type, e.target.value)}
className="w-24 px-2 py-1 border rounded text-right text-sm"
placeholder={type === 'Récup' ? `Max ${saturdayCount}` : ''}
/>
</div>
@@ -593,7 +721,6 @@ const NewLeaveRequestModal = ({
</button>
</div>
{/* 🎯 AFFICHAGE DU NOMBRE DE JOURS POUR CE TYPE */}
<div className="mt-2 text-center">
<span className="text-xs font-medium text-gray-600">
Durée :
@@ -611,7 +738,6 @@ const NewLeaveRequestModal = ({
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-900 mb-2">
Motif (optionnel)
@@ -660,7 +786,7 @@ const NewLeaveRequestModal = ({
{formData.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 ({formData.medicalDocuments.length}) :
Fichiers sélectionnés ({formData.medicalDocuments.length})
</p>
{formData.medicalDocuments.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
@@ -698,7 +824,7 @@ const NewLeaveRequestModal = ({
{error && (
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-red-700 text-sm">{error}</p>
<p className="text-red-700 text-sm whitespace-pre-line">{error}</p>
</div>
)}
@@ -713,7 +839,7 @@ const NewLeaveRequestModal = ({
<button
type="button"
onClick={handleSubmit}
disabled={isSubmitting}
disabled={isSubmitting || isLoadingCounters}
className="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium transition-colors"
>
{isSubmitting ? 'Envoi...' : 'Soumettre'}
@@ -725,4 +851,4 @@ const NewLeaveRequestModal = ({
);
};
export default NewLeaveRequestModal;
export default NewLeaveRequestModal;

View File

@@ -27,27 +27,48 @@ const Sidebar = ({ isOpen, onToggle }) => {
return 'bg-cyan-600 text-white';
case 'Collaboratrice':
return 'bg-cyan-600 text-white';
case 'Apprenti':
return 'bg-blue-100 text-blue-800';
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);
};
// ✅ VERSION ULTRA-ROBUSTE pour isForfaitJour
const isForfaitJour = (() => {
if (!user?.TypeContrat && !user?.typeContrat) return false;
// Vérifier si l'utilisateur est en forfait jour
const isForfaitJour = user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfaitjour';
const typeContrat = (user?.TypeContrat || user?.typeContrat || '').toString().toLowerCase();
// Normaliser : retirer espaces, underscores, tirets
const normalized = typeContrat.replace(/[\s_-]/g, '');
return normalized === 'forfaitjour';
})();
// ✅ Vérification pour l'accès équipe
const hasTeamAccess = [
'Collaborateur',
'Collaboratrice',
'Apprenti',
'Validateur',
'Validatrice',
'Manager',
'RH',
'Directeur de campus',
'Directrice de campus',
'President',
'Admin'
].includes(user?.role);
const isCollaboratorRole = ['Collaborateur', 'Collaboratrice', 'Apprenti'].includes(user?.role);
const teamPath = isCollaboratorRole ? '/collaborateur' : '/manager';
// 🐛 DEBUG
console.log('👤 User:', user);
console.log('📋 Type Contrat RAW:', user?.TypeContrat);
console.log('📋 normalized:', (user?.TypeContrat || '').toString().toLowerCase().replace(/[\s_-]/g, ''));
console.log('✅ isForfaitJour:', isForfaitJour);
return (
<>
@@ -64,14 +85,12 @@ const Sidebar = ({ isOpen, onToggle }) => {
${isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}
>
{/* Bouton fermer (mobile) */}
<div className="lg:hidden flex justify-end p-4">
<button onClick={onToggle} className="p-2 rounded-lg hover:bg-gray-100">
<X className="w-6 h-6" />
</button>
</div>
{/* Logo */}
<div className="p-6 border-b border-gray-100">
<div className="flex flex-col items-center gap-2">
<img
@@ -82,7 +101,6 @@ const Sidebar = ({ isOpen, onToggle }) => {
</div>
</div>
{/* Infos utilisateur */}
<div className="p-4 lg:p-6 border-b border-gray-100">
<div className="flex flex-col items-center text-center">
<img
@@ -110,26 +128,27 @@ const Sidebar = ({ isOpen, onToggle }) => {
</div>
</div>
{/* Navigation */}
<nav className="flex-1 p-4 space-y-2">
<Link
to="/dashboard"
data-tour="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" />
<span className="font-medium">Dashboard</span>
<span className="font-medium">Tableau de bord</span>
</Link>
<Link
to="/demandes"
data-tour="demandes"
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"
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<FileText className="w-5 h-5" />
@@ -138,24 +157,26 @@ const Sidebar = ({ isOpen, onToggle }) => {
<Link
to="/calendrier"
data-tour="calendrier"
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"
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "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) && (
{/* Compte-Rendu avec vérification robuste */}
{isForfaitJour && (
<Link
to="/compte-rendu-activites"
data-tour="compte-rendu"
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"
? "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" />
@@ -163,38 +184,22 @@ const Sidebar = ({ isOpen, onToggle }) => {
</Link>
)}
{(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"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Users className="w-5 h-5" />
<span className="font-medium">Mon équipe</span>
</Link>
);
})()}
{hasTeamAccess && (
<Link
to={teamPath}
data-tour="mon-equipe"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(teamPath)
? "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>
)}
</nav>
{/* Bouton déconnexion */}
<div className="p-4 border-t border-gray-100">
<button
onClick={logout}