Reapply "V1_Sans_Congé_Anticipéfemini collaboratrice"
This reverts commit 7f15e380e3.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ import { Users, CheckCircle, XCircle, Clock, Calendar, FileText, Menu, Eye, Mess
|
||||
const Collaborateur = () => {
|
||||
const { user } = useAuth();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const isEmployee = user?.role === 'Collaborateur';
|
||||
const isEmployee = user?.role === 'Collaborateur'||'Apprenti';
|
||||
const [teamMembers, setTeamMembers] = useState([]);
|
||||
const [pendingRequests, setPendingRequests] = useState([]);
|
||||
const [allRequests, setAllRequests] = useState([]);
|
||||
@@ -44,7 +44,7 @@ const Collaborateur = () => {
|
||||
|
||||
const fetchTeamMembers = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost/GTA/project/public/php/getTeamMembers.php?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(`http://localhost/GTA/project/public/php/getPendingRequests.php?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(`http://localhost/GTA/project/public/php/getAllTeamRequests.php?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('http://localhost/GTA/project/public/php/validateRequest.php', {
|
||||
const response = await fetch('http://localhost:3000/validateRequest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
984
project/src/pages/CompteRenduActivite.jsx
Normal file
984
project/src/pages/CompteRenduActivite.jsx
Normal file
@@ -0,0 +1,984 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import {
|
||||
Calendar, Clock, Check, X, Save, Lock, Unlock,
|
||||
Download, Menu, AlertCircle, ChevronLeft, ChevronRight,
|
||||
FileText, TrendingUp, RefreshCw, Info
|
||||
} from 'lucide-react';
|
||||
|
||||
const CompteRenduActivites = () => {
|
||||
const { user } = useAuth();
|
||||
const userId = user?.id || user?.CollaborateurADId || user?.ID;
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [joursActifs, setJoursActifs] = useState([]);
|
||||
const [statsAnnuelles, setStatsAnnuelles] = useState(null);
|
||||
const [mensuelData, setMensuelData] = useState(null);
|
||||
const [congesData, setCongesData] = useState([]);
|
||||
const [holidays, setHolidays] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showSaisieModal, setShowSaisieModal] = useState(false);
|
||||
const [selectedJour, setSelectedJour] = useState(null);
|
||||
const [showSaisieMasse, setShowSaisieMasse] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [infoMessage, setInfoMessage] = useState(null);
|
||||
|
||||
// Vérifier l'accès : forfait jour, directeur campus, ou RH
|
||||
const hasAccess = () => {
|
||||
const userRole = user?.role;
|
||||
const typeContrat = user?.TypeContrat || user?.typeContrat;
|
||||
|
||||
return (
|
||||
typeContrat === 'forfait-jour' ||
|
||||
typeContrat === 'forfait_jour' ||
|
||||
userRole === 'Directeur Campus' ||
|
||||
userRole === 'Directrice Campus' ||
|
||||
userRole === 'RH' ||
|
||||
userRole === 'Admin'
|
||||
);
|
||||
};
|
||||
|
||||
const isRH = user?.role === 'RH' || user?.role === 'Admin';
|
||||
const isDirecteurCampus = user?.role === 'Directeur Campus' || user?.role === 'Directrice Campus';
|
||||
|
||||
const annee = currentDate.getFullYear();
|
||||
const mois = currentDate.getMonth() + 1;
|
||||
|
||||
// Charger les jours fériés français
|
||||
const fetchFrenchHolidays = async (year) => {
|
||||
try {
|
||||
const response = await fetch(`https://calendrier.api.gouv.fr/jours-feries/metropole/${year}.json`);
|
||||
if (!response.ok) throw new Error(`Erreur API: ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
const holidayDates = Object.keys(data).map(dateStr => {
|
||||
const [year, month, day] = dateStr.split('-').map(Number);
|
||||
return {
|
||||
date: new Date(year, month - 1, day),
|
||||
name: data[dateStr]
|
||||
};
|
||||
});
|
||||
return holidayDates;
|
||||
} catch (error) {
|
||||
console.error('Erreur jours fériés:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const year = currentDate.getFullYear();
|
||||
fetchFrenchHolidays(year).then(setHolidays);
|
||||
}, [currentDate]);
|
||||
|
||||
const isHoliday = (date) => {
|
||||
if (!date) return false;
|
||||
return holidays.some(holiday =>
|
||||
holiday.date.getDate() === date.getDate() &&
|
||||
holiday.date.getMonth() === date.getMonth() &&
|
||||
holiday.date.getFullYear() === date.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
const getHolidayName = (date) => {
|
||||
const holiday = holidays.find(h =>
|
||||
h.date.getDate() === date.getDate() &&
|
||||
h.date.getMonth() === date.getMonth() &&
|
||||
h.date.getFullYear() === date.getFullYear()
|
||||
);
|
||||
return holiday?.name;
|
||||
};
|
||||
|
||||
// Charger les données du mois
|
||||
const loadCompteRendu = useCallback(async () => {
|
||||
if (!userId || !hasAccess()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`http://localhost:3000/getCompteRenduActivites?user_id=${userId}&annee=${annee}&mois=${mois}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setJoursActifs(data.jours || []);
|
||||
setMensuelData(data.mensuel);
|
||||
console.log('📅 Jours chargés:', data.jours?.length || 0, 'jours');
|
||||
console.log('📊 Détail des jours:', data.jours);
|
||||
}
|
||||
|
||||
const congesResponse = await fetch(`http://localhost:3000/getTeamLeaves?user_id=${userId}&role=${user.role}`);
|
||||
const congesData = await congesResponse.json();
|
||||
|
||||
if (congesData.success) {
|
||||
setCongesData(congesData.leaves || []);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement compte-rendu:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [userId, annee, mois, user?.role]);
|
||||
|
||||
// Charger les stats annuelles
|
||||
const loadStatsAnnuelles = useCallback(async () => {
|
||||
if (!userId || !hasAccess()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://localhost:3000/getStatsAnnuelles?user_id=${userId}&annee=${annee}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setStatsAnnuelles(data.stats);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur stats annuelles:', error);
|
||||
}
|
||||
}, [userId, annee]);
|
||||
|
||||
useEffect(() => {
|
||||
loadCompteRendu();
|
||||
loadStatsAnnuelles();
|
||||
}, [loadCompteRendu, loadStatsAnnuelles]);
|
||||
|
||||
// Vérifier si le mois est autorisé (mois en cours + mois précédent)
|
||||
const isMoisAutorise = () => {
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const currentMonth = today.getMonth() + 1;
|
||||
|
||||
const selectedYear = currentDate.getFullYear();
|
||||
const selectedMonth = currentDate.getMonth() + 1;
|
||||
|
||||
// RH peut tout voir
|
||||
if (isRH) return true;
|
||||
|
||||
// Mois en cours autorisé
|
||||
if (selectedYear === currentYear && selectedMonth === currentMonth) return true;
|
||||
|
||||
// Mois précédent autorisé
|
||||
const previousMonth = currentMonth === 1 ? 12 : currentMonth - 1;
|
||||
const previousYear = currentMonth === 1 ? currentYear - 1 : currentYear;
|
||||
|
||||
return selectedYear === previousYear && selectedMonth === previousMonth;
|
||||
};
|
||||
|
||||
// Générer les jours du mois (lundi-vendredi)
|
||||
const getDaysInMonth = () => {
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const daysInMonth = lastDay.getDate();
|
||||
|
||||
const days = [];
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const currentDay = new Date(year, month, day);
|
||||
const dayOfWeek = currentDay.getDay();
|
||||
|
||||
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
|
||||
days.push(currentDay);
|
||||
}
|
||||
}
|
||||
return days;
|
||||
};
|
||||
|
||||
const getJourData = (date) => {
|
||||
const dateStr = formatDateToString(date);
|
||||
const found = joursActifs.find(j => {
|
||||
// Normaliser la date de la BDD (peut être un objet Date ou une string)
|
||||
let jourDateStr = j.JourDate;
|
||||
if (j.JourDate instanceof Date) {
|
||||
jourDateStr = formatDateToString(j.JourDate);
|
||||
} else if (typeof j.JourDate === 'string') {
|
||||
// Si c'est déjà une string, extraire juste la partie date (YYYY-MM-DD)
|
||||
jourDateStr = j.JourDate.split('T')[0];
|
||||
}
|
||||
|
||||
const match = jourDateStr === dateStr;
|
||||
console.log('Comparaison:', jourDateStr, 'vs', dateStr, 'match:', match);
|
||||
return match;
|
||||
});
|
||||
if (found) {
|
||||
console.log('✅ Jour trouvé:', dateStr, found);
|
||||
}
|
||||
return found;
|
||||
};
|
||||
|
||||
const isJourEnConge = (date) => {
|
||||
return congesData.some(conge => {
|
||||
const start = new Date(conge.startdate);
|
||||
const end = new Date(conge.enddate);
|
||||
return date >= start && date <= end && conge.statut === 'Valide';
|
||||
});
|
||||
};
|
||||
|
||||
// Vérifier si le jour est STRICTEMENT dans le passé (pas aujourd'hui)
|
||||
const isPastOnly = (date) => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const checkDate = new Date(date);
|
||||
checkDate.setHours(0, 0, 0, 0);
|
||||
return checkDate < today;
|
||||
};
|
||||
|
||||
// Vérifier si un jour spécifique est verrouillé
|
||||
const isJourVerrouille = (date) => {
|
||||
const jourData = getJourData(date);
|
||||
return jourData?.Verrouille === true || jourData?.Verrouille === 1;
|
||||
};
|
||||
|
||||
// Afficher message d'info
|
||||
const showInfo = (message, type = 'info') => {
|
||||
setInfoMessage({ message, type });
|
||||
setTimeout(() => setInfoMessage(null), 5000);
|
||||
};
|
||||
|
||||
// Ouvrir le modal de saisie
|
||||
const handleJourClick = (date) => {
|
||||
if (!isMoisAutorise() && !isRH) {
|
||||
showInfo('Vous ne pouvez saisir que pour le mois en cours ou le mois précédent', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPastOnly(date)) {
|
||||
showInfo('Vous ne pouvez pas saisir le jour actuel. Veuillez attendre demain.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHoliday(date)) {
|
||||
showInfo(`Jour férié : ${getHolidayName(date)} - Saisie impossible`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJourEnConge(date)) {
|
||||
showInfo('Vous êtes en congé ce jour - Saisie impossible', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJourVerrouille(date) && !isRH) {
|
||||
showInfo('Ce jour est déjà verrouillé - Contactez les RH pour modification', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const jourData = getJourData(date);
|
||||
setSelectedJour({
|
||||
date: date,
|
||||
dateStr: formatDateToString(date),
|
||||
jourTravaille: jourData?.JourTravaille !== false,
|
||||
reposQuotidien: jourData?.ReposQuotidienRespect !== false,
|
||||
reposHebdo: jourData?.ReposHebdomadaireRespect !== false,
|
||||
commentaire: jourData?.CommentaireRepos || ''
|
||||
});
|
||||
setShowSaisieModal(true);
|
||||
};
|
||||
|
||||
// Sauvegarder un jour
|
||||
const handleSaveJour = async () => {
|
||||
if ((!selectedJour.reposQuotidien || !selectedJour.reposHebdo)) {
|
||||
if (!selectedJour.commentaire || selectedJour.commentaire.trim() === '') {
|
||||
showInfo('Commentaire obligatoire en cas de non-respect des repos', 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/saveCompteRenduJour', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
date: selectedJour.dateStr,
|
||||
jour_travaille: selectedJour.jourTravaille,
|
||||
repos_quotidien: selectedJour.reposQuotidien,
|
||||
repos_hebdo: selectedJour.reposHebdo,
|
||||
commentaire: selectedJour.commentaire,
|
||||
rh_override: isRH
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
await loadCompteRendu();
|
||||
await loadStatsAnnuelles();
|
||||
setShowSaisieModal(false);
|
||||
showInfo('✅ Jour enregistré', 'success');
|
||||
console.log('Données rechargées après sauvegarde');
|
||||
} else {
|
||||
showInfo(data.message || 'Erreur lors de la sauvegarde', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur sauvegarde jour:', error);
|
||||
showInfo('Erreur serveur', 'error');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Saisie en masse
|
||||
const handleSaisieMasse = async (joursTravailles) => {
|
||||
if (!isMoisAutorise() && !isRH) {
|
||||
showInfo('Vous ne pouvez saisir que pour le mois en cours ou le mois précédent', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/saveCompteRenduMasse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
annee: annee,
|
||||
mois: mois,
|
||||
jours: joursTravailles,
|
||||
rh_override: isRH
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
await loadCompteRendu();
|
||||
await loadStatsAnnuelles();
|
||||
setShowSaisieMasse(false);
|
||||
showInfo(data.message || `✅ ${data.count || 0} jours enregistrés`, 'success');
|
||||
} else {
|
||||
showInfo(data.message || 'Erreur', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur saisie masse:', error);
|
||||
showInfo('Erreur serveur', 'error');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const formatDateToString = (date) => {
|
||||
if (!date) return null;
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const navigateMonth = (direction) => {
|
||||
setCurrentDate(prev => {
|
||||
const newDate = new Date(prev);
|
||||
if (direction === 'prev') {
|
||||
newDate.setMonth(prev.getMonth() - 1);
|
||||
} else {
|
||||
newDate.setMonth(prev.getMonth() + 1);
|
||||
}
|
||||
return newDate;
|
||||
});
|
||||
};
|
||||
|
||||
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
|
||||
const days = getDaysInMonth();
|
||||
const moisAutorise = isMoisAutorise();
|
||||
|
||||
// Vérifier l'accès
|
||||
if (!hasAccess()) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||
<div className="flex-1 lg:ml-60 p-8">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
|
||||
<AlertCircle className="w-12 h-12 text-yellow-600 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">Accès restreint</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Cette fonctionnalité est réservée aux :
|
||||
</p>
|
||||
<ul className="text-gray-700 space-y-2 text-left max-w-md mx-auto">
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="w-4 h-4 text-green-600" />
|
||||
Collaborateurs en forfait jour
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="w-4 h-4 text-green-600" />
|
||||
Directeurs et directrices de campus
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="w-4 h-4 text-green-600" />
|
||||
Service RH
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||
<div className="flex-1 lg:ml-60 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<RefreshCw className="w-12 h-12 animate-spin text-cyan-600 mx-auto mb-4" />
|
||||
<p className="text-gray-600">Chargement...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||
|
||||
<div className="flex-1 lg:ml-60 p-4 lg:p-8">
|
||||
{/* Message d'information */}
|
||||
{infoMessage && (
|
||||
<div className={`mb-4 p-4 rounded-lg border-l-4 flex items-center gap-3 animate-slideIn ${infoMessage.type === 'success' ? 'bg-green-50 border-green-500' :
|
||||
infoMessage.type === 'error' ? 'bg-red-50 border-red-500' :
|
||||
infoMessage.type === 'warning' ? 'bg-orange-50 border-orange-500' :
|
||||
'bg-blue-50 border-blue-500'
|
||||
}`}>
|
||||
<Info className={`w-5 h-5 flex-shrink-0 ${infoMessage.type === 'success' ? 'text-green-600' :
|
||||
infoMessage.type === 'error' ? 'text-red-600' :
|
||||
infoMessage.type === 'warning' ? 'text-orange-600' :
|
||||
'text-blue-600'
|
||||
}`} />
|
||||
<p className={`text-sm font-medium ${infoMessage.type === 'success' ? 'text-green-800' :
|
||||
infoMessage.type === 'error' ? 'text-red-800' :
|
||||
infoMessage.type === 'warning' ? 'text-orange-800' :
|
||||
'text-blue-800'
|
||||
}`}>{infoMessage.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900">
|
||||
Compte-Rendu d'Activités
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Forfait jour - Suivi des jours travaillés et repos obligatoires
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="lg:hidden p-2 rounded-lg bg-white border"
|
||||
>
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats annuelles */}
|
||||
{statsAnnuelles && (
|
||||
<div className="bg-gradient-to-r from-cyan-500 to-blue-500 rounded-xl shadow-md p-6 text-white">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingUp className="w-6 h-6" />
|
||||
<h3 className="text-lg font-bold">Cumul annuel {annee}</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white bg-opacity-20 rounded-lg p-4">
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bandeau mois non autorisé */}
|
||||
{!moisAutorise && !isRH && (
|
||||
<div className="bg-yellow-50 border-l-4 border-yellow-500 p-4 mb-6 rounded">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertCircle className="w-6 h-6 text-yellow-600" />
|
||||
<div>
|
||||
<p className="font-semibold text-yellow-900">Mois non accessible</p>
|
||||
<p className="text-sm text-yellow-700">
|
||||
Vous pouvez saisir uniquement le mois en cours et le mois précédent (mais pas le jour actuel).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation + Actions */}
|
||||
<div className="bg-white rounded-lg border p-4 mb-6">
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => navigateMonth('prev')}
|
||||
className="p-2 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-blue-600" />
|
||||
</button>
|
||||
<h2 className="text-xl font-semibold min-w-[200px] text-center">
|
||||
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => navigateMonth('next')}
|
||||
className="p-2 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 text-blue-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{mensuelData && (
|
||||
<span className="text-sm text-gray-600">
|
||||
{mensuelData.NbJoursTravailles || 0} jours saisis
|
||||
</span>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setShowSaisieMasse(true)}
|
||||
disabled={!moisAutorise && !isRH}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<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 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">
|
||||
{days.map((date, index) => {
|
||||
const jourData = getJourData(date);
|
||||
const enConge = isJourEnConge(date);
|
||||
const ferie = isHoliday(date);
|
||||
const isPast = isPastOnly(date);
|
||||
const isToday = date.toDateString() === new Date().toDateString();
|
||||
const jourVerrouille = isJourVerrouille(date);
|
||||
|
||||
// Déterminer la classe de fond
|
||||
let bgClass = 'bg-white hover:bg-gray-50';
|
||||
let cursorClass = 'cursor-pointer hover:shadow-md';
|
||||
|
||||
if (ferie) {
|
||||
bgClass = 'bg-gray-700 text-white';
|
||||
cursorClass = 'cursor-not-allowed';
|
||||
} else if (enConge) {
|
||||
bgClass = 'bg-purple-100';
|
||||
cursorClass = 'cursor-not-allowed';
|
||||
} else if (jourData) {
|
||||
bgClass = 'bg-gray-300 hover:bg-gray-400';
|
||||
cursorClass = 'cursor-pointer hover:shadow-md';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleJourClick(date)}
|
||||
className={`
|
||||
min-h-[100px] p-3 rounded-lg border-2 transition-all
|
||||
${isToday ? 'border-cyan-500' : 'border-gray-200'}
|
||||
${bgClass}
|
||||
${cursorClass}
|
||||
${!isPast ? 'opacity-40 cursor-not-allowed' : ''}
|
||||
${!moisAutorise && !isRH ? 'opacity-60 cursor-not-allowed' : ''}
|
||||
`}
|
||||
title={
|
||||
ferie ? getHolidayName(date) :
|
||||
enConge ? 'En congé' :
|
||||
jourData ? 'Jour saisi - Cliquer pour modifier' :
|
||||
''
|
||||
}
|
||||
>
|
||||
<div className={`text-right text-sm font-semibold mb-2 flex items-center justify-end gap-1 ${ferie ? 'text-white' :
|
||||
jourData ? 'text-gray-700' :
|
||||
'text-gray-700'
|
||||
}`}>
|
||||
{date.getDate()}
|
||||
{jourData && !ferie && !enConge && (
|
||||
<Lock className="w-3 h-3 text-gray-600" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ferie ? (
|
||||
<div className="text-center">
|
||||
<div className="text-xs text-white font-bold truncate">{getHolidayName(date)}</div>
|
||||
</div>
|
||||
) : enConge ? (
|
||||
<div className="text-center">
|
||||
<div className="text-xs text-purple-700 font-semibold">En congé</div>
|
||||
</div>
|
||||
) : jourData ? (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{jourData.JourTravaille ? (
|
||||
<Check className="w-3 h-3 text-gray-700" />
|
||||
) : (
|
||||
<X className="w-3 h-3 text-gray-700" />
|
||||
)}
|
||||
<span className="text-gray-700 truncate">
|
||||
{jourData.JourTravaille ? 'Travaillé' : 'Non travaillé'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!jourData.ReposQuotidienRespect && (
|
||||
<div className="flex items-center gap-1 text-xs text-gray-700">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
<span className="truncate">Repos quotidien</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!jourData.ReposHebdomadaireRespect && (
|
||||
<div className="flex items-center gap-1 text-xs text-gray-700">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
<span className="truncate">Repos hebdo</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center text-xs text-gray-600 font-semibold mt-2">
|
||||
Saisi
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-xs text-gray-400">
|
||||
{isPast && moisAutorise ? 'Cliquer pour saisir' : 'Non disponible'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Légende */}
|
||||
<div className="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-cyan-50 border-2 border-cyan-500 rounded"></div>
|
||||
<span>Aujourd'hui</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-gray-300 border-2 border-gray-200 rounded"></div>
|
||||
<Lock className="w-3 h-3 text-gray-600" />
|
||||
<span>Jour saisi (grisé)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-purple-100 border-2 border-gray-200 rounded"></div>
|
||||
<span>En congé</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-gray-700 rounded"></div>
|
||||
<span>Jour férié</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-orange-600" />
|
||||
<span>Non-respect repos</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal saisie jour */}
|
||||
{showSaisieModal && selectedJour && (
|
||||
<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-lg w-full p-6 max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">
|
||||
Saisie du {selectedJour.date.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||
})}
|
||||
</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 && (
|
||||
<>
|
||||
<div className="border-t pt-4">
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedJour.reposQuotidien}
|
||||
onChange={(e) => setSelectedJour({ ...selectedJour, reposQuotidien: e.target.checked })}
|
||||
className="w-5 h-5 text-blue-600 rounded mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium block">
|
||||
Respect du repos quotidien
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
11 heures consécutives minimum
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedJour.reposHebdo}
|
||||
onChange={(e) => setSelectedJour({ ...selectedJour, reposHebdo: e.target.checked })}
|
||||
className="w-5 h-5 text-blue-600 rounded mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium block">
|
||||
Respect du repos hebdomadaire
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
35 heures consécutives minimum (24h + 11h)
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{(!selectedJour.reposQuotidien || !selectedJour.reposHebdo) && (
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Commentaire obligatoire
|
||||
</label>
|
||||
<textarea
|
||||
value={selectedJour.commentaire}
|
||||
onChange={(e) => setSelectedJour({ ...selectedJour, commentaire: e.target.value })}
|
||||
placeholder="Précisez les jours/semaines concernés et les raisons..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-orange-700 mt-2">
|
||||
Veuillez préciser les circonstances du non-respect des repos obligatoires
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
onClick={() => setShowSaisieModal(false)}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||
disabled={isSaving}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveJour}
|
||||
disabled={isSaving}
|
||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
Enregistrement...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4" />
|
||||
Enregistrer
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal saisie en masse */}
|
||||
{showSaisieMasse && (
|
||||
<SaisieMasseModal
|
||||
mois={mois}
|
||||
annee={annee}
|
||||
days={days}
|
||||
congesData={congesData}
|
||||
holidays={holidays}
|
||||
onClose={() => setShowSaisieMasse(false)}
|
||||
onSave={handleSaisieMasse}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Modal de saisie en masse
|
||||
const SaisieMasseModal = ({ mois, annee, days, congesData, holidays, onClose, onSave, isSaving }) => {
|
||||
const [selectedDays, setSelectedDays] = useState([]);
|
||||
|
||||
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
|
||||
const isJourEnConge = (date) => {
|
||||
return congesData.some(conge => {
|
||||
const start = new Date(conge.startdate);
|
||||
const end = new Date(conge.enddate);
|
||||
return date >= start && date <= end && conge.statut === 'Valide';
|
||||
});
|
||||
};
|
||||
|
||||
const isHoliday = (date) => {
|
||||
return holidays.some(holiday =>
|
||||
holiday.date.getDate() === date.getDate() &&
|
||||
holiday.date.getMonth() === date.getMonth() &&
|
||||
holiday.date.getFullYear() === date.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
const isPastOnly = (date) => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
date = new Date(date);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date < today;
|
||||
};
|
||||
|
||||
const toggleDay = (date) => {
|
||||
const dateStr = formatDateToString(date);
|
||||
if (selectedDays.includes(dateStr)) {
|
||||
setSelectedDays(selectedDays.filter(d => d !== dateStr));
|
||||
} else {
|
||||
setSelectedDays([...selectedDays, dateStr]);
|
||||
}
|
||||
};
|
||||
|
||||
const selectAllWorkingDays = () => {
|
||||
const workingDays = days
|
||||
.filter(date => isPastOnly(date) && !isJourEnConge(date) && !isHoliday(date))
|
||||
.map(date => formatDateToString(date));
|
||||
|
||||
setSelectedDays(workingDays);
|
||||
};
|
||||
|
||||
const formatDateToString = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const joursTravailles = selectedDays.map(dateStr => ({
|
||||
date: dateStr,
|
||||
jour_travaille: true,
|
||||
repos_quotidien: true,
|
||||
repos_hebdo: true,
|
||||
commentaire: ''
|
||||
}));
|
||||
|
||||
onSave(joursTravailles);
|
||||
};
|
||||
|
||||
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">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">
|
||||
Saisie en masse - {monthNames[mois - 1]} {annee}
|
||||
</h3>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
||||
<p className="text-sm text-blue-800">
|
||||
Sélectionnez tous les jours travaillés du mois. Le jour actuel, les jours fériés et les congés sont automatiquement exclus.
|
||||
Les repos quotidien et hebdomadaire seront considérés comme respectés.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={selectAllWorkingDays}
|
||||
className="mb-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Sélectionner tous les jours ouvrés disponibles
|
||||
</button>
|
||||
|
||||
<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);
|
||||
const isPast = isPastOnly(date);
|
||||
const isSelected = selectedDays.includes(dateStr);
|
||||
const isToday = date.toDateString() === new Date().toDateString();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => !enConge && !ferie && isPast && toggleDay(date)}
|
||||
className={`
|
||||
p-3 rounded-lg border-2 text-center cursor-pointer transition-all
|
||||
${isToday ? 'border-cyan-500 bg-cyan-100' : ''}
|
||||
${ferie ? 'bg-gray-700 text-white cursor-not-allowed' : ''}
|
||||
${enConge ? 'bg-purple-100 cursor-not-allowed' : ''}
|
||||
${!isPast ? 'opacity-30 cursor-not-allowed' : ''}
|
||||
${isSelected ? 'bg-green-500 border-green-600 text-white' : 'bg-white border-gray-200 hover:bg-gray-50'}
|
||||
`}
|
||||
>
|
||||
<div className="font-semibold">{date.getDate()}</div>
|
||||
{isToday && <div className="text-xs mt-1">Aujourd'hui</div>}
|
||||
{ferie && <div className="text-xs mt-1">Férié</div>}
|
||||
{enConge && <div className="text-xs mt-1 text-purple-700">Congé</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 mb-4">
|
||||
{selectedDays.length} jour{selectedDays.length > 1 ? 's' : ''} sélectionné{selectedDays.length > 1 ? 's' : ''}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||
disabled={isSaving}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || selectedDays.length === 0}
|
||||
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
Enregistrement...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4" />
|
||||
Enregistrer {selectedDays.length} jours
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompteRenduActivites;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,12 +13,13 @@ const EmployeeDetails = () => {
|
||||
fetchEmployeeData();
|
||||
}, [id]);
|
||||
|
||||
// Dans EmployeeDetails.jsx, modifier fetchEmployeeData:
|
||||
const fetchEmployeeData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 1️⃣ Données employé
|
||||
const resEmployee = await fetch(`http://localhost/GTA/project/public/php/getEmploye.php?id=${id}`);
|
||||
// 1️⃣ Données employé (avec compteurs inclus)
|
||||
const resEmployee = await fetch(`http://localhost:3000/getEmploye?id=${id}`);
|
||||
const dataEmployee = await resEmployee.json();
|
||||
console.log("Réponse API employé:", dataEmployee);
|
||||
|
||||
@@ -26,23 +27,16 @@ const EmployeeDetails = () => {
|
||||
setEmployee(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Les compteurs sont déjà dans la réponse
|
||||
setEmployee(dataEmployee.employee);
|
||||
|
||||
// 2️⃣ Historique des demandes
|
||||
const resRequests = await fetch(`http://localhost/GTA/project/public/php/getEmployeRequest.php?id=${id}`);
|
||||
const resRequests = await fetch(`http://localhost:3000/getEmployeRequest?id=${id}`);
|
||||
const dataRequests = await resRequests.json();
|
||||
setRequests(dataRequests.requests || []);
|
||||
|
||||
// 3️⃣ Compteurs de congés et RTT
|
||||
const resCounters = await fetch(`http://localhost/GTA/project/public/php/getLeaveCounters.php?user_id=${id}`);
|
||||
const dataCounters = await resCounters.json();
|
||||
|
||||
if (dataCounters.success) {
|
||||
setEmployee(prev => ({
|
||||
...prev,
|
||||
conges_restants: dataCounters.counters.availableCP,
|
||||
rtt_restants: dataCounters.counters.availableRTT
|
||||
}));
|
||||
if (dataRequests.success) {
|
||||
setRequests(dataRequests.requests || []);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
|
||||
0
project/src/pages/LeaveScheduling.jsx
Normal file
0
project/src/pages/LeaveScheduling.jsx
Normal file
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Building2, Mail, Lock, Eye, EyeOff, AlertTriangle } from 'lucide-react';
|
||||
import { Building2, AlertTriangle } from 'lucide-react';
|
||||
|
||||
const Login = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -9,10 +9,10 @@ const Login = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [authMethod, setAuthMethod] = useState(''); // Pour tracker la méthode d'auth utilisée
|
||||
const [authMethod, setAuthMethod] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { login, loginWithO365, isAuthorized } = useAuth();
|
||||
const { login, loginWithO365 } = useAuth();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -40,7 +40,6 @@ const Login = () => {
|
||||
setAuthMethod('o365');
|
||||
|
||||
try {
|
||||
// Étape 1 : Login O365
|
||||
const success = await loginWithO365();
|
||||
|
||||
if (!success) {
|
||||
@@ -49,30 +48,7 @@ const Login = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Étape 2 : Récupération du token d’authentification (si ton context le fournit)
|
||||
const token = localStorage.getItem("o365_token");
|
||||
// ⚠️ Ici j’imagine que tu stockes ton token quelque part (dans ton AuthContext ou localStorage).
|
||||
// Adapte selon ton implémentation de loginWithO365
|
||||
|
||||
// Étape 3 : Appel de ton API PHP
|
||||
const response = await fetch("http://localhost/GTA/project/public/php/initial-sync.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Résultat syncGroups :", data);
|
||||
|
||||
if (!data.success) {
|
||||
setError("Erreur de synchronisation des groupes : " + data.message);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Étape 4 : Redirection vers le dashboard
|
||||
// Redirection vers le dashboard
|
||||
navigate('/dashboard');
|
||||
|
||||
} catch (error) {
|
||||
@@ -90,8 +66,8 @@ const Login = () => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
|
||||
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"
|
||||
@@ -105,17 +81,20 @@ const Login = () => {
|
||||
<div className="max-w-md w-full">
|
||||
<div className="bg-white rounded-2xl shadow-xl p-6 lg:p-8">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-6 lg:mb-8">
|
||||
<div className="w-12 h-12 lg:w-16 lg:h-16 bg-cyan-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<Building2 className="w-6 h-6 lg:w-8 lg:h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-xl lg:text-2xl font-bold text-gray-900">GTA</h1>
|
||||
<p className="text-sm lg:text-base text-gray-600">Gestion de congés</p>
|
||||
<div className="text-center mb-4">
|
||||
<img
|
||||
src="/assets/GA.svg"
|
||||
alt="GTA Logo"
|
||||
className="h-36 lg:h-40 w-auto mx-auto"
|
||||
/>
|
||||
<p className="text-lg lg:text-xl font-semibold mb-6" style={{ color: '#7e5aa2' }}>
|
||||
GESTION DES TEMPS ET DES ACTIVITÉS
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Connexion Office 365 prioritaire */}
|
||||
<div className="mb-6">
|
||||
{/* Bouton Office 365 */}
|
||||
<div>
|
||||
<button
|
||||
data-testid="o365-login-btn"
|
||||
onClick={handleO365Login}
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
@@ -134,42 +113,29 @@ const Login = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Formulaire classique */}
|
||||
|
||||
|
||||
|
||||
{/* Affichage des erreurs */}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-start space-x-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-red-700 text-sm font-medium">
|
||||
{error.includes('Accès refusé') ? 'Accès refusé' : 'Erreur de connexion'}
|
||||
{/* Message d'erreur */}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg mt-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-red-700 text-sm font-medium">
|
||||
{error.includes('Accès refusé') ? 'Accès refusé' : 'Erreur de connexion'}
|
||||
</p>
|
||||
<p className="text-red-600 text-xs mt-1">{error}</p>
|
||||
{error.includes('groupe autorisé') && (
|
||||
<p className="text-red-600 text-xs mt-2">
|
||||
Contactez votre administrateur pour être ajouté aux groupes appropriés.
|
||||
</p>
|
||||
<p className="text-red-600 text-xs mt-1">{error}</p>
|
||||
{error.includes('groupe autorisé') && (
|
||||
<p className="text-red-600 text-xs mt-2">
|
||||
Contactez votre administrateur pour être ajouté aux groupes appropriés.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Info sur l'authentification */}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import { Users, CheckCircle, XCircle, Clock, Calendar, FileText, Menu, Eye, MessageSquare } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Sidebar from "../components/Sidebar";
|
||||
import {
|
||||
Users,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
FileText,
|
||||
Eye,
|
||||
Check,
|
||||
X,
|
||||
MessageSquare,
|
||||
} from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
const Manager = () => {
|
||||
const { user } = useAuth();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const isEmployee = user?.role === 'validateur';
|
||||
const isEmployee = user?.role === "Collaborateur" || user?.role === "Apprenti";
|
||||
|
||||
const [teamMembers, setTeamMembers] = useState([]);
|
||||
const [pendingRequests, setPendingRequests] = useState([]);
|
||||
const [allRequests, setAllRequests] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedRequest, setSelectedRequest] = useState(null);
|
||||
const [showValidationModal, setShowValidationModal] = useState(false);
|
||||
const [validationComment, setValidationComment] = useState('');
|
||||
const [validationAction, setValidationAction] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const [toast, setToast] = useState(null);
|
||||
const [validationModal, setValidationModal] = useState(null);
|
||||
const [comment, setComment] = useState("");
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
fetchTeamData();
|
||||
}
|
||||
if (user?.id) fetchTeamData();
|
||||
}, [user]);
|
||||
|
||||
const fetchTeamData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Récupérer les membres de l'équipe
|
||||
await fetchTeamMembers();
|
||||
|
||||
// Récupérer les demandes en attente
|
||||
await fetchPendingRequests();
|
||||
|
||||
// Récupérer toutes les demandes de l'équipe
|
||||
await fetchAllTeamRequests();
|
||||
|
||||
await Promise.all([
|
||||
fetchTeamMembers(),
|
||||
fetchPendingRequests(),
|
||||
fetchAllTeamRequests(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des données équipe:', error);
|
||||
console.error("Erreur lors du chargement:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -46,483 +51,451 @@ const Manager = () => {
|
||||
|
||||
const fetchTeamMembers = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost/GTA/project/public/php/getTeamMembers.php?manager_id=${user.id}`);
|
||||
const text = await response.text();
|
||||
console.log('Réponse équipe:', text);
|
||||
|
||||
const data = JSON.parse(text);
|
||||
if (data.success) {
|
||||
setTeamMembers(data.team_members || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération équipe:', error);
|
||||
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([]);
|
||||
} catch {
|
||||
setTeamMembers([]);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPendingRequests = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost/GTA/project/public/php/getPendingRequests.php?manager_id=${user.id}`);
|
||||
const text = await response.text();
|
||||
console.log('Réponse demandes en attente:', text);
|
||||
|
||||
const data = JSON.parse(text);
|
||||
if (data.success) {
|
||||
setPendingRequests(data.requests || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération demandes en attente:', error);
|
||||
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([]);
|
||||
} catch {
|
||||
setPendingRequests([]);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAllTeamRequests = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost/GTA/project/public/php/getAllTeamRequests.php?SuperieurId=${user.id}`);
|
||||
const text = await response.text();
|
||||
console.log('Réponse toutes demandes équipe:', text);
|
||||
|
||||
const data = JSON.parse(text);
|
||||
if (data.success) {
|
||||
setAllRequests(data.requests || []);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
console.error('Erreur récupération toutes demandes:', error);
|
||||
console.log('Réponse brute:', text);
|
||||
const res = await fetch(`http://localhost:3000/getAllTeamRequests?SuperieurId=${user.id}`);
|
||||
const data = await res.json();
|
||||
if (data.success) setAllRequests(data.requests || []);
|
||||
else setAllRequests([]);
|
||||
} catch {
|
||||
setAllRequests([]);
|
||||
}
|
||||
};
|
||||
|
||||
const openValidationModal = (request, action) => {
|
||||
setValidationModal({ request, action });
|
||||
setComment("");
|
||||
};
|
||||
|
||||
const closeValidationModal = () => {
|
||||
setValidationModal(null);
|
||||
setComment("");
|
||||
};
|
||||
|
||||
const confirmValidation = async () => {
|
||||
const { request, action } = validationModal;
|
||||
|
||||
if (action === "reject" && !comment.trim()) {
|
||||
showToast("error", "Un commentaire est obligatoire pour refuser une demande");
|
||||
return;
|
||||
}
|
||||
|
||||
await handleValidateRequest(request.id, action, comment);
|
||||
closeValidationModal();
|
||||
};
|
||||
|
||||
const handleValidateRequest = async (requestId, action, comment = '') => {
|
||||
if (!user || !user.id) {
|
||||
alert('❌ Utilisateur non identifié');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost/GTA/project/public/php/validateRequest.php', {
|
||||
setIsValidating(true); // ✅ Maintenant défini
|
||||
|
||||
const response = await fetch('http://localhost:3000/validateRequest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
action: action, // 'approve' ou 'reject'
|
||||
comment: comment,
|
||||
validator_id: user.id
|
||||
action: action,
|
||||
validator_id: user.id,
|
||||
comment: comment
|
||||
}),
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
console.log('Réponse validation:', text);
|
||||
|
||||
const data = JSON.parse(text);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
|
||||
// Rafraîchir les données
|
||||
await fetchTeamData();
|
||||
setShowValidationModal(false);
|
||||
setSelectedRequest(null);
|
||||
setValidationComment('');
|
||||
|
||||
alert(`Demande ${action === 'approve' ? 'approuvée' : 'refusée'} avec succès !`);
|
||||
await Promise.all([
|
||||
fetchPendingRequests(),
|
||||
fetchAllTeamRequests()
|
||||
]);
|
||||
} else {
|
||||
alert(`Erreur: ${data.message}`);
|
||||
alert(`❌ Erreur : ${data.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur validation:', error);
|
||||
alert('Erreur lors de la validation');
|
||||
console.error('❌ Erreur lors de la validation:', error);
|
||||
alert('❌ Erreur lors de la validation de la demande');
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openValidationModal = (request, action) => {
|
||||
setSelectedRequest(request);
|
||||
setValidationAction(action);
|
||||
setValidationComment('');
|
||||
setShowValidationModal(true);
|
||||
const showToast = (type, message) => {
|
||||
setToast({ type, message });
|
||||
setTimeout(() => setToast(null), 4000);
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'En attente': return 'bg-yellow-100 text-yellow-800';
|
||||
case 'Validée':
|
||||
case 'Approuvé': return 'bg-green-100 text-green-800';
|
||||
case 'Refusée': return 'bg-red-100 text-red-800';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
case "En attente": return "bg-yellow-100 text-yellow-800";
|
||||
case "Validée":
|
||||
case "Approuvé": return "bg-green-100 text-green-800";
|
||||
case "Refusée": return "bg-red-100 text-red-800";
|
||||
default: return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeColor = (type) => {
|
||||
switch (type) {
|
||||
case 'Congés payés':
|
||||
case 'Congé payé': return 'bg-blue-100 text-blue-800';
|
||||
case 'RTT': return 'bg-green-100 text-green-800';
|
||||
case 'Congé maladie': return 'bg-red-100 text-red-800';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
case "Congés payés":
|
||||
case "Congé payé": return "bg-blue-100 text-blue-800";
|
||||
case "RTT": return "bg-green-100 text-green-800";
|
||||
case "Congé maladie": return "bg-red-100 text-red-800";
|
||||
default: return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
const EmptyBackground = ({ icon: Icon, title, subtitle }) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="absolute inset-0 flex flex-col items-center justify-center bg-gradient-to-b from-gray-50 to-gray-100 text-gray-500 pointer-events-none"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ y: [0, -8, 0] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="bg-gray-200 p-5 rounded-full shadow-inner mb-4"
|
||||
>
|
||||
<Icon className="w-12 h-12 text-gray-400" />
|
||||
</motion.div>
|
||||
<h2 className="text-xl font-semibold mb-1 text-gray-700">{title}</h2>
|
||||
<p className="text-sm text-gray-500">{subtitle}</p>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||
<div className="lg:ml-60 flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Chargement des données équipe...</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 mx-auto mb-3"></div>
|
||||
<p className="text-gray-600">Chargement des données...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||
<div className="relative min-h-screen bg-gray-50 flex overflow-hidden">
|
||||
{/* Toast Notification */}
|
||||
<AnimatePresence>
|
||||
{toast && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -50, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||
className="fixed top-6 left-1/2 transform -translate-x-1/2 z-50 max-w-md w-full mx-4"
|
||||
>
|
||||
<div className={`rounded-xl shadow-2xl p-4 flex items-center gap-3 backdrop-blur-sm border-2 ${toast.type === "success" ? "bg-green-50 border-green-500 text-green-900" : "bg-red-50 border-red-500 text-red-900"
|
||||
}`}>
|
||||
<div className={`p-2 rounded-full ${toast.type === "success" ? "bg-green-500" : "bg-red-500"}`}>
|
||||
{toast.type === "success" ? (
|
||||
<Check className="w-5 h-5 text-white" />
|
||||
) : (
|
||||
<X className="w-5 h-5 text-white" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-sm">{toast.message}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setToast(null)}
|
||||
className={`p-1 rounded-lg transition ${toast.type === "success" ? "hover:bg-green-200" : "hover:bg-red-200"}`}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="flex-1 lg:ml-60">
|
||||
<div className="p-4 lg:p-8 w-full">
|
||||
{/* Mobile menu button */}
|
||||
<div className="lg:hidden mb-4">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="p-2 rounded-lg bg-white shadow-sm border border-gray-200"
|
||||
{/* Modal de validation */}
|
||||
<AnimatePresence>
|
||||
{validationModal && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||
onClick={closeValidationModal}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-white rounded-xl shadow-2xl max-w-md w-full"
|
||||
>
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<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 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6 mb-8">
|
||||
<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">Équipe</p>
|
||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">{teamMembers.length}</p>
|
||||
<p className="text-xs text-gray-500">membres</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Users className="w-4 h-4 lg:w-6 lg:h-6 text-blue-600" />
|
||||
<div className="p-6 border-b border-gray-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-full ${validationModal.action === "approve" ? "bg-green-100" : "bg-red-100"
|
||||
}`}>
|
||||
{validationModal.action === "approve" ? (
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="w-6 h-6 text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{validationModal.action === "approve" ? "Approuver la demande" : "Refuser la demande"}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">{validationModal.request.employee_name}</p>
|
||||
</div>
|
||||
</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">En attente</p>
|
||||
<p className="text-xl lg:text-2xl font-bold text-gray-900">{pendingRequests.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-yellow-100 rounded-lg flex items-center justify-center">
|
||||
<Clock className="w-4 h-4 lg:w-6 lg:h-6 text-yellow-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">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 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Demandes en attente */}
|
||||
{!isEmployee && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||
<h2 className="text-lg lg:text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-yellow-600" />
|
||||
Demandes en attente ({pendingRequests.length})
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-4 lg:p-6">
|
||||
{pendingRequests.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Clock className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-600">Aucune demande en attente</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{pendingRequests.map((request) => (
|
||||
<div key={request.id} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-medium text-gray-900">{request.employee_name}</h3>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(request.type)}`}>
|
||||
{request.type}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{request.date_display}</p>
|
||||
<p className="text-xs text-gray-500">Soumis le {request.submitted_display}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-gray-900">{request.days}j</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{request.reason && (
|
||||
<div className="mb-3 p-2 bg-gray-50 rounded text-sm text-gray-700">
|
||||
<strong>Motif:</strong> {request.reason}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => openValidationModal(request, 'approve')}
|
||||
className="flex-1 bg-green-600 text-white px-3 py-2 rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center gap-2 text-sm"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Approuver
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openValidationModal(request, 'reject')}
|
||||
className="flex-1 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 transition-colors flex items-center justify-center gap-2 text-sm"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
Refuser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="bg-gray-50 rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Type</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(validationModal.request.type)}`}>
|
||||
{validationModal.request.type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Période</span>
|
||||
<span className="text-sm font-medium text-gray-900">{validationModal.request.date_display}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Durée</span>
|
||||
<span className="text-sm font-medium text-gray-900">{validationModal.request.days} jour(s)</span>
|
||||
</div>
|
||||
{validationModal.request.reason && (
|
||||
<div className="pt-2 border-t border-gray-200">
|
||||
<p className="text-xs text-gray-500 mb-1">Motif :</p>
|
||||
<p className="text-sm text-gray-700">{validationModal.request.reason}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Équipe */}
|
||||
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 ${isEmployee ? 'lg:col-span-2' : ''}`}>
|
||||
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||
<h2 className="text-lg lg:text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
Mon équipe ({teamMembers.length})
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-4 lg:p-6">
|
||||
{teamMembers.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Users className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-600">Aucun membre d'équipe</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{teamMembers.map((member) => (
|
||||
<div key={member.id}
|
||||
onClick={() => navigate(`/employee/${member.id}`)}
|
||||
className={`flex items-center justify-between p-3 bg-gray-50 rounded-lg ${isEmployee ? 'lg:p-4' : ''}`}>
|
||||
<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">
|
||||
{member.prenom?.charAt(0)}{member.nom?.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{member.prenom} {member.nom}</p>
|
||||
<p className="text-sm text-gray-600">{member.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!isEmployee && (
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{allRequests.filter(r => r.employee_id === member.id && r.status === 'En attente').length} en attente
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{allRequests.filter(r => r.employee_id === member.id).length} total
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Historique des demandes */}
|
||||
{!isEmployee && (
|
||||
<div className="mt-6 bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="p-4 lg:p-6 border-b border-gray-100">
|
||||
<h2 className="text-lg lg:text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-gray-600" />
|
||||
Historique des demandes ({allRequests.length})
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-4 lg:p-6">
|
||||
{allRequests.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-600">Aucune demande</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-80 overflow-y-auto">
|
||||
{allRequests.map((request) => (
|
||||
<div key={request.id} className="p-3 border border-gray-100 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<p className="font-medium text-gray-900">{request.employee_name}</p>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(request.type)}`}>
|
||||
{request.type}
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(request.status)}`}>
|
||||
{request.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{request.date_display}</p>
|
||||
<p className="text-xs text-gray-500 mb-2">Soumis le {request.submitted_display}</p>
|
||||
|
||||
{request.reason && (
|
||||
<p className="text-sm text-gray-700 mb-1"><strong>Motif :</strong> {request.reason}</p>
|
||||
)}
|
||||
|
||||
{request.file && (
|
||||
<div className="text-sm mt-1">
|
||||
<p className="text-gray-500">Document joint</p>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
Voir le fichier
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-right mt-2">
|
||||
<p className="font-medium text-gray-900">{request.days}j</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal de validation */}
|
||||
|
||||
{showValidationModal && selectedRequest && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-md w-full">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-100">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{validationAction === 'approve' ? 'Approuver' : 'Refuser'} la demande
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Corps du contenu */}
|
||||
<div className="p-6">
|
||||
<div className="mb-4 p-4 bg-gray-50 rounded-lg">
|
||||
<p className="font-medium text-gray-900">{selectedRequest.employee_name}</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{selectedRequest.type} - {selectedRequest.date_display}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{selectedRequest.days} jour(s)</p>
|
||||
|
||||
{selectedRequest.reason && (
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
<strong>Motif:</strong> {selectedRequest.reason}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{selectedRequest.file && (
|
||||
<div>
|
||||
<p className="text-gray-500">Document joint</p>
|
||||
<a
|
||||
href={`http://localhost/GTA/project/uploads/${selectedRequest.file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline flex items-center gap-2"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
Voir le fichier
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
Commentaire{" "}
|
||||
{validationModal.action === "reject" && <span className="text-red-600">*</span>}
|
||||
{validationModal.action === "approve" && <span className="text-gray-400 font-normal">(optionnel)</span>}
|
||||
</label>
|
||||
<textarea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder={validationModal.action === "approve" ? "Ajouter un commentaire..." : "Expliquer le motif du refus..."}
|
||||
rows={4}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:outline-none resize-none ${validationModal.action === "reject" && !comment.trim()
|
||||
? "border-red-300 focus:ring-red-500 focus:border-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500 focus:border-blue-500"
|
||||
}`}
|
||||
/>
|
||||
{validationModal.action === "reject" && !comment.trim() && (
|
||||
<p className="text-xs text-red-600 mt-1">Un commentaire est obligatoire pour un refus</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Champ commentaire */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Commentaire {validationAction === 'reject' ? '(obligatoire)' : '(optionnel)'}
|
||||
</label>
|
||||
<textarea
|
||||
value={validationComment}
|
||||
onChange={(e) => setValidationComment(e.target.value)}
|
||||
placeholder={validationAction === 'approve' ? 'Commentaire optionnel...' : 'Motif du refus...'}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Boutons */}
|
||||
<div className="flex gap-3">
|
||||
<div className="p-6 border-t border-gray-100 flex gap-3">
|
||||
<button
|
||||
onClick={() => setShowValidationModal(false)}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
onClick={closeValidationModal}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition font-medium"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleValidateRequest(selectedRequest.id, validationAction, validationComment)
|
||||
}
|
||||
disabled={validationAction === 'reject' && !validationComment.trim()}
|
||||
className={`flex-1 px-4 py-2 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${validationAction === 'approve'
|
||||
? 'bg-green-600 hover:bg-green-700'
|
||||
: 'bg-red-600 hover:bg-red-700'
|
||||
onClick={confirmValidation}
|
||||
disabled={validationModal.action === "reject" && !comment.trim()}
|
||||
className={`flex-1 px-4 py-2 text-white rounded-lg transition font-medium disabled:opacity-50 disabled:cursor-not-allowed ${validationModal.action === "approve" ? "bg-green-600 hover:bg-green-700" : "bg-red-600 hover:bg-red-700"
|
||||
}`}
|
||||
>
|
||||
{validationAction === 'approve' ? 'Approuver' : 'Refuser'}
|
||||
{validationModal.action === "approve" ? "Approuver" : "Refuser"}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Fond animé si aucune donnée */}
|
||||
{!isLoading && teamMembers.length === 0 && pendingRequests.length === 0 && allRequests.length === 0 && (
|
||||
<EmptyBackground
|
||||
icon={Users}
|
||||
title="Bienvenue dans la gestion d'équipe 👋"
|
||||
subtitle="Les demandes et collaborateurs apparaîtront ici dès qu'ils seront disponibles."
|
||||
/>
|
||||
)}
|
||||
|
||||
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||
|
||||
<div className="flex-1 lg:ml-60 p-6 space-y-8 relative z-10">
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
{isEmployee ? "Mon équipe 👥" : "Gestion d'équipe 👥"}
|
||||
</h1>
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{pendingRequests.length === 0 ? (
|
||||
<p className="text-center text-gray-500">Aucune demande en attente</p>
|
||||
) : (
|
||||
pendingRequests.map((r) => (
|
||||
<div key={r.id} className="border p-4 rounded-lg bg-gray-50 hover:bg-gray-100 transition">
|
||||
<div className="flex justify-between mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{r.employee_name}</p>
|
||||
<p className="text-sm text-gray-600">{r.date_display}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(r.type)}`}>
|
||||
{r.type}
|
||||
</span>
|
||||
</div>
|
||||
{r.reason && (
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
<strong>Motif:</strong> {r.reason}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<XCircle className="w-4 h-4 inline mr-1" />
|
||||
Refuser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div className="p-4 space-y-2">
|
||||
{teamMembers.length === 0 ? (
|
||||
<p className="text-center text-gray-500">Aucun membre d'équipe</p>
|
||||
) : (
|
||||
teamMembers.map((m) => (
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
{m.prenom?.charAt(0)}{m.nom?.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{m.prenom} {m.nom}</p>
|
||||
<p className="text-sm text-gray-600">{m.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!isEmployee && (
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{allRequests.filter((r) => r.employee_id === m.id && r.status === "En attente").length} en attente
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{allRequests.filter((r) => r.employee_id === m.id).length} total
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Manager;
|
||||
{!isEmployee && (
|
||||
<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>
|
||||
</div>
|
||||
<div className="p-4 space-y-3 max-h-80 overflow-y-auto">
|
||||
{allRequests.length === 0 ? (
|
||||
<p className="text-center text-gray-500">Aucune demande</p>
|
||||
) : (
|
||||
allRequests.map((r) => (
|
||||
<div key={r.id} className="p-3 border border-gray-100 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<p className="font-medium text-gray-900">{r.employee_name}</p>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(r.type)}`}>
|
||||
{r.type}
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(r.status)}`}>
|
||||
{r.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{r.date_display}</p>
|
||||
<p className="text-xs text-gray-500 mb-2">Soumis le {r.submitted_display}</p>
|
||||
{r.reason && (
|
||||
<p className="text-sm text-gray-700 mb-1">
|
||||
<strong>Motif :</strong> {r.reason}
|
||||
</p>
|
||||
)}
|
||||
{r.file && (
|
||||
<div className="text-sm mt-1">
|
||||
<p className="text-gray-500">Document joint</p>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
Voir le fichier
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default Manager;
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user