Reapply "V1_Fonctionnel_GTAV1_GTA"

This reverts commit 244db6bfb6.
This commit is contained in:
2025-12-02 18:04:52 +01:00
parent 6f75a66906
commit 89d74363f8
55 changed files with 7567 additions and 5819 deletions

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;