This commit is contained in:
2025-11-28 16:55:45 +01:00
parent f22979a44a
commit 881476122c
54 changed files with 7628 additions and 5654 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@ const Collaborateur = () => {
const fetchTeamMembers = async () => {
try {
const response = await fetch(`http://localhost:3000/getTeamMembers?manager_id=${user.id}`);
const response = await fetch(`/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(`http://localhost:3000/getPendingRequests?manager_id=${user.id}`);
const response = await fetch(`/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(`http://localhost:3000/getAllTeamRequests?SuperieurId=${user.id}`);
const response = await fetch(`/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('http://localhost:3000/validateRequest', {
const response = await fetch('/validateRequest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -191,9 +191,7 @@ 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 */}
@@ -224,35 +222,9 @@ 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 */}
@@ -408,7 +380,7 @@ const Collaborateur = () => {
<div className="text-sm mt-1">
<p className="text-gray-500">Document joint</p>
<a
href={`http://localhost/GTA/project/uploads/${request.file}`}
href={`/GTA/project/uploads/${request.file}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-1 mt-1"
@@ -464,7 +436,7 @@ const Collaborateur = () => {
<div>
<p className="text-gray-500">Document joint</p>
<a
href={`http://localhost/GTA/project/uploads/${selectedRequest.file}`}
href={`/GTA/project/uploads/${selectedRequest.file}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-2"

View File

@@ -96,7 +96,7 @@ const CompteRenduActivites = () => {
setIsLoading(true);
try {
const response = await fetch(`http://localhost:3000/getCompteRenduActivites?user_id=${userId}&annee=${annee}&mois=${mois}`);
const response = await fetch(`/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(`http://localhost:3000/getTeamLeaves?user_id=${userId}&role=${user.role}`);
const congesResponse = await fetch(`/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(`http://localhost:3000/getStatsAnnuelles?user_id=${userId}&annee=${annee}`);
const response = await fetch(`/getStatsAnnuelles?user_id=${userId}&annee=${annee}`);
const data = await response.json();
if (data.success) {
@@ -163,22 +163,38 @@ const CompteRenduActivites = () => {
return selectedYear === previousYear && selectedMonth === previousMonth;
};
// Générer les jours du mois (lundi-vendredi)
// Générer les jours du mois (lundi-samedi) avec décalage correct
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();
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
// Exclure les dimanches (0)
if (dayOfWeek !== 0) {
days.push(currentDay);
}
}
return days;
};
@@ -235,6 +251,8 @@ 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;
@@ -284,7 +302,7 @@ const CompteRenduActivites = () => {
setIsSaving(true);
try {
const response = await fetch('http://localhost:3000/saveCompteRenduJour', {
const response = await fetch('/saveCompteRenduJour', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -327,7 +345,7 @@ const CompteRenduActivites = () => {
setIsSaving(true);
try {
const response = await fetch('http://localhost:3000/saveCompteRenduMasse', {
const response = await fetch('/saveCompteRenduMasse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -487,14 +505,7 @@ 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>
)}
@@ -552,23 +563,30 @@ 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-5 gap-2 p-4 bg-gray-50">
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi'].map(day => (
<div className="grid grid-cols-6 gap-2 p-4 bg-gray-50">
{['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'].map(day => (
<div key={day} className="text-center font-semibold text-gray-700 text-sm">
{day}
</div>
))}
</div>
<div className="grid grid-cols-5 gap-2 p-4">
<div className="grid grid-cols-6 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);
@@ -707,17 +725,7 @@ 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 && (
<>
@@ -815,7 +823,7 @@ const CompteRenduActivites = () => {
<SaisieMasseModal
mois={mois}
annee={annee}
days={days}
days={days.filter(d => d !== null)} // Filtrer les cases vides
congesData={congesData}
holidays={holidays}
onClose={() => setShowSaisieMasse(false)}
@@ -894,6 +902,30 @@ 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">
@@ -915,8 +947,15 @@ const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, on
Sélectionner tous les jours ouvrés disponibles
</button>
<div className="grid grid-cols-5 gap-2 mb-6">
{days.map((date, index) => {
<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>
);
}
const dateStr = formatDateToString(date);
const enConge = isJourEnConge(date);
const ferie = isHoliday(date);

View File

@@ -1,10 +1,11 @@
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 } from 'lucide-react';
import { Calendar, Clock, Plus, RefreshCw, Menu, Info, Briefcase, AlertCircle, Wifi, WifiOff, TrendingUp, HelpCircle, FileText, ChevronRight, Users } 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();
@@ -19,16 +20,23 @@ 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 = {
@@ -52,7 +60,7 @@ const Dashboard = () => {
const fetchNotifications = async () => {
try {
const response = await fetch(`http://localhost:3000/getNotifications?user_id=${userId}`);
const response = await fetch(`/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')) {
@@ -73,29 +81,46 @@ const Dashboard = () => {
const fetchDetailedCounters = async () => {
try {
if (!isLoading) setIsRefreshing(true);
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);
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(`http://localhost:3000/getCongesAnticipes?user_id=${userId}`);
const response = await fetch(`/getCongesAnticipes?user_id=${userId}`);
const data = await response.json();
if (data.success) {
@@ -109,10 +134,84 @@ const Dashboard = () => {
}
};
// ⭐ FONCTION DE RAFRAÎCHISSEMENT UNIFIÉE (modifiée)
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);
}
};
const refreshAllData = useCallback(async () => {
if (!userId) return;
console.log('🔄 Rafraîchissement des données...');
setIsRefreshing(true);
@@ -120,7 +219,9 @@ const Dashboard = () => {
await Promise.all([
fetchDetailedCounters(),
fetchNotifications(),
fetchCongesAnticipes() // ⭐ AJOUT
fetchCongesAnticipes(),
fetchRecentRequests(),
fetchTeamLeaves()
]);
setLastRefresh(new Date());
console.log('✅ Données rafraîchies');
@@ -131,7 +232,6 @@ const Dashboard = () => {
}
}, [userId]);
// ⭐ FONCTION POUR AFFICHER DES TOASTS
const showToast = useCallback((message, type = 'info') => {
const id = Date.now();
const newToast = { id, message, type };
@@ -143,13 +243,12 @@ const Dashboard = () => {
}, 5000);
}, []);
// ⭐ CONNEXION SSE
useEffect(() => {
if (!userId) return;
console.log('🔌 Connexion SSE au serveur collaborateurs...');
const eventSource = new EventSource(`http://localhost:3000/api/events/collaborateur?user_id=${userId}`);
const eventSource = new EventSource(`/api/events/collaborateur?user_id=${userId}`);
eventSource.onopen = () => {
console.log('✅ SSE connecté');
@@ -167,6 +266,7 @@ const Dashboard = () => {
break;
case 'heartbeat':
// Ne rien afficher pour éviter de polluer les logs
break;
case 'demande-validated-rh':
@@ -182,9 +282,44 @@ const Dashboard = () => {
break;
case 'compteur-updated':
console.log('📊 Compteurs mis à jour via SSE');
fetchDetailedCounters();
fetchCongesAnticipes(); // ⭐ AJOUT
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');
}
break;
case 'demande-list-updated':
@@ -227,14 +362,12 @@ const Dashboard = () => {
};
}, [userId, refreshAllData, showToast]);
// ⭐ RAFRAÎCHISSEMENT INITIAL
useEffect(() => {
if (userId) {
refreshAllData();
}
}, [userId, refreshAllData]);
// ⭐ Polling de secours
useEffect(() => {
if (!userId) return;
@@ -246,7 +379,6 @@ const Dashboard = () => {
return () => clearInterval(interval);
}, [userId, refreshAllData]);
// ⭐ Rafraîchissement quand la page redevient visible
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && userId) {
@@ -259,7 +391,6 @@ const Dashboard = () => {
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, [userId, refreshAllData]);
// ⭐ Rafraîchissement au focus
useEffect(() => {
const handleFocus = () => {
if (userId) {
@@ -274,7 +405,7 @@ const Dashboard = () => {
const markNotificationRead = async (id) => {
try {
const res = await fetch(`http://localhost:3000/markNotificationRead`, {
const res = await fetch(`/markNotificationRead`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ notificationId: id }),
@@ -302,31 +433,6 @@ 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', {
@@ -445,6 +551,7 @@ 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"
@@ -456,6 +563,7 @@ 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"
>
@@ -514,6 +622,7 @@ 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"
>
@@ -537,10 +646,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">
<div className="flex flex-wrap gap-3 text-sm mb-3">
<div className="flex items-center gap-2">
<Briefcase className="w-4 h-4" />
<span>{detailedCounters.user.role}</span>
<span className="font-medium">{detailedCounters.user.role}</span>
</div>
{detailedCounters.user.service && (
<div className="flex items-center gap-2">
@@ -557,8 +666,25 @@ 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-2 opacity-90">
<p className="text-sm mt-3 opacity-90">
📅 Ancienneté : {detailedCounters.user.ancienneteAnnees} an{detailedCounters.user.ancienneteAnnees > 1 ? 's' : ''} et {detailedCounters.user.ancienneteMoisRestants} mois
{' '}(depuis le {formatDate(detailedCounters.user.dateEntree)})
</p>
@@ -571,255 +697,237 @@ const Dashboard = () => {
{detailedCounters && (
<div className="space-y-6 mb-8">
<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>
{/* 🆕 SECTION CÔTE À CÔTE : Demandes récentes + Congés du service */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 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>
{/* ⭐ 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>
{/* 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>
<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
{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
</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 className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
<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-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">Reporté | Exercice {detailedCounters.cpN1.exercice}</p>
<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>
</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">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é</span>
<span className="text-lg font-bold text-red-600">-{detailedCounters.cpN1.pris.toFixed(2)}j</span>
</div>
<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-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-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>
</div>
</div>
)}
{detailedCounters.cpN && (
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
<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-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">Exercice {detailedCounters.cpN.exercice}</p>
<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>
</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="text-xs text-gray-500 pt-2">
Reste à acquérir : {detailedCounters.cpN.joursRestantsAAcquerir.toFixed(2)}j
<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>
<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 className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
<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-gradient-to-r from-green-500 to-green-600 p-4 text-white">
<div className="flex items-center justify-between">
<div>
@@ -832,49 +940,58 @@ 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="text-xs text-gray-500 pt-2">
Reste à acquérir : {detailedCounters.rttN.joursRestantsAAcquerir.toFixed(2)}j
<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>
</div>
</div>
)}
{detailedCounters.recupN && (
<div className="bg-white rounded-xl shadow-md border border-gray-200 overflow-hidden">
<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-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ération {detailedCounters.recupN.annee}</h3>
<p className="text-sm opacity-90">Samedis travaillés</p>
<h3 className="text-lg font-bold">Récupérations {detailedCounters.recupN.annee}</h3>
</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é</span>
<span className="text-sm font-medium text-gray-600">Consommé(s)</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>
@@ -882,18 +999,21 @@ 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)}
@@ -906,9 +1026,10 @@ const Dashboard = () => {
availableRTT_N1: 0,
availableABS: 0,
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
availableRTT: detailedCounters.rttN?.solde || 0
availableRTT: detailedCounters.rttN?.solde || 0,
availableRecup: detailedCounters?.recupN?.solde || 0,
availableRecup_N: detailedCounters?.recupN?.solde || 0
}}
// PASSER LES DONNÉES D'ANTICIPATION
congesAnticipes={congesAnticipes}
accessToken={graphToken}
userId={userId}

View File

@@ -19,7 +19,7 @@ const EmployeeDetails = () => {
setIsLoading(true);
// 1⃣ Données employé (avec compteurs inclus)
const resEmployee = await fetch(`http://localhost:3000/getEmploye?id=${id}`);
const resEmployee = await fetch(`/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(`http://localhost:3000/getEmployeRequest?id=${id}`);
const resRequests = await fetch(`/getEmployeRequest?id=${id}`);
const dataRequests = await resRequests.json();
if (dataRequests.success) {

View File

@@ -48,7 +48,6 @@ const Login = () => {
return;
}
// Redirection vers le dashboard
navigate('/dashboard');
} catch (error) {
@@ -67,7 +66,6 @@ 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"
@@ -91,8 +89,9 @@ const Login = () => {
GESTION DES TEMPS ET DES ACTIVITÉS
</p>
</div>
{/* Bouton Office 365 */}
<div>
<div className="mb-4">
<button
data-testid="o365-login-btn"
onClick={handleO365Login}
@@ -113,6 +112,63 @@ 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">
@@ -135,8 +191,8 @@ const Login = () => {
</div>
</div>
</div>
</div>
</div>
);
};
export default Login;
export default Login;

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { useAuth } from "../context/AuthContext";
import Sidebar from "../components/Sidebar";
import GlobalTutorial from '../components/GlobalTutorial';
import {
Users,
CheckCircle,
@@ -30,6 +31,7 @@ const Manager = () => {
const [comment, setComment] = useState("");
const [isValidating, setIsValidating] = useState(false);
useEffect(() => {
if (user?.id) fetchTeamData();
}, [user]);
@@ -51,7 +53,7 @@ const Manager = () => {
const fetchTeamMembers = async () => {
try {
const res = await fetch(`http://localhost:3000/getTeamMembers?manager_id=${user.id}`);
const res = await fetch(`/getTeamMembers?manager_id=${user.id}`);
const data = await res.json();
if (data.success) setTeamMembers(data.team_members || []);
else setTeamMembers([]);
@@ -62,7 +64,7 @@ const Manager = () => {
const fetchPendingRequests = async () => {
try {
const res = await fetch(`http://localhost:3000/getPendingRequests?manager_id=${user.id}`);
const res = await fetch(`/getPendingRequests?manager_id=${user.id}`);
const data = await res.json();
if (data.success) setPendingRequests(data.requests || []);
else setPendingRequests([]);
@@ -73,7 +75,7 @@ const Manager = () => {
const fetchAllTeamRequests = async () => {
try {
const res = await fetch(`http://localhost:3000/getAllTeamRequests?SuperieurId=${user.id}`);
const res = await fetch(`/getAllTeamRequests?SuperieurId=${user.id}`);
const data = await res.json();
if (data.success) setAllRequests(data.requests || []);
else setAllRequests([]);
@@ -113,7 +115,7 @@ const Manager = () => {
try {
setIsValidating(true); // ✅ Maintenant défini
const response = await fetch('http://localhost:3000/validateRequest', {
const response = await fetch('/validateRequest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -353,7 +355,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">
<div className="bg-white rounded-xl shadow-sm border border-gray-100" data-tour="demandes-attente">
<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>
@@ -382,14 +384,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>
@@ -401,7 +403,7 @@ const Manager = () => {
</div>
)}
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 ${isEmployee ? "lg:col-span-2" : ""}`}>
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 ${isEmployee ? "lg:col-span-2" : ""}`} data-tour="mon-equipe">
<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>
@@ -415,7 +417,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">
@@ -445,7 +447,7 @@ const Manager = () => {
</div>
{!isEmployee && (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 mt-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-100 mt-6" data-tour="historique-demandes">
<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>
@@ -473,10 +475,10 @@ const Manager = () => {
</p>
)}
{r.file && (
<div className="text-sm mt-1">
<div className="text-sm mt-1" data-tour="document-joint">
<p className="text-gray-500">Document joint</p>
<a
href={`http://localhost:3000/uploads/${r.file}`}
href={`/uploads/${r.file}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-1 mt-1"
@@ -493,7 +495,8 @@ const Manager = () => {
</div>
</div>
)}
</div>
</div>
<GlobalTutorial userId={user?.id} userRole={user?.role} />
</div >
);
};

View File

@@ -6,6 +6,8 @@ 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();
@@ -45,6 +47,55 @@ 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 = {
@@ -64,7 +115,7 @@ const Requests = () => {
const fetchDetailedCounters = async () => {
try {
const response = await fetch(`http://localhost:3000/getDetailedLeaveCounters?user_id=${userId}`);
const response = await fetch(`/getDetailedLeaveCounters?user_id=${userId}`);
const data = await response.json();
if (data.success) {
@@ -79,7 +130,7 @@ const Requests = () => {
const fetchAllRequests = async () => {
try {
const url = `http://localhost:3000/getRequests?user_id=${userId}`;
const url = `/getRequests?user_id=${userId}`;
const response = await fetch(url);
const text = await response.text();
let data;
@@ -144,56 +195,157 @@ const Requests = () => {
};
// ⭐ NOUVELLE FONCTION : Supprimer une demande
const handleDeleteRequest = (request) => {
setRequestToDelete(request);
setShowDeleteConfirm(true);
// ⭐ 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');
};
// ⭐ NOUVELLE FONCTION : Confirmer la suppression
// ⭐ NOUVELLE FONCTION : Confirmer la suppression (sans setDeletedRequests)
const confirmDeleteRequest = async () => {
if (!requestToDelete) return;
setIsSubmitting(true);
try {
const response = await fetch('http://localhost:3000/deleteRequest', {
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', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestId: requestToDelete.id,
userId: userId,
userEmail: user.email,
userName: `${user.prenom} ${user.nom}`,
accessToken: graphToken
}),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.success) {
showToast('✅ Demande supprimée avec succès', 'success');
refreshAllData();
setShowDeleteConfirm(false);
setRequestToDelete(null);
if (selectedRequest?.id === requestToDelete.id) {
setSelectedRequest(null);
}
} else {
showToast(`❌ Erreur : ${data.message}`, 'error');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
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 (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');
}
} catch (error) {
console.error('Erreur suppression:', error);
showToast('❌ Erreur lors de la suppression', '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);
}
}
};
// Connexion SSE
useEffect(() => {
if (!userId) return;
console.log('🔌 Connexion SSE au serveur collaborateurs...');
const eventSource = new EventSource(`http://localhost:3000/api/events/collaborateur?user_id=${userId}`);
const eventSource = new EventSource(`/api/events/collaborateur?user_id=${userId}`);
eventSource.onopen = () => {
console.log('✅ SSE connecté');
@@ -275,7 +427,7 @@ const Requests = () => {
setFilteredRequests(filtered);
setCurrentPage(1);
}, [allRequests, searchTerm, statusFilter, typeFilter]);
}, [allRequests, searchTerm, statusFilter, typeFilter]);
const indexOfLastRequest = currentPage * requestsPerPage;
const indexOfFirstRequest = indexOfLastRequest - requestsPerPage;
@@ -289,6 +441,7 @@ 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';
}
};
@@ -307,6 +460,32 @@ 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 */}
@@ -327,30 +506,72 @@ const Requests = () => {
</div>
{/* Modal de confirmation de suppression */}
{showDeleteConfirm && (
{showDeleteConfirm && requestToDelete && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black bg-opacity-50" onClick={() => setShowDeleteConfirm(false)}></div>
<div className="absolute inset-0 bg-black bg-opacity-50" onClick={() => !isSubmitting && 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">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>
<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>
<div className="flex gap-3 justify-end">
<button
onClick={() => setShowDeleteConfirm(false)}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
disabled={isSubmitting}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50"
>
Annuler
</button>
<button
onClick={confirmDeleteRequest}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
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"
>
Supprimer
{isSubmitting ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Annulation...
</>
) : (
<>
<Trash2 className="w-4 h-4" />
Confirmer l'annulation
</>
)}
</button>
</div>
</div>
@@ -380,6 +601,7 @@ 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"
>
@@ -389,50 +611,7 @@ 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 */}
@@ -441,7 +620,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="relative">
<div className="flex-1 relative" data-tour="recherche">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
@@ -453,6 +632,7 @@ 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"
>
@@ -472,6 +652,7 @@ 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}
@@ -479,10 +660,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é payé</option>
<option value="Congé payé">Congé(s) payé(s)</option>
<option value="RTT">RTT</option>
<option value="Arrêt maladie">Arrêt maladie</option>
<option value="Absence">Absence</option>
<option value="Récupération">Récupération</option>
</select>
</div>
)}
@@ -495,13 +676,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">
<div className="space-y-3" data-tour="liste-demandes">
{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">
@@ -534,14 +715,16 @@ const Requests = () => {
)}
{/* Bouton Supprimer */}
<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>
{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>
)}
{/* Bouton Voir */}
<button
@@ -637,11 +820,11 @@ const Requests = () => {
</button>
)}
<button
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"
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"
>
<Trash2 className="w-4 h-4" />
Supprimer cette demande
<X className="w-4 h-4" />
Annuler cette demande
</button>
</div>
</div>
@@ -667,7 +850,9 @@ const Requests = () => {
availableRTT_N1: 0,
availableABS: 0,
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
availableRTT: detailedCounters.rttN?.solde || 0
availableRTT: detailedCounters.rttN?.solde || 0,
availableRecup: detailedCounters?.recupN?.solde || 0,
availableRecup_N: detailedCounters?.recupN?.solde || 0
}}
accessToken={graphToken}
userId={userId}
@@ -697,13 +882,15 @@ const Requests = () => {
availableRTT_N1: 0,
availableABS: 0,
availableCP: (detailedCounters.cpN1?.solde || 0) + (detailedCounters.cpN?.solde || 0),
availableRTT: detailedCounters.rttN?.solde || 0
availableRTT: detailedCounters.rttN?.solde || 0,
availableRecup: detailedCounters?.recupN?.solde || 0,
availableRecup_N: detailedCounters?.recupN?.solde || 0
}}
accessToken={graphToken}
userId={userId}
userEmail={user.email}
userRole={user.role}
userName={`${user.prenom} ${user.nom}`}
userName={`${user.prenom} ${user.nom}`} // CORRIGÉ : ajout des accolades {}
onRequestUpdated={() => {
refreshAllData();
}}