@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider, useAuth } from './context/AuthContext'; // ⭐ Ajout de useAuth
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Login from './pages/Login';
|
||||
import Requests from './pages/Requests';
|
||||
@@ -9,103 +9,87 @@ import Manager from './pages/Manager';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import EmployeeDetails from './pages/EmployeeDetails';
|
||||
import Collaborateur from './pages/Collaborateur';
|
||||
import CompteRenduActivites from './pages/CompteRenduActivite';
|
||||
import GlobalTutorial from './components/GlobalTutorial';
|
||||
|
||||
// ⭐ Créer un composant séparé pour utiliser useAuth
|
||||
function AppContent() {
|
||||
const { user } = useAuth();
|
||||
const userId = user?.id || user?.CollaborateurADId || user?.ID;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ⭐ Tutoriel global - Il s'affichera sur toutes les pages */}
|
||||
<GlobalTutorial userId={userId} />
|
||||
|
||||
<Routes>
|
||||
{/* Route publique */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/* Routes protégées */}
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/demandes"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'RH', 'Admin']}>
|
||||
<Requests />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/calendrier"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||
<Calendar />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/manager"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||
<Manager />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/collaborateur"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti']}>
|
||||
<Collaborateur />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/employee/:id"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['RH', 'Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'Admin', 'President']}>
|
||||
<EmployeeDetails />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* ⭐ Nouvelle route pour Compte-Rendu d'Activités */}
|
||||
<Route
|
||||
path="/compte-rendu-activites"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||
<CompteRenduActivites />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Redirection par défaut */}
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
|
||||
{/* Route 404 - Redirection vers dashboard */}
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import CompteRenduActivites from './pages/CompteRenduActivite'; // ⭐ Ajout
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<AppContent />
|
||||
<Routes>
|
||||
{/* Route publique */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/* Routes protégées */}
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/demandes"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'RH', 'Admin']}>
|
||||
<Requests />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/calendrier"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti', 'Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||
<Calendar />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/manager"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||
<Manager />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/collaborateur"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Collaborateur', 'Collaboratrice', 'Apprenti']}>
|
||||
<Collaborateur />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/employee/:id"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['RH', 'Manager', 'Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'Admin', 'President']}>
|
||||
<EmployeeDetails />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* ⭐ Nouvelle route pour Compte-Rendu d'Activités */}
|
||||
<Route
|
||||
path="/compte-rendu-activites"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['Validateur', 'Validatrice', 'Directeur de campus', 'Directrice de campus', 'RH', 'Admin', 'President']}>
|
||||
<CompteRenduActivites />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Redirection par défaut */}
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
|
||||
{/* Route 404 - Redirection vers dashboard */}
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
@@ -1,47 +1,22 @@
|
||||
// authConfig.js
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
const protocol = window.location.protocol;
|
||||
|
||||
// Détection environnements (utile pour le debug)
|
||||
const isProduction = hostname === "mygta.ensup-adm.net";
|
||||
|
||||
// --- API URL ---
|
||||
// On utilise TOUJOURS /api car le proxy Vite (port 80) va rediriger vers le backend (port 3000)
|
||||
// Cela évite les problèmes CORS et les problèmes de ports fermés (8000)
|
||||
export const API_BASE_URL = "/api";
|
||||
|
||||
// --- MSAL Config ---
|
||||
export const msalConfig = {
|
||||
auth: {
|
||||
clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561",
|
||||
authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9",
|
||||
|
||||
// En prod, on force l'URL sans slash final pour être propre
|
||||
redirectUri: isProduction
|
||||
? "https://mygta.ensup-adm.net"
|
||||
: `${protocol}//${hostname}`,
|
||||
clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561", // Application (client) ID dans Azure
|
||||
authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9", // Directory (tenant) ID
|
||||
redirectUri: "http://localhost:5173"
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: "sessionStorage",
|
||||
cacheLocation: "sessionStorage",
|
||||
storeAuthStateInCookie: false,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// --- Permissions Graph ---
|
||||
export const loginRequest = {
|
||||
scopes: [
|
||||
"User.Read",
|
||||
"User.Read.All",
|
||||
"Group.Read.All",
|
||||
"GroupMember.Read.All",
|
||||
"Mail.Send",
|
||||
],
|
||||
"User.Read.All", // Pour lire les profils des autres utilisateurs
|
||||
"Group.Read.All", // Pour lire les groupes
|
||||
"GroupMember.Read.All", // Pour lire les membres des groupes
|
||||
"Mail.Send" //Envoyer les emails.
|
||||
]
|
||||
};
|
||||
|
||||
console.log("🔧 Config Auth:", {
|
||||
hostname,
|
||||
protocol,
|
||||
API_BASE_URL,
|
||||
redirectUri: msalConfig.auth.redirectUri,
|
||||
});
|
||||
|
||||
@@ -178,44 +178,30 @@ const EditLeaveRequestModal = ({
|
||||
try {
|
||||
const formDataToSend = new FormData();
|
||||
|
||||
// ⭐ Ajouter tous les champs texte AVANT les fichiers
|
||||
formDataToSend.append('requestId', request.id.toString());
|
||||
formDataToSend.append('leaveType', leaveType.toString());
|
||||
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.toString());
|
||||
formDataToSend.append('reason', reason);
|
||||
formDataToSend.append('userId', userId);
|
||||
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.toString());
|
||||
formDataToSend.append('businessDays', daysToSend);
|
||||
|
||||
// ⭐ Documents médicaux EN DERNIER
|
||||
if (medicalDocuments.length > 0) {
|
||||
medicalDocuments.forEach((file) => {
|
||||
formDataToSend.append('medicalDocuments', file);
|
||||
});
|
||||
}
|
||||
|
||||
// ⭐ 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('/updateRequest', {
|
||||
method: 'POST',
|
||||
// ⭐ NE PAS mettre de Content-Type, le navigateur le fera automatiquement avec boundary
|
||||
body: formDataToSend
|
||||
// ⭐ Documents médicaux
|
||||
medicalDocuments.forEach((file) => {
|
||||
formDataToSend.append('medicalDocuments', file);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const response = await fetch('http://localhost:3000/updateRequest', {
|
||||
method: 'POST',
|
||||
body: formDataToSend
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -236,7 +222,7 @@ const EditLeaveRequestModal = ({
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur:', error);
|
||||
console.error('Erreur:', error);
|
||||
setSubmitMessage({
|
||||
type: 'error',
|
||||
text: '❌ Une erreur est survenue. Veuillez réessayer.'
|
||||
|
||||
@@ -1,728 +0,0 @@
|
||||
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;
|
||||
1
project/src/components/Layout.jsx
Normal file
1
project/src/components/Layout.jsx
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -15,7 +15,7 @@ const MedicalDocuments = ({ demandeId }) => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/medical-documents/${demandeId}`);
|
||||
const response = await fetch(`http://localhost:3000/medical-documents/${demandeId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -116,7 +116,7 @@ const MedicalDocuments = ({ demandeId }) => {
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={`${doc.downloadUrl}`}
|
||||
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"
|
||||
|
||||
@@ -28,52 +28,11 @@ const NewLeaveRequestModal = ({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// ⭐ É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(
|
||||
`/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
|
||||
const safeCounters = {
|
||||
availableCP: availableLeaveCounters?.availableCP ?? 0,
|
||||
availableRTT: availableLeaveCounters?.availableRTT ?? 0
|
||||
};
|
||||
|
||||
console.log('📊 Compteurs disponibles:', safeCounters);
|
||||
console.log('📊 Données complètes:', countersData);
|
||||
|
||||
useEffect(() => {
|
||||
if (preselectedStartDate || preselectedEndDate) {
|
||||
setFormData(prev => ({
|
||||
@@ -85,6 +44,7 @@ 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);
|
||||
@@ -110,7 +70,8 @@ const NewLeaveRequestModal = ({
|
||||
debut: formData.startDate,
|
||||
fin: formData.endDate,
|
||||
joursOuvres: workingDays,
|
||||
samedis: saturdays
|
||||
samedis: saturdays,
|
||||
message: 'Les samedis ne sont comptés QUE si "Récup" est coché'
|
||||
});
|
||||
}
|
||||
}, [formData.startDate, formData.endDate]);
|
||||
@@ -144,15 +105,20 @@ 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 }));
|
||||
}
|
||||
@@ -160,6 +126,7 @@ const NewLeaveRequestModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
const files = Array.from(e.target.files);
|
||||
const validFiles = [];
|
||||
@@ -203,15 +170,6 @@ 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;
|
||||
@@ -242,82 +200,58 @@ const NewLeaveRequestModal = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasRecup = formData.types.includes('Récup');
|
||||
const hasABS = formData.types.includes('ABS');
|
||||
|
||||
if (hasABS && formData.types.length > 1) {
|
||||
setError('Un arrêt maladie ne peut pas être mélangé avec d\'autres types de congés');
|
||||
// ⭐ 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)".');
|
||||
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 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;
|
||||
}
|
||||
// ⭐ VALIDATION RÉPARTITION AMÉLIORÉE
|
||||
if (formData.types.length > 1) {
|
||||
const sum = Object.values(repartition).reduce((a, b) => a + b, 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;
|
||||
console.log('📊 Validation répartition:', {
|
||||
somme: sum,
|
||||
attendu: expectedTotal,
|
||||
joursOuvres: totalDays,
|
||||
samedisIgnores: !hasRecup ? saturdayCount : 0
|
||||
});
|
||||
}
|
||||
|
||||
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)`);
|
||||
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.` : ''}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -327,7 +261,6 @@ const NewLeaveRequestModal = ({
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setError('');
|
||||
|
||||
@@ -348,13 +281,15 @@ 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' || type === 'Récup') && (periode === 'Matin' || periode === 'Après-midi')) {
|
||||
if ((type === 'CP' || type === 'RTT') && (periode === 'Matin' || periode === 'Après-midi')) {
|
||||
totalJoursToSend = 0.5;
|
||||
}
|
||||
}
|
||||
@@ -362,31 +297,50 @@ 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) {
|
||||
const periode = periodeSelection[type] || 'Journée entière';
|
||||
|
||||
if ((type === 'CP' || type === 'RTT' || type === 'Récup') &&
|
||||
// Un seul type : utiliser soit 0.5 (demi-journée) soit totalDays
|
||||
const periode = periodeSelection[type];
|
||||
if ((type === 'CP' || type === 'RTT') &&
|
||||
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 (${periodeJournee})`);
|
||||
console.log(`📝 ${type}: ${nombreJours}j (${periodeSelection[type] || 'Journée entière'})`);
|
||||
|
||||
return {
|
||||
TypeConge: type,
|
||||
NombreJours: nombreJours,
|
||||
PeriodeJournee: ['CP', 'RTT', 'Récup'].includes(type) ? periodeJournee : 'Journée entière'
|
||||
PeriodeJournee: ['CP', 'RTT'].includes(type)
|
||||
? (periodeSelection[type] || 'Journée entière')
|
||||
: 'Journée entière'
|
||||
};
|
||||
});
|
||||
|
||||
@@ -399,7 +353,7 @@ const NewLeaveRequestModal = ({
|
||||
formDataToSend.append('medicalDocuments', file);
|
||||
});
|
||||
|
||||
const response = await fetch('/submitLeaveRequest', {
|
||||
const response = await fetch('http://localhost:3000/submitLeaveRequest', {
|
||||
method: 'POST',
|
||||
body: formDataToSend
|
||||
});
|
||||
@@ -422,6 +376,7 @@ const NewLeaveRequestModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleTypeToggle = (type) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
@@ -431,74 +386,20 @@ 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)',
|
||||
// ✅ Afficher seulement le solde actuel (sans anticipé)
|
||||
available: countersData?.data?.cpN?.solde || 0,
|
||||
details: countersData?.data?.cpN
|
||||
},
|
||||
{ key: 'CP', label: 'Congés payés', available: safeCounters.availableCP },
|
||||
{ key: 'ABS', label: 'Arrêt maladie' },
|
||||
{ key: 'Formation', label: 'Formation' },
|
||||
{
|
||||
key: 'Récup',
|
||||
label: 'Récupération(s)',
|
||||
available: countersData?.data?.recupN?.solde || 0
|
||||
},
|
||||
{ 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' }
|
||||
{ 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)' },
|
||||
];
|
||||
|
||||
|
||||
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">
|
||||
@@ -510,75 +411,34 @@ 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 d'absences *
|
||||
Types de congé *
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{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' :
|
||||
{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' :
|
||||
type.key === 'RTT' ? 'bg-green-100 text-green-800' :
|
||||
type.key === 'ABS' ? 'bg-red-100 text-red-800' :
|
||||
type.key === 'Récup' ? 'bg-orange-100 text-orange-800' :
|
||||
'bg-purple-100 text-purple-800'
|
||||
}`}>
|
||||
{type.label}
|
||||
'bg-purple-100 text-purple-800'
|
||||
}`}>
|
||||
{type.label}
|
||||
</span>
|
||||
{type.available !== undefined && (
|
||||
<span className="text-sm text-gray-600">
|
||||
({type.available.toFixed(2)} disponibles)
|
||||
</span>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
</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">
|
||||
@@ -608,7 +468,12 @@ const NewLeaveRequestModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.types.length === 1 && ['CP', 'RTT', 'Récup'].includes(formData.types[0]) && (
|
||||
|
||||
|
||||
|
||||
{/* ⭐ SECTION PÉRIODE POUR UN SEUL TYPE */}
|
||||
{/* ⭐ SECTION PÉRIODE POUR UN SEUL TYPE */}
|
||||
{formData.types.length === 1 && ['CP', 'RTT'].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
|
||||
@@ -631,6 +496,7 @@ 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>
|
||||
@@ -640,8 +506,10 @@ 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' : ''}`;
|
||||
@@ -653,17 +521,20 @@ 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">
|
||||
Indiquez la répartition souhaitée (le système vérifiera automatiquement)
|
||||
La somme doit être égale à {totalDays} jour(s)
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{formData.types.map((type) => {
|
||||
const showPeriode = ['CP', 'RTT', 'Récup'].includes(type);
|
||||
const showPeriode = ['CP', 'RTT'].includes(type);
|
||||
const currentValue = repartition[type] || 0;
|
||||
|
||||
return (
|
||||
@@ -676,10 +547,11 @@ const NewLeaveRequestModal = ({
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max={totalDays}
|
||||
max={type === 'Récup' ? saturdayCount : 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>
|
||||
|
||||
@@ -721,6 +593,7 @@ 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 :
|
||||
@@ -738,6 +611,7 @@ const NewLeaveRequestModal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-900 mb-2">
|
||||
Motif (optionnel)
|
||||
@@ -786,7 +660,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">
|
||||
@@ -824,7 +698,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 whitespace-pre-line">{error}</p>
|
||||
<p className="text-red-700 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -839,7 +713,7 @@ const NewLeaveRequestModal = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || isLoadingCounters}
|
||||
disabled={isSubmitting}
|
||||
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'}
|
||||
@@ -851,4 +725,4 @@ const NewLeaveRequestModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default NewLeaveRequestModal;
|
||||
export default NewLeaveRequestModal;
|
||||
@@ -32,17 +32,22 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 🔧 CORRECTION : Vérification améliorée du type de contrat
|
||||
const isForfaitJour =
|
||||
user?.TypeContrat === 'forfait_jour' ||
|
||||
user?.typeContrat === 'forfait_jour' ||
|
||||
user?.TypeContrat === 'forfaitjour' ||
|
||||
user?.typeContrat === 'forfaitjour';
|
||||
// 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);
|
||||
};
|
||||
|
||||
// 🐛 DEBUG : Décommentez cette ligne pour voir la valeur
|
||||
console.log('👤 User:', user);
|
||||
console.log('📋 Type Contrat:', user?.TypeContrat, user?.typeContrat);
|
||||
console.log('✅ isForfaitJour:', isForfaitJour);
|
||||
// Vérifier si l'utilisateur est en forfait jour
|
||||
const isForfaitJour = user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfaitjour';
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -59,12 +64,14 @@ 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
|
||||
@@ -75,6 +82,7 @@ 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
|
||||
@@ -102,10 +110,10 @@ 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"
|
||||
@@ -113,12 +121,11 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
||||
}`}
|
||||
>
|
||||
<Home className="w-5 h-5" />
|
||||
<span className="font-medium">Tableau de bord</span>
|
||||
<span className="font-medium">Dashboard</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"
|
||||
@@ -131,7 +138,6 @@ 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"
|
||||
@@ -142,11 +148,10 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
||||
<span className="font-medium">Calendrier</span>
|
||||
</Link>
|
||||
|
||||
{/* 🔧 LIEN COMPTE-RENDU AVEC DEBUG */}
|
||||
{isForfaitJour && (
|
||||
{/* Lien Compte-Rendu d'Activités - Visible pour validateurs et directeurs */}
|
||||
{(canViewCompteRendu() || 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"
|
||||
@@ -176,7 +181,6 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
||||
return (
|
||||
<Link
|
||||
to={targetPath}
|
||||
data-tour="mon-equipe"
|
||||
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"
|
||||
@@ -190,6 +194,7 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
||||
})()}
|
||||
</nav>
|
||||
|
||||
{/* Bouton déconnexion */}
|
||||
<div className="p-4 border-t border-gray-100">
|
||||
<button
|
||||
onClick={logout}
|
||||
@@ -204,4 +209,4 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
export default Sidebar;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import * as msal from '@azure/msal-browser';
|
||||
// ✅ Correction: Import de API_BASE_URL
|
||||
import { msalConfig, loginRequest, API_BASE_URL } from '../authConfig';
|
||||
import { msalConfig, loginRequest } from '../AuthConfig';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
@@ -20,12 +19,7 @@ export const AuthProvider = ({ children }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isMsalInitialized, setIsMsalInitialized] = useState(false);
|
||||
|
||||
// ✅ Fonction corrigée pour construire l'URL
|
||||
const getApiUrl = (endpoint) => {
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
||||
// API_BASE_URL est "/api", donc cela retourne "/api/endpoint"
|
||||
return `${API_BASE_URL}/${cleanEndpoint}`;
|
||||
};
|
||||
const getApiUrl = (endpoint) => `http://localhost:3000/${endpoint}`;
|
||||
|
||||
// --- Vérifie l'autorisation de l'utilisateur via groupes
|
||||
const checkUserAuthorization = async (userPrincipalName, accessToken) => {
|
||||
@@ -173,7 +167,7 @@ export const AuthProvider = ({ children }) => {
|
||||
if (authResult.authorized) {
|
||||
setUser({
|
||||
id: syncResult?.localUserId || entraUser.id,
|
||||
CollaborateurADId: syncResult?.localUserId,
|
||||
CollaborateurADId: syncResult?.localUserId, // ⭐ AJOUT
|
||||
entraUserId: entraUser.id,
|
||||
name: entraUser.displayName,
|
||||
prenom: entraUser.givenName || entraUser.displayName?.split(' ')[0] || '',
|
||||
@@ -185,8 +179,8 @@ export const AuthProvider = ({ children }) => {
|
||||
jobTitle: entraUser.jobTitle,
|
||||
department: entraUser.department,
|
||||
officeLocation: entraUser.officeLocation,
|
||||
typeContrat: syncResult?.typeContrat || '37h',
|
||||
dateEntree: syncResult?.dateEntree || null,
|
||||
typeContrat: syncResult?.typeContrat || '37h', // ⭐ AJOUT
|
||||
dateEntree: syncResult?.dateEntree || null, // ⭐ AJOUT
|
||||
groups: authResult.groups
|
||||
});
|
||||
setIsAuthorized(true);
|
||||
|
||||
@@ -4,7 +4,7 @@ import App from './App.jsx';
|
||||
import './index.css';
|
||||
import { MsalProvider } from "@azure/msal-react";
|
||||
import { PublicClientApplication } from "@azure/msal-browser";
|
||||
import { msalConfig } from "./authConfig";
|
||||
import { msalConfig } from "./AuthConfig";
|
||||
|
||||
const msalInstance = new PublicClientApplication(msalConfig);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@ const Collaborateur = () => {
|
||||
|
||||
const fetchTeamMembers = async () => {
|
||||
try {
|
||||
const response = await fetch(`/getTeamMembers?manager_id=${user.id}`);
|
||||
const response = await fetch(`http://localhost:3000/getTeamMembers?manager_id=${user.id}`);
|
||||
const text = await response.text();
|
||||
console.log('Réponse équipe:', text);
|
||||
|
||||
@@ -60,7 +60,7 @@ const Collaborateur = () => {
|
||||
|
||||
const fetchPendingRequests = async () => {
|
||||
try {
|
||||
const response = await fetch(`/getPendingRequests?manager_id=${user.id}`);
|
||||
const response = await fetch(`http://localhost:3000/getPendingRequests?manager_id=${user.id}`);
|
||||
const text = await response.text();
|
||||
console.log('Réponse demandes en attente:', text);
|
||||
|
||||
@@ -76,7 +76,7 @@ const Collaborateur = () => {
|
||||
|
||||
const fetchAllTeamRequests = async () => {
|
||||
try {
|
||||
const response = await fetch(`/getAllTeamRequests?SuperieurId=${user.id}`);
|
||||
const response = await fetch(`http://localhost:3000/getAllTeamRequests?SuperieurId=${user.id}`);
|
||||
const text = await response.text();
|
||||
console.log('Réponse toutes demandes équipe:', text);
|
||||
|
||||
@@ -94,7 +94,7 @@ const Collaborateur = () => {
|
||||
|
||||
const handleValidateRequest = async (requestId, action, comment = '') => {
|
||||
try {
|
||||
const response = await fetch('/validateRequest', {
|
||||
const response = await fetch('http://localhost:3000/validateRequest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -191,7 +191,9 @@ const Collaborateur = () => {
|
||||
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
||||
{isEmployee ? 'Mon équipe 👥' : 'Gestion d\'équipe 👥'}
|
||||
</h1>
|
||||
|
||||
<p className="text-sm lg:text-base text-gray-600">
|
||||
{isEmployee ? 'Consultez les congés de votre équipe' : 'Gérez les demandes de congés de votre équipe'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
@@ -222,9 +224,35 @@ const Collaborateur = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs lg:text-sm font-medium text-gray-600">Approuvées</p>
|
||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">
|
||||
{allRequests.filter(r => r.status === 'Validée' || r.status === 'Approuvé').length}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">demandes</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<CheckCircle className="w-4 h-4 lg:w-6 lg:h-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs lg:text-sm font-medium text-gray-600">Refusées</p>
|
||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">
|
||||
{allRequests.filter(r => r.status === 'Refusée').length}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">demandes</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-red-100 rounded-lg flex items-center justify-center">
|
||||
<XCircle className="w-4 h-4 lg:w-6 lg:h-6 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
@@ -380,7 +408,7 @@ const Collaborateur = () => {
|
||||
<div className="text-sm mt-1">
|
||||
<p className="text-gray-500">Document joint</p>
|
||||
<a
|
||||
href={`/GTA/project/uploads/${request.file}`}
|
||||
href={`http://localhost/GTA/project/uploads/${request.file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline flex items-center gap-1 mt-1"
|
||||
@@ -436,7 +464,7 @@ const Collaborateur = () => {
|
||||
<div>
|
||||
<p className="text-gray-500">Document joint</p>
|
||||
<a
|
||||
href={`/GTA/project/uploads/${selectedRequest.file}`}
|
||||
href={`http://localhost/GTA/project/uploads/${selectedRequest.file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline flex items-center gap-2"
|
||||
|
||||
@@ -96,7 +96,7 @@ const CompteRenduActivites = () => {
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/getCompteRenduActivites?user_id=${userId}&annee=${annee}&mois=${mois}`);
|
||||
const response = await fetch(`http://localhost:3000/getCompteRenduActivites?user_id=${userId}&annee=${annee}&mois=${mois}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -106,7 +106,7 @@ const CompteRenduActivites = () => {
|
||||
console.log('📊 Détail des jours:', data.jours);
|
||||
}
|
||||
|
||||
const congesResponse = await fetch(`/getTeamLeaves?user_id=${userId}&role=${user.role}`);
|
||||
const congesResponse = await fetch(`http://localhost:3000/getTeamLeaves?user_id=${userId}&role=${user.role}`);
|
||||
const congesData = await congesResponse.json();
|
||||
|
||||
if (congesData.success) {
|
||||
@@ -125,7 +125,7 @@ const CompteRenduActivites = () => {
|
||||
if (!userId || !hasAccess()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/getStatsAnnuelles?user_id=${userId}&annee=${annee}`);
|
||||
const response = await fetch(`http://localhost:3000/getStatsAnnuelles?user_id=${userId}&annee=${annee}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -163,38 +163,22 @@ const CompteRenduActivites = () => {
|
||||
return selectedYear === previousYear && selectedMonth === previousMonth;
|
||||
};
|
||||
|
||||
// Générer les jours du mois (lundi-samedi) avec décalage correct
|
||||
// Générer les jours du mois (lundi-vendredi)
|
||||
const getDaysInMonth = () => {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const daysInMonth = lastDay.getDate();
|
||||
|
||||
// Jour de la semaine du 1er (0=dimanche, 1=lundi, ..., 6=samedi)
|
||||
let firstDayOfWeek = firstDay.getDay();
|
||||
|
||||
// Convertir pour que lundi = 0, mardi = 1, ..., samedi = 5, dimanche = 6
|
||||
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
|
||||
|
||||
const days = [];
|
||||
|
||||
// Ajouter des cases vides pour le décalage initial
|
||||
for (let i = 0; i < firstDayOfWeek; i++) {
|
||||
days.push(null);
|
||||
}
|
||||
|
||||
// Ajouter tous les jours du mois (lundi-samedi uniquement)
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const currentDay = new Date(year, month, day);
|
||||
const dayOfWeek = currentDay.getDay();
|
||||
|
||||
// Exclure les dimanches (0)
|
||||
if (dayOfWeek !== 0) {
|
||||
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
|
||||
days.push(currentDay);
|
||||
}
|
||||
}
|
||||
|
||||
return days;
|
||||
};
|
||||
|
||||
@@ -251,8 +235,6 @@ const CompteRenduActivites = () => {
|
||||
|
||||
// Ouvrir le modal de saisie
|
||||
const handleJourClick = (date) => {
|
||||
if (!date) return; // Ignorer les cases vides
|
||||
|
||||
if (!isMoisAutorise() && !isRH) {
|
||||
showInfo('Vous ne pouvez saisir que pour le mois en cours ou le mois précédent', 'warning');
|
||||
return;
|
||||
@@ -302,7 +284,7 @@ const CompteRenduActivites = () => {
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/saveCompteRenduJour', {
|
||||
const response = await fetch('http://localhost:3000/saveCompteRenduJour', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -345,7 +327,7 @@ const CompteRenduActivites = () => {
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/saveCompteRenduMasse', {
|
||||
const response = await fetch('http://localhost:3000/saveCompteRenduMasse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -505,7 +487,14 @@ const CompteRenduActivites = () => {
|
||||
<p className="text-sm opacity-90">Jours travaillés</p>
|
||||
<p className="text-3xl font-bold">{statsAnnuelles.totalJoursTravailles || 0}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white bg-opacity-20 rounded-lg p-4">
|
||||
<p className="text-sm opacity-90">Non-respect repos quotidien</p>
|
||||
<p className="text-3xl font-bold">{statsAnnuelles.totalNonRespectQuotidien || 0}</p>
|
||||
</div>
|
||||
<div className="bg-white bg-opacity-20 rounded-lg p-4">
|
||||
<p className="text-sm opacity-90">Non-respect repos hebdo</p>
|
||||
<p className="text-3xl font-bold">{statsAnnuelles.totalNonRespectHebdo || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -563,30 +552,23 @@ const CompteRenduActivites = () => {
|
||||
<span className="hidden sm:inline">Saisie en masse</span>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendrier */}
|
||||
<div className="bg-white rounded-lg border overflow-hidden shadow-sm">
|
||||
<div className="grid grid-cols-6 gap-2 p-4 bg-gray-50">
|
||||
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'].map(day => (
|
||||
<div className="grid grid-cols-5 gap-2 p-4 bg-gray-50">
|
||||
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi'].map(day => (
|
||||
<div key={day} className="text-center font-semibold text-gray-700 text-sm">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-6 gap-2 p-4">
|
||||
<div className="grid grid-cols-5 gap-2 p-4">
|
||||
{days.map((date, index) => {
|
||||
// Case vide pour le décalage
|
||||
if (date === null) {
|
||||
return (
|
||||
<div key={`empty-${index}`} className="min-h-[100px] p-3"></div>
|
||||
);
|
||||
}
|
||||
|
||||
const jourData = getJourData(date);
|
||||
const enConge = isJourEnConge(date);
|
||||
const ferie = isHoliday(date);
|
||||
@@ -725,7 +707,17 @@ const CompteRenduActivites = () => {
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedJour.jourTravaille}
|
||||
onChange={(e) => setSelectedJour({ ...selectedJour, jourTravaille: e.target.checked })}
|
||||
className="w-5 h-5 text-blue-600 rounded"
|
||||
/>
|
||||
<span className="text-gray-700 font-medium">Jour travaillé</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{selectedJour.jourTravaille && (
|
||||
<>
|
||||
@@ -823,7 +815,7 @@ const CompteRenduActivites = () => {
|
||||
<SaisieMasseModal
|
||||
mois={mois}
|
||||
annee={annee}
|
||||
days={days.filter(d => d !== null)} // Filtrer les cases vides
|
||||
days={days}
|
||||
congesData={congesData}
|
||||
holidays={holidays}
|
||||
onClose={() => setShowSaisieMasse(false)}
|
||||
@@ -902,30 +894,6 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
||||
onSave(joursTravailles);
|
||||
};
|
||||
|
||||
// Générer les jours avec décalage pour la saisie en masse aussi
|
||||
const getDaysWithOffset = () => {
|
||||
const year = annee;
|
||||
const month = mois - 1;
|
||||
const firstDay = new Date(year, month, 1);
|
||||
|
||||
let firstDayOfWeek = firstDay.getDay();
|
||||
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
|
||||
|
||||
const daysWithOffset = [];
|
||||
|
||||
// Ajouter des cases vides pour le décalage
|
||||
for (let i = 0; i < firstDayOfWeek; i++) {
|
||||
daysWithOffset.push(null);
|
||||
}
|
||||
|
||||
// Ajouter les jours réels
|
||||
daysWithOffset.push(...days);
|
||||
|
||||
return daysWithOffset;
|
||||
};
|
||||
|
||||
const daysWithOffset = getDaysWithOffset();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full p-6 max-h-[90vh] overflow-y-auto">
|
||||
@@ -947,15 +915,8 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
|
||||
Sélectionner tous les jours ouvrés disponibles
|
||||
</button>
|
||||
|
||||
<div className="grid grid-cols-6 gap-2 p-4">
|
||||
{daysWithOffset.map((date, index) => {
|
||||
// Case vide
|
||||
if (date === null) {
|
||||
return (
|
||||
<div key={`empty-${index}`} className="p-3"></div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="grid grid-cols-5 gap-2 mb-6">
|
||||
{days.map((date, index) => {
|
||||
const dateStr = formatDateToString(date);
|
||||
const enConge = isJourEnConge(date);
|
||||
const ferie = isHoliday(date);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import { Calendar, Clock, Plus, RefreshCw, Menu, Info, Briefcase, AlertCircle, Wifi, WifiOff, TrendingUp, HelpCircle, FileText, ChevronRight, Users } from 'lucide-react';
|
||||
import { Calendar, Clock, Plus, RefreshCw, Menu, Info, Briefcase, AlertCircle, Wifi, WifiOff, TrendingUp } from 'lucide-react';
|
||||
import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
||||
import { useMsal } from "@azure/msal-react";
|
||||
import { loginRequest } from "../authConfig";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -20,23 +19,16 @@ const Dashboard = () => {
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
const [lastRefresh, setLastRefresh] = useState(new Date());
|
||||
|
||||
// ⭐ NOUVEAUX STATES POUR SSE
|
||||
const [sseConnected, setSseConnected] = useState(false);
|
||||
const [toasts, setToasts] = useState([]);
|
||||
|
||||
// ⭐ NOUVEAU STATE POUR CONGÉS ANTICIPÉS
|
||||
const [congesAnticipes, setCongesAnticipes] = useState(null);
|
||||
const [showAnticipes, setShowAnticipes] = useState(false);
|
||||
const [recentRequests, setRecentRequests] = useState([]);
|
||||
const [teamLeaves, setTeamLeaves] = useState([]);
|
||||
const [isUpdatingCounters, setIsUpdatingCounters] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const userId = user?.id || user?.CollaborateurADId || user?.ID;
|
||||
|
||||
// 🎯 FONCTION POUR RELANCER LE TUTORIEL
|
||||
const handleRestartTutorial = () => {
|
||||
localStorage.removeItem(`global-tutorial-completed-${userId}`);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (accounts.length > 0) {
|
||||
const request = {
|
||||
@@ -60,7 +52,7 @@ const Dashboard = () => {
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
try {
|
||||
const response = await fetch(`/getNotifications?user_id=${userId}`);
|
||||
const response = await fetch(`http://localhost:3000/getNotifications?user_id=${userId}`);
|
||||
if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`);
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
@@ -81,46 +73,29 @@ const Dashboard = () => {
|
||||
|
||||
const fetchDetailedCounters = async () => {
|
||||
try {
|
||||
if (!isLoading) {
|
||||
setIsRefreshing(true);
|
||||
setIsUpdatingCounters(true);
|
||||
}
|
||||
|
||||
console.log('🔄 Appel getDetailedLeaveCounters pour userId:', userId);
|
||||
|
||||
const response = await fetch(`/getDetailedLeaveCounters?user_id=${userId}`);
|
||||
|
||||
// Debug: afficher le statut HTTP
|
||||
console.log('📡 Statut HTTP:', response.status);
|
||||
if (!isLoading) setIsRefreshing(true);
|
||||
|
||||
const response = await fetch(`http://localhost:3000/getDetailedLeaveCounters?user_id=${userId}`);
|
||||
const data = await response.json();
|
||||
|
||||
// Debug: afficher les données reçues
|
||||
console.log('📊 Données compteurs reçues:', JSON.stringify(data, null, 2));
|
||||
|
||||
if (data.success) {
|
||||
console.log('✅ Compteurs mis à jour dans le state');
|
||||
console.log(' - CP N-1:', data.data?.cpN1?.solde);
|
||||
console.log(' - CP N:', data.data?.cpN?.solde);
|
||||
console.log(' - RTT:', data.data?.rttN?.solde);
|
||||
setDetailedCounters(data.data);
|
||||
} else {
|
||||
console.error("❌ Erreur compteurs:", data.message);
|
||||
console.error("Erreur compteurs:", data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("💥 Erreur compteurs:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsRefreshing(false);
|
||||
setIsUpdatingCounters(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ⭐ NOUVELLE FONCTION : Récupérer les congés anticipés
|
||||
const fetchCongesAnticipes = async () => {
|
||||
try {
|
||||
console.log('🔍 Récupération des congés anticipés...');
|
||||
const response = await fetch(`/getCongesAnticipes?user_id=${userId}`);
|
||||
const response = await fetch(`http://localhost:3000/getCongesAnticipes?user_id=${userId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -134,84 +109,10 @@ const Dashboard = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecentRequests = async () => {
|
||||
try {
|
||||
console.log('🔍 Récupération des demandes récentes pour userId:', userId);
|
||||
|
||||
const url = `/getRequests?user_id=${userId}`;
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (parseError) {
|
||||
console.error('❌ Erreur parsing JSON:', parseError);
|
||||
console.log('📄 Réponse brute:', text.substring(0, 200));
|
||||
setRecentRequests([]);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📋 Données reçues:', data);
|
||||
|
||||
if (data.success && data.requests) {
|
||||
console.log('✅ Nombre total de demandes:', data.requests.length);
|
||||
|
||||
if (data.requests.length > 0) {
|
||||
console.log('📋 Structure de la première demande:', data.requests[0]);
|
||||
console.log('📋 Tous les champs disponibles:', Object.keys(data.requests[0]));
|
||||
}
|
||||
|
||||
const sortedRequests = data.requests
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.submittedAt || a.DateCreation || a.created_at);
|
||||
const dateB = new Date(b.submittedAt || b.DateCreation || b.created_at);
|
||||
return dateB - dateA;
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
console.log('✅ 5 demandes récentes:', sortedRequests);
|
||||
setRecentRequests(sortedRequests);
|
||||
} else {
|
||||
console.warn('⚠️ Aucune demande trouvée ou erreur:', data.message);
|
||||
setRecentRequests([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("💥 Erreur demandes récentes:", error);
|
||||
setRecentRequests([]);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTeamLeaves = async () => {
|
||||
try {
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const response = await fetch(`/getTeamLeaves?user_id=${userId}&role=${user.role}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const filteredLeaves = (data.leaves || []).filter(leave => {
|
||||
const startDate = new Date(leave.startdate);
|
||||
const endDate = new Date(leave.enddate);
|
||||
const startMonth = startDate.getMonth();
|
||||
const startYear = startDate.getFullYear();
|
||||
const endMonth = endDate.getMonth();
|
||||
const endYear = endDate.getFullYear();
|
||||
|
||||
return (startYear === currentYear && startMonth === currentMonth) ||
|
||||
(endYear === currentYear && endMonth === currentMonth) ||
|
||||
(startDate <= new Date(currentYear, currentMonth, 1) &&
|
||||
endDate >= new Date(currentYear, currentMonth + 1, 0));
|
||||
});
|
||||
setTeamLeaves(filteredLeaves);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("💥 Erreur congés équipe:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// ⭐ FONCTION DE RAFRAÎCHISSEMENT UNIFIÉE (modifiée)
|
||||
const refreshAllData = useCallback(async () => {
|
||||
if (!userId) return;
|
||||
|
||||
console.log('🔄 Rafraîchissement des données...');
|
||||
setIsRefreshing(true);
|
||||
|
||||
@@ -219,9 +120,7 @@ const Dashboard = () => {
|
||||
await Promise.all([
|
||||
fetchDetailedCounters(),
|
||||
fetchNotifications(),
|
||||
fetchCongesAnticipes(),
|
||||
fetchRecentRequests(),
|
||||
fetchTeamLeaves()
|
||||
fetchCongesAnticipes() // ⭐ AJOUT
|
||||
]);
|
||||
setLastRefresh(new Date());
|
||||
console.log('✅ Données rafraîchies');
|
||||
@@ -232,6 +131,7 @@ const Dashboard = () => {
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
// ⭐ FONCTION POUR AFFICHER DES TOASTS
|
||||
const showToast = useCallback((message, type = 'info') => {
|
||||
const id = Date.now();
|
||||
const newToast = { id, message, type };
|
||||
@@ -243,12 +143,13 @@ const Dashboard = () => {
|
||||
}, 5000);
|
||||
}, []);
|
||||
|
||||
// ⭐ CONNEXION SSE
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
console.log('🔌 Connexion SSE au serveur collaborateurs...');
|
||||
|
||||
const eventSource = new EventSource(`/api/events/collaborateur?user_id=${userId}`);
|
||||
const eventSource = new EventSource(`http://localhost:3000/api/events/collaborateur?user_id=${userId}`);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('✅ SSE connecté');
|
||||
@@ -266,7 +167,6 @@ const Dashboard = () => {
|
||||
break;
|
||||
|
||||
case 'heartbeat':
|
||||
// Ne rien afficher pour éviter de polluer les logs
|
||||
break;
|
||||
|
||||
case 'demande-validated-rh':
|
||||
@@ -282,44 +182,9 @@ const Dashboard = () => {
|
||||
break;
|
||||
|
||||
case 'compteur-updated':
|
||||
console.log('\n💰 === COMPTEUR MIS À JOUR (SSE) ===');
|
||||
console.log(' Collaborateur SSE:', data.collaborateurId, typeof data.collaborateurId);
|
||||
console.log(' UserId local:', userId, typeof userId);
|
||||
console.log(' Type congé:', data.typeConge);
|
||||
console.log(' Année:', data.annee);
|
||||
console.log(' Jours:', data.jours);
|
||||
console.log(' Type mise à jour:', data.typeUpdate);
|
||||
|
||||
// ✅ CORRECTION: Comparer en convertissant les deux en nombres
|
||||
const collabIdNum = parseInt(data.collaborateurId);
|
||||
const userIdNum = parseInt(userId);
|
||||
|
||||
console.log(' Comparaison:', collabIdNum, '===', userIdNum, '?', collabIdNum === userIdNum);
|
||||
|
||||
// Vérifier que c'est bien pour cet utilisateur
|
||||
if (collabIdNum === userIdNum) {
|
||||
console.log('✅ C\'EST POUR MOI ! Mise à jour des compteurs...');
|
||||
|
||||
// Afficher un toast informatif
|
||||
if (data.typeUpdate === 'reinitialisation') {
|
||||
showToast(`📊 Compteur ${data.typeConge} ${data.annee} réinitialisé`, 'info');
|
||||
} else if (data.typeUpdate === 'actualisation_manuel') {
|
||||
showToast(`🔄 Compteur ${data.typeConge} actualisé`, 'success');
|
||||
} else if (data.typeUpdate === 'actualisation_globale') {
|
||||
showToast('✅ Mise à jour automatique des compteurs', 'info');
|
||||
} else if (data.typeUpdate === 'modification_manuelle') {
|
||||
showToast(`✏️ Compteur ${data.typeConge} ${data.annee} modifié par RH`, 'info');
|
||||
}
|
||||
|
||||
// Rafraîchir les compteurs
|
||||
console.log('🔄 Rafraîchissement des compteurs...');
|
||||
fetchDetailedCounters().then(() => {
|
||||
fetchCongesAnticipes();
|
||||
console.log('✅ Compteurs rafraîchis');
|
||||
});
|
||||
} else {
|
||||
console.log('❌ Pas pour moi, j\'ignore');
|
||||
}
|
||||
console.log('📊 Compteurs mis à jour via SSE');
|
||||
fetchDetailedCounters();
|
||||
fetchCongesAnticipes(); // ⭐ AJOUT
|
||||
break;
|
||||
|
||||
case 'demande-list-updated':
|
||||
@@ -362,12 +227,14 @@ const Dashboard = () => {
|
||||
};
|
||||
}, [userId, refreshAllData, showToast]);
|
||||
|
||||
// ⭐ RAFRAÎCHISSEMENT INITIAL
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
refreshAllData();
|
||||
}
|
||||
}, [userId, refreshAllData]);
|
||||
|
||||
// ⭐ Polling de secours
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
@@ -379,6 +246,7 @@ const Dashboard = () => {
|
||||
return () => clearInterval(interval);
|
||||
}, [userId, refreshAllData]);
|
||||
|
||||
// ⭐ Rafraîchissement quand la page redevient visible
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible' && userId) {
|
||||
@@ -391,6 +259,7 @@ const Dashboard = () => {
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}, [userId, refreshAllData]);
|
||||
|
||||
// ⭐ Rafraîchissement au focus
|
||||
useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
if (userId) {
|
||||
@@ -405,7 +274,7 @@ const Dashboard = () => {
|
||||
|
||||
const markNotificationRead = async (id) => {
|
||||
try {
|
||||
const res = await fetch(`/markNotificationRead`, {
|
||||
const res = await fetch(`http://localhost:3000/markNotificationRead`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ notificationId: id }),
|
||||
@@ -433,6 +302,31 @@ const Dashboard = () => {
|
||||
await refreshAllData();
|
||||
};
|
||||
|
||||
const handleUpdateCounters = async () => {
|
||||
if (!confirm("🔄 Mettre à jour vos compteurs avec l'acquisition du mois en cours ?")) return;
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/updateCounters', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ collaborateur_id: userId }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
const updatesText = data.updates.map(u =>
|
||||
`${u.type}: +${(u.increment || 0).toFixed(2)}j (total: ${(u.nouveauSolde || 0).toFixed(2)}j)`
|
||||
).join('\n');
|
||||
alert("✅ Compteurs mis à jour !\n\n" + updatesText);
|
||||
|
||||
await refreshAllData();
|
||||
} else {
|
||||
alert(`❌ Erreur : ${data.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur:", error);
|
||||
alert("❌ Erreur serveur");
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return 'N/A';
|
||||
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
||||
@@ -551,7 +445,6 @@ const Dashboard = () => {
|
||||
</div>
|
||||
<div className="flex gap-2 lg:gap-3">
|
||||
<button
|
||||
data-tour="refresh"
|
||||
onClick={handleManualRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="bg-gray-200 text-gray-700 px-3 lg:px-4 py-2 lg:py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@@ -563,7 +456,6 @@ const Dashboard = () => {
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
data-tour="notifications"
|
||||
onClick={() => setShowNotifications(!showNotifications)}
|
||||
className="relative bg-white border border-gray-200 px-3 lg:px-4 py-2 lg:py-3 rounded-lg font-medium hover:bg-gray-50 transition-colors flex items-center gap-2 shadow-sm"
|
||||
>
|
||||
@@ -622,7 +514,6 @@ const Dashboard = () => {
|
||||
</div>
|
||||
|
||||
<button
|
||||
data-tour="nouvelle-demande"
|
||||
onClick={() => setShowNewRequestModal(true)}
|
||||
className="bg-cyan-600 text-white px-3 lg:px-6 py-2 lg:py-3 rounded-lg font-medium hover:bg-cyan-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
@@ -646,10 +537,10 @@ const Dashboard = () => {
|
||||
<div className="bg-gradient-to-r from-cyan-500 to-blue-500 rounded-xl shadow-md p-6 mb-6 text-white">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-wrap gap-3 text-sm mb-3">
|
||||
<div className="flex flex-wrap gap-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Briefcase className="w-4 h-4" />
|
||||
<span className="font-medium">{detailedCounters.user.role}</span>
|
||||
<span>{detailedCounters.user.role}</span>
|
||||
</div>
|
||||
{detailedCounters.user.service && (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -666,25 +557,8 @@ const Dashboard = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-white border-opacity-20">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide opacity-90 mb-1">
|
||||
Fonction
|
||||
</p>
|
||||
<p className="text-sm font-medium">
|
||||
{detailedCounters.user.description || '(Non renseignée)'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detailedCounters.user.dateEntree && (
|
||||
<p className="text-sm mt-3 opacity-90">
|
||||
<p className="text-sm mt-2 opacity-90">
|
||||
📅 Ancienneté : {detailedCounters.user.ancienneteAnnees} an{detailedCounters.user.ancienneteAnnees > 1 ? 's' : ''} et {detailedCounters.user.ancienneteMoisRestants} mois
|
||||
{' '}(depuis le {formatDate(detailedCounters.user.dateEntree)})
|
||||
</p>
|
||||
@@ -697,237 +571,255 @@ const Dashboard = () => {
|
||||
{detailedCounters && (
|
||||
<div className="space-y-6 mb-8">
|
||||
|
||||
{/* 🆕 SECTION CÔTE À CÔTE : Demandes récentes + Congés du service */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border-2 border-cyan-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Info className="w-5 h-5 text-cyan-600" />
|
||||
Total disponible
|
||||
</h3>
|
||||
|
||||
{/* DEMANDES RÉCENTES - 1 colonne */}
|
||||
<div data-tour="demandes-recentes" className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 lg:col-span-1">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-cyan-600" />
|
||||
Mes dernières demandes
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => navigate('/demandes')}
|
||||
className="text-sm text-cyan-600 hover:text-cyan-700 flex items-center gap-1"
|
||||
>
|
||||
Voir tout
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{recentRequests.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm text-center py-4">Aucune demande récente</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{recentRequests.map((request, idx) => {
|
||||
const type = request.type || 'Congé';
|
||||
const statut = request.status || 'En attente';
|
||||
const dateDebut = request.startDate;
|
||||
const dateFin = request.endDate;
|
||||
const nbJours = request.days || 0;
|
||||
|
||||
return (
|
||||
<div key={request.id || idx} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer" onClick={() => navigate('/demandes')}>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{type}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${statut === 'Validée' ? 'bg-green-100 text-green-700' :
|
||||
statut === 'En attente' ? 'bg-orange-100 text-orange-700' :
|
||||
statut === 'Refusée' ? 'bg-red-100 text-red-700' :
|
||||
statut === 'Annulée' ? 'bg-gray-100 text-gray-700' :
|
||||
'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{statut}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{dateDebut && dateFin ? (
|
||||
<>
|
||||
Du {new Date(dateDebut).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||
{' '}au {new Date(dateFin).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' })}
|
||||
</>
|
||||
) : request.dateDisplay || (
|
||||
<span className="text-gray-400">Dates non disponibles</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-cyan-600">
|
||||
{nbJours}j
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* CONGÉS DU SERVICE - 2 colonnes (plus large) */}
|
||||
<div data-tour="conges-service" className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 lg:col-span-2">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-cyan-600" />
|
||||
Congés du service - {new Date().toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => navigate('/calendrier')}
|
||||
className="text-sm text-cyan-600 hover:text-cyan-700 flex items-center gap-1"
|
||||
>
|
||||
Voir tout
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/* ⭐ PANNEAU CONGÉS ANTICIPÉS */}
|
||||
{showAnticipes && congesAnticipes && (
|
||||
<div className="bg-gradient-to-br from-gray-50 to-slate-50 border-2 border-gray-300 rounded-lg p-6 mb-6 shadow-lg">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<TrendingUp className="w-6 h-6 text-gray-600" />
|
||||
<h4 className="text-xl font-bold text-gray-900">
|
||||
Possibilités de congés anticipés
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{teamLeaves.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm text-center py-4">Aucun congé prévu ce mois-ci</p>
|
||||
) : (
|
||||
<div className="space-y-2 overflow-y-auto" style={{ maxHeight: '400px' }}>
|
||||
{teamLeaves.map((leave, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div className={`w-3 h-3 rounded-full flex-shrink-0 ${leave.type === 'Formation' ? 'bg-blue-400' :
|
||||
leave.statut === 'Validée' ? 'bg-green-400' :
|
||||
leave.statut === 'En attente' ? 'bg-orange-400' :
|
||||
'bg-gray-400'
|
||||
}`}></div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-gray-900 truncate">{leave.employeename}</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
{new Date(leave.startdate).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||
{' '}-{' '}
|
||||
{new Date(leave.enddate).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||
</p>
|
||||
</div>
|
||||
{leave.type === 'Formation' && (
|
||||
<span className="text-xs text-blue-600 font-medium whitespace-nowrap bg-blue-50 px-2 py-1 rounded">
|
||||
📚 Formation
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
{/* CP Anticipés */}
|
||||
<div className="bg-white rounded-lg p-4 border-2 border-gray-200 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h5 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5" />
|
||||
Congés Payés
|
||||
</h5>
|
||||
{congesAnticipes.congesPayes.limiteAnticipe > 0 && (
|
||||
<span className="bg-green-100 text-green-800 text-xs font-bold px-2 py-1 rounded-full">
|
||||
Disponible
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Acquis actuellement:</span>
|
||||
<span className="font-semibold text-gray-900">
|
||||
{congesAnticipes.congesPayes.acquisActuelle}j
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Prévision fin exercice:</span>
|
||||
<span className="font-semibold text-gray-900">
|
||||
{congesAnticipes.congesPayes.acquisTotalePrevu}j
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t pt-2">
|
||||
<span className="text-gray-600">Solde actuel:</span>
|
||||
<span className="font-bold text-cyan-600">
|
||||
{congesAnticipes.congesPayes.soldeActuel}j
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between bg-gray-100 -mx-2 px-2 py-2 rounded">
|
||||
<span className="font-semibold text-gray-900">Anticipation possible:</span>
|
||||
<span className="font-bold text-gray-700 text-lg">
|
||||
{congesAnticipes.congesPayes.limiteAnticipe}j
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between bg-gradient-to-r from-gray-200 to-slate-200 -mx-2 px-2 py-2 rounded font-bold">
|
||||
<span className="text-gray-900">TOTAL DISPONIBLE:</span>
|
||||
<span className="text-gray-700 text-lg">
|
||||
{congesAnticipes.congesPayes.totalDisponible}j
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-gray-700 bg-gray-50 p-2 rounded">
|
||||
💡 {congesAnticipes.congesPayes.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RTT Anticipés */}
|
||||
{congesAnticipes.rtt && (
|
||||
<div className="bg-white rounded-lg p-4 border-2 border-gray-200 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h5 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
RTT
|
||||
</h5>
|
||||
{congesAnticipes.rtt.limiteAnticipe > 0 && (
|
||||
<span className="bg-green-100 text-green-800 text-xs font-bold px-2 py-1 rounded-full">
|
||||
Disponible
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Acquis actuellement:</span>
|
||||
<span className="font-semibold text-gray-900">
|
||||
{congesAnticipes.rtt.acquisActuelle}j
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Prévision fin année:</span>
|
||||
<span className="font-semibold text-gray-900">
|
||||
{congesAnticipes.rtt.acquisTotalePrevu}j
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t pt-2">
|
||||
<span className="text-gray-600">Solde actuel:</span>
|
||||
<span className="font-bold text-green-600">
|
||||
{congesAnticipes.rtt.soldeActuel}j
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between bg-gray-100 -mx-2 px-2 py-2 rounded">
|
||||
<span className="font-semibold text-gray-900">Anticipation possible:</span>
|
||||
<span className="font-bold text-gray-700 text-lg">
|
||||
{congesAnticipes.rtt.limiteAnticipe}j
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between bg-gradient-to-r from-gray-200 to-slate-200 -mx-2 px-2 py-2 rounded font-bold">
|
||||
<span className="text-gray-900">TOTAL DISPONIBLE:</span>
|
||||
<span className="text-gray-700 text-lg">
|
||||
{congesAnticipes.rtt.totalDisponible}j
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-gray-700 bg-gray-50 p-2 rounded">
|
||||
💡 {congesAnticipes.rtt.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Règles d'anticipation */}
|
||||
<div className="bg-gray-100 border border-gray-300 rounded-lg p-4">
|
||||
<h5 className="font-semibold text-gray-900 mb-2 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
Règles d'anticipation
|
||||
</h5>
|
||||
<ul className="text-sm text-gray-800 space-y-1">
|
||||
<li>• CP : Maximum {congesAnticipes.regles.cpMaxAnnuel}j par exercice (jusqu'au 31 mai)</li>
|
||||
<li>• RTT : Maximum {congesAnticipes.regles.rttMaxAnnuel}j par an (jusqu'au 31 décembre)</li>
|
||||
<li>• L'anticipation est basée sur l'acquisition future prévue</li>
|
||||
<li>• Vous pouvez poser des congés avant de les avoir acquis</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Soldes actuels */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-cyan-50 rounded-lg p-4">
|
||||
<p className="text-sm text-cyan-700 mb-1">Congés Payés</p>
|
||||
<p className="text-3xl font-bold text-cyan-900">
|
||||
{detailedCounters.totalDisponible.cp.toFixed(2)}
|
||||
<span className="text-lg">j</span>
|
||||
</p>
|
||||
<p className="text-xs text-cyan-600 mt-1">
|
||||
N-1: {(detailedCounters.cpN1?.solde || 0).toFixed(2)}j +
|
||||
N: {(detailedCounters.cpN?.solde || 0).toFixed(2)}j
|
||||
</p>
|
||||
</div>
|
||||
{detailedCounters.user.role !== 'Apprenti' && (
|
||||
<div className="bg-green-50 rounded-lg p-4">
|
||||
<p className="text-sm text-green-700 mb-1">RTT</p>
|
||||
<p className="text-3xl font-bold text-green-900">
|
||||
{detailedCounters.totalDisponible.rtt.toFixed(2)}
|
||||
<span className="text-lg">j</span>
|
||||
</p>
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
Année {detailedCounters.anneeRTT} • {getTypeContratLabel(detailedCounters.user.typeContrat)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{detailedCounters.recupN && detailedCounters.recupN.solde > 0 && (
|
||||
<div className="bg-purple-50 rounded-lg p-4">
|
||||
<p className="text-sm text-purple-700 mb-1">Récupération</p>
|
||||
<p className="text-3xl font-bold text-purple-900">
|
||||
{detailedCounters.recupN.solde.toFixed(2)}
|
||||
<span className="text-lg">j</span>
|
||||
</p>
|
||||
<p className="text-xs text-purple-600 mt-1">
|
||||
Samedis travaillés
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reste du code identique... */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
{/* RACCOURCI COMPTE-RENDU ACTIVITÉS (si forfait jour) */}
|
||||
{(user?.TypeContrat === 'forfait_jour' || user?.typeContrat === 'forfait_jour') && (
|
||||
<div
|
||||
onClick={() => navigate('/compte-rendu-activites')}
|
||||
className="bg-gradient-to-r from-purple-500 to-purple-600 rounded-xl shadow-md p-6 text-white cursor-pointer hover:shadow-lg transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-2 flex items-center gap-2">
|
||||
<FileText className="w-6 h-6" />
|
||||
Compte-Rendu d'Activités
|
||||
</h3>
|
||||
<p className="text-sm opacity-90">
|
||||
Suivez vos jours travaillés et repos obligatoires
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="w-8 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CARTES DES COMPTEURS (CP N-1, CP N, RTT, Récup) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{detailedCounters.cpN1 && (
|
||||
<div data-tour="cp-n-1" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||
{isUpdatingCounters && (
|
||||
<div className="absolute top-2 right-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full animate-pulse z-10">
|
||||
🔄 Mise à jour...
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-4 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold">CP N-1</h3>
|
||||
<p className="text-sm opacity-90">
|
||||
Congés acquis du 1er juin {parseInt(detailedCounters.cpN1.exercice.split('-')[0])} au 31 mai {parseInt(detailedCounters.cpN1.exercice.split('-')[1])}
|
||||
</p>
|
||||
<p className="text-sm opacity-90">Reporté | Exercice {detailedCounters.cpN1.exercice}</p>
|
||||
</div>
|
||||
<Calendar className="w-8 h-8 opacity-80" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="flex justify-between items-center py-3 bg-blue-50 rounded-lg px-3 mt-3">
|
||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||
<span className="text-2xl font-bold text-blue-600">{detailedCounters.cpN1.solde.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Acquis </span>
|
||||
<span className="text-sm font-medium text-gray-600">Reporté initial</span>
|
||||
<span className="text-lg font-bold text-gray-900">{detailedCounters.cpN1.reporte.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Consommé(s)</span>
|
||||
<span className="text-sm font-medium text-gray-600">Consommé</span>
|
||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.cpN1.pris.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg mt-2">
|
||||
<AlertCircle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-amber-800">
|
||||
<strong>À solder avant le 31/05/{parseInt(detailedCounters.cpN1.exercice.split('-')[1]) + 1}</strong>
|
||||
</p>
|
||||
<div className="flex justify-between items-center py-3 bg-blue-50 rounded-lg px-3 mt-3">
|
||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||
<span className="text-2xl font-bold text-blue-600">{detailedCounters.cpN1.solde.toFixed(2)}j</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{detailedCounters.cpN && (
|
||||
<div data-tour="cp-n" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||
{isUpdatingCounters && (
|
||||
<div className="absolute top-2 right-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full animate-pulse z-10">
|
||||
🔄 Mise à jour...
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-cyan-500 to-cyan-600 p-4 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold">CP N</h3>
|
||||
<p className="text-sm opacity-90">
|
||||
Congés acquis du 1er juin {parseInt(detailedCounters.cpN.exercice.split('-')[0])} au 31 mai {parseInt(detailedCounters.cpN.exercice.split('-')[1])}
|
||||
</p>
|
||||
<p className="text-sm opacity-90">Exercice {detailedCounters.cpN.exercice}</p>
|
||||
</div>
|
||||
<Calendar className="w-8 h-8 opacity-80" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Acquis ({detailedCounters.cpN.moisTravailles.toFixed(1)}/12)</span>
|
||||
<span className="text-lg font-bold text-green-600">+{detailedCounters.cpN.acquis.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Consommé</span>
|
||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.cpN.pris.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-3 bg-cyan-50 rounded-lg px-3 mt-3">
|
||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||
<span className="text-2xl font-bold text-cyan-600">{detailedCounters.cpN.solde.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Acquis </span>
|
||||
<span className="text-lg font-bold text-green-600">+{detailedCounters.cpN.acquis.toFixed(2)}j</span>
|
||||
<div className="text-xs text-gray-500 pt-2">
|
||||
Reste à acquérir : {detailedCounters.cpN.joursRestantsAAcquerir.toFixed(2)}j
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Consommé(s)</span>
|
||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.cpN.pris.toFixed(2)}j</span>
|
||||
</div>
|
||||
|
||||
{detailedCounters.cpN.solde > 0 && (
|
||||
<div className="flex items-start gap-2 p-3 bg-blue-50 border border-blue-200 rounded-lg mt-2">
|
||||
<Info className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-blue-800">
|
||||
<strong>À solder avant le 31/05/{parseInt(detailedCounters.cpN.exercice.split('-')[1]) + 1}</strong>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{detailedCounters.rttN && detailedCounters.user.role !== 'Apprenti' && (
|
||||
<div data-tour="rtt" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||
{isUpdatingCounters && (
|
||||
<div className="absolute top-2 right-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full animate-pulse z-10">
|
||||
🔄 Mise à jour...
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-green-500 to-green-600 p-4 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -940,58 +832,49 @@ const Dashboard = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Acquis ({detailedCounters.rttN.moisTravailles.toFixed(1)}/12)</span>
|
||||
<span className="text-lg font-bold text-green-600">+{detailedCounters.rttN.acquis.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Consommé</span>
|
||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.rttN.pris.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-3 bg-green-50 rounded-lg px-3 mt-3">
|
||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||
<span className="text-2xl font-bold text-green-600">{detailedCounters.rttN.solde.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Acquis </span>
|
||||
<span className="text-lg font-bold text-green-600">+{detailedCounters.rttN.acquis.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Consommé(s)</span>
|
||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.rttN.pris.toFixed(2)}j</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 p-3 bg-green-50 border border-green-200 rounded-lg mt-2">
|
||||
<Info className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-green-800">
|
||||
<strong>À consommer avant le 31/12/{detailedCounters.rttN.annee}</strong>
|
||||
</p>
|
||||
<div className="text-xs text-gray-500 pt-2">
|
||||
Reste à acquérir : {detailedCounters.rttN.joursRestantsAAcquerir.toFixed(2)}j
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{detailedCounters.recupN && (
|
||||
<div data-tour="recup" className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden relative">
|
||||
{isUpdatingCounters && (
|
||||
<div className="absolute top-2 right-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded-full animate-pulse z-10">
|
||||
🔄 Mise à jour...
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-purple-500 to-purple-600 p-4 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold">Récupérations {detailedCounters.recupN.annee}</h3>
|
||||
<h3 className="text-lg font-bold">Récupération {detailedCounters.recupN.annee}</h3>
|
||||
<p className="text-sm opacity-90">Samedis travaillés</p>
|
||||
</div>
|
||||
<Calendar className="w-8 h-8 opacity-80" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="flex justify-between items-center py-3 bg-purple-50 rounded-lg px-3 mt-3">
|
||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||
<span className="text-2xl font-bold text-purple-600">{detailedCounters.recupN.solde.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Jours accumulés</span>
|
||||
<span className="text-lg font-bold text-green-600">+{detailedCounters.recupN.acquis.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-sm font-medium text-gray-600">Consommé(s)</span>
|
||||
<span className="text-sm font-medium text-gray-600">Consommé</span>
|
||||
<span className="text-lg font-bold text-red-600">-{detailedCounters.recupN.pris.toFixed(2)}j</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-3 bg-purple-50 rounded-lg px-3 mt-3">
|
||||
<span className="text-base font-semibold text-gray-700">Solde disponible</span>
|
||||
<span className="text-2xl font-bold text-purple-600">{detailedCounters.recupN.solde.toFixed(2)}j</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 pt-2">
|
||||
{detailedCounters.recupN.message}
|
||||
</div>
|
||||
@@ -999,21 +882,18 @@ const Dashboard = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{detailedCounters.user.role === 'Apprenti' && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>ℹ️ Information :</strong> En tant qu'apprenti, vous ne bénéficiez pas de jours RTT.
|
||||
Vous disposez uniquement de vos congés payés.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 🎯 BOUTON AIDE POUR RELANCER LE TUTORIEL */}
|
||||
<button
|
||||
onClick={handleRestartTutorial}
|
||||
className="fixed bottom-20 right-4 bg-cyan-600 text-white shadow-lg hover:bg-cyan-700 transition-all flex items-center gap-2 z-40 group overflow-hidden rounded-full hover:rounded-lg hover:px-4 px-3 py-3"
|
||||
title="Relancer le tutoriel"
|
||||
>
|
||||
<HelpCircle className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="max-w-0 group-hover:max-w-xs overflow-hidden transition-all duration-300 text-sm font-medium whitespace-nowrap">
|
||||
Aide
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showNewRequestModal && detailedCounters && (
|
||||
<NewLeaveRequestModal
|
||||
onClose={() => setShowNewRequestModal(false)}
|
||||
@@ -1026,10 +906,9 @@ const Dashboard = () => {
|
||||
availableRTT_N1: 0,
|
||||
availableABS: 0,
|
||||
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
||||
availableRTT: detailedCounters.rttN?.solde || 0,
|
||||
availableRecup: detailedCounters?.recupN?.solde || 0,
|
||||
availableRecup_N: detailedCounters?.recupN?.solde || 0
|
||||
availableRTT: detailedCounters.rttN?.solde || 0
|
||||
}}
|
||||
// ⭐ PASSER LES DONNÉES D'ANTICIPATION
|
||||
congesAnticipes={congesAnticipes}
|
||||
accessToken={graphToken}
|
||||
userId={userId}
|
||||
|
||||
@@ -19,7 +19,7 @@ const EmployeeDetails = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
// 1️⃣ Données employé (avec compteurs inclus)
|
||||
const resEmployee = await fetch(`/getEmploye?id=${id}`);
|
||||
const resEmployee = await fetch(`http://localhost:3000/getEmploye?id=${id}`);
|
||||
const dataEmployee = await resEmployee.json();
|
||||
console.log("Réponse API employé:", dataEmployee);
|
||||
|
||||
@@ -32,7 +32,7 @@ const EmployeeDetails = () => {
|
||||
setEmployee(dataEmployee.employee);
|
||||
|
||||
// 2️⃣ Historique des demandes
|
||||
const resRequests = await fetch(`/getEmployeRequest?id=${id}`);
|
||||
const resRequests = await fetch(`http://localhost:3000/getEmployeRequest?id=${id}`);
|
||||
const dataRequests = await resRequests.json();
|
||||
|
||||
if (dataRequests.success) {
|
||||
|
||||
0
project/src/pages/LeaveScheduling.jsx
Normal file
0
project/src/pages/LeaveScheduling.jsx
Normal file
@@ -48,6 +48,7 @@ const Login = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirection vers le dashboard
|
||||
navigate('/dashboard');
|
||||
|
||||
} catch (error) {
|
||||
@@ -66,6 +67,7 @@ const Login = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex flex-col lg:flex-row">
|
||||
{/* Image côté gauche */}
|
||||
<div className="h-32 lg:h-auto lg:flex lg:w-1/2 bg-cover bg-center"
|
||||
@@ -89,9 +91,8 @@ const Login = () => {
|
||||
GESTION DES TEMPS ET DES ACTIVITÉS
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bouton Office 365 */}
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<button
|
||||
data-testid="o365-login-btn"
|
||||
onClick={handleO365Login}
|
||||
@@ -112,63 +113,6 @@ const Login = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Séparateur */}
|
||||
<div className="flex items-center my-4">
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
<span className="px-3 text-xs text-gray-500">ou connexion locale</span>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
|
||||
{/* Formulaire local email/mot de passe */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email professionnel
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="prenom.nom@ensup.fr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Mot de passe
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 pr-10"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 px-3 text-xs text-gray-500"
|
||||
>
|
||||
{showPassword ? 'Masquer' : 'Afficher'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading && authMethod === 'local'}
|
||||
className="w-full bg-indigo-600 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading && authMethod === 'local'
|
||||
? 'Connexion...'
|
||||
: 'Se connecter'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Message d'erreur */}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg mt-4">
|
||||
@@ -191,8 +135,8 @@ const Login = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default Login;
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Sidebar from "../components/Sidebar";
|
||||
import GlobalTutorial from '../components/GlobalTutorial';
|
||||
import {
|
||||
Users,
|
||||
CheckCircle,
|
||||
@@ -31,7 +30,6 @@ const Manager = () => {
|
||||
const [comment, setComment] = useState("");
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.id) fetchTeamData();
|
||||
}, [user]);
|
||||
@@ -53,7 +51,7 @@ const Manager = () => {
|
||||
|
||||
const fetchTeamMembers = async () => {
|
||||
try {
|
||||
const res = await fetch(`/getTeamMembers?manager_id=${user.id}`);
|
||||
const res = await fetch(`http://localhost:3000/getTeamMembers?manager_id=${user.id}`);
|
||||
const data = await res.json();
|
||||
if (data.success) setTeamMembers(data.team_members || []);
|
||||
else setTeamMembers([]);
|
||||
@@ -64,7 +62,7 @@ const Manager = () => {
|
||||
|
||||
const fetchPendingRequests = async () => {
|
||||
try {
|
||||
const res = await fetch(`/getPendingRequests?manager_id=${user.id}`);
|
||||
const res = await fetch(`http://localhost:3000/getPendingRequests?manager_id=${user.id}`);
|
||||
const data = await res.json();
|
||||
if (data.success) setPendingRequests(data.requests || []);
|
||||
else setPendingRequests([]);
|
||||
@@ -75,7 +73,7 @@ const Manager = () => {
|
||||
|
||||
const fetchAllTeamRequests = async () => {
|
||||
try {
|
||||
const res = await fetch(`/getAllTeamRequests?SuperieurId=${user.id}`);
|
||||
const res = await fetch(`http://localhost:3000/getAllTeamRequests?SuperieurId=${user.id}`);
|
||||
const data = await res.json();
|
||||
if (data.success) setAllRequests(data.requests || []);
|
||||
else setAllRequests([]);
|
||||
@@ -115,7 +113,7 @@ const Manager = () => {
|
||||
try {
|
||||
setIsValidating(true); // ✅ Maintenant défini
|
||||
|
||||
const response = await fetch('/validateRequest', {
|
||||
const response = await fetch('http://localhost:3000/validateRequest', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -355,7 +353,7 @@ const Manager = () => {
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{!isEmployee && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100" data-tour="demandes-attente">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-yellow-600" />
|
||||
<h2 className="font-semibold text-gray-900">Demandes en attente ({pendingRequests.length})</h2>
|
||||
@@ -384,14 +382,14 @@ const Manager = () => {
|
||||
<button
|
||||
onClick={() => openValidationModal(r, "approve")}
|
||||
className="flex-1 bg-green-600 text-white px-3 py-2 rounded-lg hover:bg-green-700 text-sm"
|
||||
data-tour="approuver-btn" >
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 inline mr-1" />
|
||||
Approuver
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openValidationModal(r, "reject")}
|
||||
className="flex-1 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 text-sm"
|
||||
data-tour="refuser-btn" >
|
||||
>
|
||||
<XCircle className="w-4 h-4 inline mr-1" />
|
||||
Refuser
|
||||
</button>
|
||||
@@ -403,7 +401,7 @@ const Manager = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 ${isEmployee ? "lg:col-span-2" : ""}`} data-tour="mon-equipe">
|
||||
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 ${isEmployee ? "lg:col-span-2" : ""}`}>
|
||||
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="font-semibold text-gray-900">Mon équipe ({teamMembers.length})</h2>
|
||||
@@ -417,7 +415,7 @@ const Manager = () => {
|
||||
key={m.id}
|
||||
onClick={() => navigate(`/employee/${m.id}`)}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer transition"
|
||||
data-tour="membre-equipe" >
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-blue-600 font-medium text-sm">
|
||||
@@ -447,7 +445,7 @@ const Manager = () => {
|
||||
</div>
|
||||
|
||||
{!isEmployee && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 mt-6" data-tour="historique-demandes">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 mt-6">
|
||||
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-gray-600" />
|
||||
<h2 className="font-semibold text-gray-900">Historique des demandes ({allRequests.length})</h2>
|
||||
@@ -475,10 +473,10 @@ const Manager = () => {
|
||||
</p>
|
||||
)}
|
||||
{r.file && (
|
||||
<div className="text-sm mt-1" data-tour="document-joint">
|
||||
<div className="text-sm mt-1">
|
||||
<p className="text-gray-500">Document joint</p>
|
||||
<a
|
||||
href={`/uploads/${r.file}`}
|
||||
href={`http://localhost:3000/uploads/${r.file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline flex items-center gap-1 mt-1"
|
||||
@@ -495,8 +493,7 @@ const Manager = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<GlobalTutorial userId={user?.id} userRole={user?.role} />
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,8 +6,6 @@ import NewLeaveRequestModal from '../components/NewLeaveRequestModal';
|
||||
import EditLeaveRequestModal from '../components/EditLeaveRequestModal';
|
||||
import { useMsal } from "@azure/msal-react";
|
||||
import MedicalDocuments from '../components/MedicalDocuments';
|
||||
import Joyride, { STATUS } from 'react-joyride';
|
||||
|
||||
|
||||
const Requests = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -47,55 +45,6 @@ const Requests = () => {
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [requestToDelete, setRequestToDelete] = useState(null);
|
||||
|
||||
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// 🎯 STATES POUR LE TUTORIEL
|
||||
const [runTour, setRunTour] = useState(false);
|
||||
|
||||
|
||||
|
||||
// 🎯 DÉCLENCHER LE TUTORIEL À CHAQUE FOIS
|
||||
useEffect(() => {
|
||||
if (userId && !isLoading) {
|
||||
setTimeout(() => setRunTour(true), 1500);
|
||||
}
|
||||
}, [userId, isLoading]);
|
||||
|
||||
// 🎯 DÉFINITION DES ÉTAPES DU TUTORIEL
|
||||
const tourSteps = [
|
||||
{
|
||||
target: '[data-tour="nouvelle-demande"]',
|
||||
content: '➕ Créez une nouvelle demande de congé en cliquant ici.',
|
||||
placement: 'bottom',
|
||||
},
|
||||
{
|
||||
target: '[data-tour="recherche"]',
|
||||
content: '🔍 Recherchez vos demandes par type ou statut.',
|
||||
placement: 'bottom',
|
||||
},
|
||||
{
|
||||
target: '[data-tour="filtres"]',
|
||||
content: '🎯 Filtrez vos demandes par statut ou type de congé.',
|
||||
placement: 'bottom',
|
||||
},
|
||||
{
|
||||
target: '[data-tour="liste-demandes"]',
|
||||
content: '📋 Consultez la liste de toutes vos demandes ici.',
|
||||
placement: 'top',
|
||||
},
|
||||
];
|
||||
|
||||
// 🎯 GÉRER LA FIN DU TOUR
|
||||
const handleJoyrideCallback = (data) => {
|
||||
const { status } = data;
|
||||
const finishedStatuses = [STATUS.FINISHED, STATUS.SKIPPED];
|
||||
if (finishedStatuses.includes(status)) {
|
||||
setRunTour(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (accounts.length > 0) {
|
||||
const request = {
|
||||
@@ -115,7 +64,7 @@ const Requests = () => {
|
||||
|
||||
const fetchDetailedCounters = async () => {
|
||||
try {
|
||||
const response = await fetch(`/getDetailedLeaveCounters?user_id=${userId}`);
|
||||
const response = await fetch(`http://localhost:3000/getDetailedLeaveCounters?user_id=${userId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -130,7 +79,7 @@ const Requests = () => {
|
||||
|
||||
const fetchAllRequests = async () => {
|
||||
try {
|
||||
const url = `/getRequests?user_id=${userId}`;
|
||||
const url = `http://localhost:3000/getRequests?user_id=${userId}`;
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
let data;
|
||||
@@ -195,157 +144,56 @@ const Requests = () => {
|
||||
};
|
||||
|
||||
// ⭐ NOUVELLE FONCTION : Supprimer une demande
|
||||
// ⭐ NOUVELLE FONCTION : Annuler une demande (En attente OU Validée, si date future)
|
||||
const handleDeleteRequest = async (requestId) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
console.log('🗑️ Début annulation, ID:', requestId);
|
||||
|
||||
// Chercher la demande dans allRequests
|
||||
let request = allRequests.find(r => r.id === requestId);
|
||||
|
||||
if (!request) {
|
||||
console.log('⚠️ Demande non trouvée dans l\'état local, récupération via API...');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/getRequests?user_id=${user.id}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.requests) {
|
||||
request = result.requests.find(r => r.id === requestId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erreur récupération demande:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!request) {
|
||||
showToast('❌ Demande introuvable', 'error');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📋 Demande trouvée:', request);
|
||||
|
||||
// Vérifier la date de début
|
||||
const dateDebut = new Date(request.startDate);
|
||||
const aujourdhui = new Date();
|
||||
aujourdhui.setHours(0, 0, 0, 0);
|
||||
dateDebut.setHours(0, 0, 0, 0);
|
||||
|
||||
console.log('📅 Date début:', dateDebut.toLocaleDateString('fr-FR'));
|
||||
console.log('📅 Aujourd\'hui:', aujourdhui.toLocaleDateString('fr-FR'));
|
||||
|
||||
if (dateDebut <= aujourdhui) {
|
||||
showToast(
|
||||
`❌ Impossible d'annuler : la date de début (${dateDebut.toLocaleDateString('fr-FR')}) est déjà passée ou c'est aujourd'hui`,
|
||||
'error'
|
||||
);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// ⭐ CONFIRMATION AVEC MODAL AU LIEU DE alert()
|
||||
setRequestToDelete(request);
|
||||
setShowDeleteConfirm(true);
|
||||
setIsLoading(false);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur annulation:', error);
|
||||
showToast(`Erreur: ${error.message}`, 'error');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
// Fonction helper pour formater les dates
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('fr-FR');
|
||||
const handleDeleteRequest = (request) => {
|
||||
setRequestToDelete(request);
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
|
||||
// ⭐ NOUVELLE FONCTION : Confirmer la suppression (sans setDeletedRequests)
|
||||
// ⭐ NOUVELLE FONCTION : Confirmer la suppression
|
||||
const confirmDeleteRequest = async () => {
|
||||
if (!requestToDelete) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const requestData = {
|
||||
requestId: requestToDelete.id,
|
||||
userId: userId,
|
||||
userEmail: user.email,
|
||||
userName: `${user.prenom} ${user.nom}`,
|
||||
accessToken: graphToken
|
||||
};
|
||||
|
||||
console.log('📤 Envoi requête:', requestData);
|
||||
|
||||
const response = await fetch('/deleteRequest', {
|
||||
const response = await fetch('http://localhost:3000/deleteRequest', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestData)
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requestId: requestToDelete.id,
|
||||
userId: userId,
|
||||
userEmail: user.email,
|
||||
userName: `${user.prenom} ${user.nom}`,
|
||||
accessToken: graphToken
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const result = await response.json();
|
||||
console.log('📥 Réponse API:', result);
|
||||
|
||||
if (result.success) {
|
||||
// ✅ Succès
|
||||
showToast('✅ Demande annulée avec succès', 'success');
|
||||
|
||||
if (result.counterRestored && result.repartition) {
|
||||
// Afficher les détails de la restauration
|
||||
const repartitionText = result.repartition
|
||||
.map(r => `${r.type}: ${r.jours}j${r.periode !== 'Journée entière' ? ` (${r.periode})` : ''}`)
|
||||
.join(', ');
|
||||
|
||||
showToast(`📊 Compteurs restaurés: ${repartitionText}`, 'info');
|
||||
if (data.success) {
|
||||
showToast('✅ Demande supprimée avec succès', 'success');
|
||||
refreshAllData();
|
||||
setShowDeleteConfirm(false);
|
||||
setRequestToDelete(null);
|
||||
if (selectedRequest?.id === requestToDelete.id) {
|
||||
setSelectedRequest(null);
|
||||
}
|
||||
|
||||
if (result.emailsSent) {
|
||||
if (result.emailsSent.collaborateur) {
|
||||
showToast('📧 Email de confirmation envoyé', 'info');
|
||||
}
|
||||
if (result.emailsSent.manager) {
|
||||
showToast('📧 Manager notifié par email', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// ⭐ Rafraîchir les données
|
||||
await refreshAllData();
|
||||
|
||||
} else {
|
||||
// ❌ Erreur
|
||||
showToast(result.message || 'Erreur lors de l\'annulation', 'error');
|
||||
showToast(`❌ Erreur : ${data.message}`, 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur annulation:', error);
|
||||
showToast(`Erreur serveur: ${error.message}`, 'error');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setShowDeleteConfirm(false);
|
||||
setRequestToDelete(null);
|
||||
|
||||
// Fermer les détails si c'était la demande affichée
|
||||
if (selectedRequest?.id === requestToDelete?.id) {
|
||||
setSelectedRequest(null);
|
||||
}
|
||||
console.error('Erreur suppression:', error);
|
||||
showToast('❌ Erreur lors de la suppression', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Connexion SSE
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
console.log('🔌 Connexion SSE au serveur collaborateurs...');
|
||||
|
||||
const eventSource = new EventSource(`/api/events/collaborateur?user_id=${userId}`);
|
||||
const eventSource = new EventSource(`http://localhost:3000/api/events/collaborateur?user_id=${userId}`);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('✅ SSE connecté');
|
||||
@@ -427,7 +275,7 @@ const Requests = () => {
|
||||
|
||||
setFilteredRequests(filtered);
|
||||
setCurrentPage(1);
|
||||
}, [allRequests, searchTerm, statusFilter, typeFilter]);
|
||||
}, [allRequests, searchTerm, statusFilter, typeFilter]);
|
||||
|
||||
const indexOfLastRequest = currentPage * requestsPerPage;
|
||||
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
|
||||
@@ -441,7 +289,6 @@ const Requests = () => {
|
||||
case 'En attente': return 'bg-yellow-100 text-yellow-800';
|
||||
case 'Validée': return 'bg-green-100 text-green-800';
|
||||
case 'Refusée': return 'bg-red-100 text-red-800';
|
||||
case 'Annulée': return 'bg-gray-100 text-gray-800'; // ⭐ AJOUT
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
@@ -460,32 +307,6 @@ const Requests = () => {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
|
||||
{/* 🎯 TUTORIEL INTERACTIF */}
|
||||
<Joyride
|
||||
steps={tourSteps}
|
||||
run={runTour}
|
||||
continuous
|
||||
showProgress={false}
|
||||
showSkipButton
|
||||
callback={handleJoyrideCallback}
|
||||
styles={{ options: { primaryColor: '#0891b2', zIndex: 10000 } }}
|
||||
tooltipComponent={({ continuous, index, step, backProps, primaryProps, skipProps, tooltipProps, size }) => (
|
||||
<div {...tooltipProps} style={{ backgroundColor: 'white', borderRadius: '12px', padding: '20px', maxWidth: '350px', boxShadow: '0 10px 25px rgba(0,0,0,0.15)', fontSize: '14px' }}>
|
||||
<div style={{ marginBottom: '15px', color: '#374151' }}>{step.content}</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: '6px 12px', borderRadius: '6px', border: '1px solid #d1d5db', backgroundColor: 'white', color: '#6b7280', cursor: 'pointer', fontSize: '13px', fontWeight: '500' }}>Retour</button>}
|
||||
{continuous && index < size - 1 && <button {...primaryProps} style={{ padding: '6px 16px', borderRadius: '6px', border: 'none', backgroundColor: '#0891b2', color: 'white', cursor: 'pointer', fontSize: '13px', fontWeight: '500' }}>Suivant</button>}
|
||||
{(!continuous || index === size - 1) && <button {...primaryProps} style={{ padding: '6px 16px', borderRadius: '6px', border: 'none', backgroundColor: '#0891b2', color: 'white', cursor: 'pointer', fontSize: '13px', fontWeight: '500' }}>Terminer</button>}
|
||||
<button {...skipProps} style={{ padding: '6px 10px', borderRadius: '6px', border: 'none', backgroundColor: 'transparent', color: '#9ca3af', cursor: 'pointer', fontSize: '12px' }}>Passer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
||||
|
||||
{/* Toast container */}
|
||||
@@ -506,72 +327,30 @@ const Requests = () => {
|
||||
</div>
|
||||
|
||||
{/* Modal de confirmation de suppression */}
|
||||
{showDeleteConfirm && requestToDelete && (
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50" onClick={() => !isSubmitting && setShowDeleteConfirm(false)}></div>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50" onClick={() => setShowDeleteConfirm(false)}></div>
|
||||
<div className="relative bg-white rounded-xl shadow-xl p-6 max-w-md w-full mx-4">
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-900">
|
||||
⚠️ Confirmer l'annulation
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3 mb-6">
|
||||
<p className="text-gray-700">
|
||||
Voulez-vous annuler cette demande de congé ?
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Type :</span>
|
||||
<span className="font-medium">{requestToDelete.type}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Période :</span>
|
||||
<span className="font-medium">{requestToDelete.dateDisplay}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Durée :</span>
|
||||
<span className="font-medium">{requestToDelete.days} jour(s)</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Statut :</span>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${getStatusColor(requestToDelete.status)}`}>
|
||||
{requestToDelete.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(requestToDelete.status === 'Validée' || requestToDelete.status === 'Validé') && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm text-blue-800">
|
||||
<p className="font-medium">ℹ️ Information</p>
|
||||
<p className="mt-1">Cette demande a été validée. Vos compteurs seront automatiquement restaurés.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold mb-4">Confirmer la suppression</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Êtes-vous sûr de vouloir supprimer cette demande ?
|
||||
<br /><strong>Type :</strong> {requestToDelete?.type}
|
||||
<br /><strong>Dates :</strong> {requestToDelete?.dateDisplay}
|
||||
<br /><br />
|
||||
<span className="text-sm text-gray-500">Un email sera envoyé à votre manager pour l'informer.</span>
|
||||
</p>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50"
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmDeleteRequest}
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 disabled:opacity-50 flex items-center gap-2"
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
Annulation...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Confirmer l'annulation
|
||||
</>
|
||||
)}
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -601,7 +380,6 @@ const Requests = () => {
|
||||
<RefreshCw className={`w-5 h-5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
data-tour="nouvelle-demande"
|
||||
onClick={() => setShowNewRequestModal(true)}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-700 text-sm lg:text-base"
|
||||
>
|
||||
@@ -611,7 +389,50 @@ const Requests = () => {
|
||||
</div>
|
||||
|
||||
{/* Compteurs */}
|
||||
|
||||
{detailedCounters && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{/* CP N */}
|
||||
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-sm font-medium text-gray-500">CP Année N</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900">{detailedCounters.cpN?.solde?.toFixed(1) || '0.0'}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Sur {detailedCounters.cpN?.acquis?.toFixed(1) || '0.0'} acquis</p>
|
||||
</div>
|
||||
|
||||
{/* CP N-1 */}
|
||||
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-sm font-medium text-gray-500">CP Année N-1</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900">{detailedCounters.cpN1?.solde?.toFixed(1) || '0.0'}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Sur {detailedCounters.cpN1?.acquis?.toFixed(1) || '0.0'} acquis</p>
|
||||
</div>
|
||||
|
||||
{/* RTT N */}
|
||||
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-sm font-medium text-gray-500">RTT Année N</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900">{detailedCounters.rttN?.solde?.toFixed(1) || '0.0'}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Sur {detailedCounters.rttN?.acquis?.toFixed(1) || '0.0'} acquis</p>
|
||||
</div>
|
||||
|
||||
{/* Total disponible */}
|
||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 p-4 rounded-xl shadow-sm text-white">
|
||||
<h3 className="text-sm font-medium opacity-90 mb-2">Total disponible</h3>
|
||||
<p className="text-2xl font-bold">
|
||||
{(
|
||||
(detailedCounters.cpN?.solde || 0) +
|
||||
(detailedCounters.cpN1?.solde || 0) +
|
||||
(detailedCounters.rttN?.solde || 0)
|
||||
).toFixed(1)}
|
||||
</p>
|
||||
<p className="text-xs opacity-75 mt-1">Jours ouvrés</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left: list */}
|
||||
@@ -620,7 +441,7 @@ const Requests = () => {
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-4">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex-1 relative" data-tour="recherche">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="text"
|
||||
@@ -632,7 +453,6 @@ const Requests = () => {
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
data-tour="filtres"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
@@ -652,7 +472,6 @@ const Requests = () => {
|
||||
<option value="En attente">En attente</option>
|
||||
<option value="Validée">Validée</option>
|
||||
<option value="Refusée">Refusée</option>
|
||||
<option value="Annulée">Annulée</option>
|
||||
</select>
|
||||
<select
|
||||
value={typeFilter}
|
||||
@@ -660,10 +479,10 @@ const Requests = () => {
|
||||
className="px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">Tous les types</option>
|
||||
<option value="Congé payé">Congé(s) payé(s)</option>
|
||||
<option value="Congé payé">Congé payé</option>
|
||||
<option value="RTT">RTT</option>
|
||||
<option value="Arrêt maladie">Arrêt maladie</option>
|
||||
<option value="Récupération">Récupération</option>
|
||||
<option value="Absence">Absence</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
@@ -676,13 +495,13 @@ const Requests = () => {
|
||||
<p className="text-gray-500">Chargement...</p>
|
||||
</div>
|
||||
) : currentRequests.length === 0 ? (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 text-center" >
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 text-center">
|
||||
<Info className="w-12 h-12 mx-auto text-gray-300 mb-3" />
|
||||
<p className="text-gray-500">Aucune demande trouvée</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3" data-tour="liste-demandes">
|
||||
<div className="space-y-3">
|
||||
{currentRequests.map((request) => (
|
||||
<div key={request.id} className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
@@ -715,16 +534,14 @@ const Requests = () => {
|
||||
)}
|
||||
|
||||
{/* Bouton Supprimer */}
|
||||
{request.status === 'En attente' && (
|
||||
<button
|
||||
onClick={() => handleDeleteRequest(request.id)}
|
||||
className="text-orange-600 hover:text-orange-700 flex items-center gap-1 px-2 py-1 hover:bg-orange-50 rounded"
|
||||
title="Annuler"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Annuler</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDeleteRequest(request)}
|
||||
className="text-red-600 hover:text-red-700 flex items-center gap-1 px-2 py-1 hover:bg-red-50 rounded"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Supprimer</span>
|
||||
</button>
|
||||
|
||||
{/* Bouton Voir */}
|
||||
<button
|
||||
@@ -820,11 +637,11 @@ const Requests = () => {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDeleteRequest(selectedRequest.id)}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"
|
||||
onClick={() => handleDeleteRequest(selectedRequest)}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
Annuler cette demande
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Supprimer cette demande
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -850,9 +667,7 @@ const Requests = () => {
|
||||
availableRTT_N1: 0,
|
||||
availableABS: 0,
|
||||
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
||||
availableRTT: detailedCounters.rttN?.solde || 0,
|
||||
availableRecup: detailedCounters?.recupN?.solde || 0,
|
||||
availableRecup_N: detailedCounters?.recupN?.solde || 0
|
||||
availableRTT: detailedCounters.rttN?.solde || 0
|
||||
}}
|
||||
accessToken={graphToken}
|
||||
userId={userId}
|
||||
@@ -882,15 +697,13 @@ const Requests = () => {
|
||||
availableRTT_N1: 0,
|
||||
availableABS: 0,
|
||||
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
|
||||
availableRTT: detailedCounters.rttN?.solde || 0,
|
||||
availableRecup: detailedCounters?.recupN?.solde || 0,
|
||||
availableRecup_N: detailedCounters?.recupN?.solde || 0
|
||||
availableRTT: detailedCounters.rttN?.solde || 0
|
||||
}}
|
||||
accessToken={graphToken}
|
||||
userId={userId}
|
||||
userEmail={user.email}
|
||||
userRole={user.role}
|
||||
userName={`${user.prenom} ${user.nom}`} // ⭐ CORRIGÉ : ajout des accolades {}
|
||||
userName={`${user.prenom} ${user.nom}`}
|
||||
onRequestUpdated={() => {
|
||||
refreshAllData();
|
||||
}}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: true,
|
||||
port: 3000
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vendor: ['react', 'react-dom', 'react-router-dom']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user