Files
GTA/project/src/components/GlobalTutorial.jsx
2026-01-12 12:16:53 +01:00

728 lines
34 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 mai de l\'année suivante !',
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 au JPO/SF.',
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;