Vue global collaborateur pour manager

This commit is contained in:
2025-08-28 11:59:58 +02:00
parent ed4a7c02ca
commit ac0ae03904
14 changed files with 733 additions and 268 deletions

View File

@@ -7,35 +7,73 @@ import Requests from './pages/Requests';
import Calendar from './pages/Calendar';
import Manager from './pages/Manager';
import ProtectedRoute from './components/ProtectedRoute';
import EmployeeDetails from './pages/EmployeeDetails';
import EmployeeDetails from './pages/EmployeeDetails';
import Collaborateur from './pages/Collaborateur';
function App() {
return (
<AuthProvider>
<Router>
<Routes>
{/* Route publique */}
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
<Route path="/demandes" element={
<ProtectedRoute>
<Requests />
</ProtectedRoute>
} />
<Route path="/calendrier" element={
<ProtectedRoute>
<Calendar />
</ProtectedRoute>
} />
<Route path="/manager" element={
<ProtectedRoute>
<Manager />
</ProtectedRoute>
} />
<Route path="/employee/:id" element={<ProtectedRoute><EmployeeDetails /></ProtectedRoute>} />
{/* Routes protégées */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/demandes"
element={
<ProtectedRoute allowedRoles={['Collaborateur', 'RH']}>
<Requests />
</ProtectedRoute>
}
/>
<Route
path="/calendrier"
element={
<ProtectedRoute allowedRoles={['Collaborateur', 'Manager', 'RH']}>
<Calendar />
</ProtectedRoute>
}
/>
<Route
path="/manager"
element={
<ProtectedRoute allowedRoles={['Manager']}>
<Manager />
</ProtectedRoute>
}
/>
<Route
path="/collaborateur"
element={
<ProtectedRoute allowedRoles={['Collaborateur']}>
<Collaborateur />
</ProtectedRoute>
}
/>
<Route
path="/employee/:id"
element={
<ProtectedRoute allowedRoles={['RH', 'Manager']}>
<EmployeeDetails />
</ProtectedRoute>
}
/>
{/* Redirection par défaut */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Router>
@@ -43,4 +81,4 @@ function App() {
);
}
export default App;
export default App;

View File

@@ -15,8 +15,10 @@ const Sidebar = ({ isOpen, onToggle }) => {
return 'bg-red-100 text-red-800';
case 'Validateur':
return 'bg-green-100 text-green-800';
case 'Collaborateur':
return 'bg-cyan-600 text-white';
default:
return 'bg-blue-100 text-blue-800';
return 'bg-gray-100 text-gray-800';
}
};
@@ -29,10 +31,12 @@ const Sidebar = ({ isOpen, onToggle }) => {
/>
)}
<div className={`
fixed inset-y-0 left-0 z-50 w-60 bg-white border-r border-gray-200 min-h-screen flex flex-col transform transition-transform duration-300 ease-in-out
${isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<div
className={`
fixed inset-y-0 left-0 z-50 w-60 bg-white border-r border-gray-200 min-h-screen flex flex-col transform transition-transform duration-300 ease-in-out
${isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}
>
{/* Bouton fermer (mobile) */}
<div className="lg:hidden flex justify-end p-4">
<button onClick={onToggle} className="p-2 rounded-lg hover:bg-gray-100">
@@ -43,7 +47,7 @@ const Sidebar = ({ isOpen, onToggle }) => {
{/* Logo */}
<div className="p-6 border-b border-gray-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
<div className="w-10 h-10 bg-cyan-600 rounded-lg flex items-center justify-center">
<Building2 className="w-6 h-6 text-white" />
</div>
<div>
@@ -69,7 +73,11 @@ const Sidebar = ({ isOpen, onToggle }) => {
{user?.service || "Service non défini"}
</p>
{user?.role && (
<span className={`inline-block mt-2 px-3 py-1 text-xs font-medium rounded-full ${getRoleBadgeClass(user.role)}`}>
<span
className={`inline-block mt-2 px-3 py-1 text-xs font-medium rounded-full ${getRoleBadgeClass(
user.role
)}`}
>
{user.role}
</span>
)}
@@ -82,7 +90,9 @@ const Sidebar = ({ isOpen, onToggle }) => {
<Link
to="/dashboard"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/dashboard") ? "bg-blue-50 text-blue-700 border-r-2 border-blue-700" : "text-gray-700 hover:bg-gray-50"
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/dashboard")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Home className="w-5 h-5" />
@@ -92,7 +102,9 @@ const Sidebar = ({ isOpen, onToggle }) => {
<Link
to="/demandes"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/demandes") ? "bg-blue-50 text-blue-700 border-r-2 border-blue-700" : "text-gray-700 hover:bg-gray-50"
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/demandes")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<FileText className="w-5 h-5" />
@@ -102,26 +114,37 @@ const Sidebar = ({ isOpen, onToggle }) => {
<Link
to="/calendrier"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/calendrier") ? "bg-blue-50 text-blue-700 border-r-2 border-blue-700" : "text-gray-700 hover:bg-gray-50"
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/calendrier")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Calendar className="w-5 h-5" />
<span className="font-medium">Calendrier</span>
</Link>
{(user?.role === 'Validateur' || user?.role === 'Admin' || user?.role === 'Collaborateur') && (
<Link
to="/manager"
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive("/manager") ? "bg-blue-50 text-blue-700 border-r-2 border-blue-700" : "text-gray-700 hover:bg-gray-50"
}`}
>
<Users className="w-5 h-5" />
<span className="font-medium">
{user?.role === 'Collaborateur' ? 'Mon équipe' : 'Équipe'}
</span>
</Link>
)}
{/* Rubrique dynamique Collaborateur / Validateur */}
{(user?.role === "Collaborateur" ||
user?.role === "Validateur" ||
user?.role === "Manager" ||
user?.role === "RH" ||
user?.role === "Admin") && (
<Link
to={user?.role === "Collaborateur" ? "/collaborateur" : "/manager"}
onClick={() => window.innerWidth < 1024 && onToggle()}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(user?.role === "Collaborateur" ? "/collaborateur" : "/manager")
? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
: "text-gray-700 hover:bg-gray-50"
}`}
>
<Users className="w-5 h-5" />
<span className="font-medium">
{user?.role === "Collaborateur"
? "Mon équipe"
: "Mon équipe"}
</span>
</Link>
)}
</nav>
{/* Bouton déconnexion */}

View File

@@ -118,13 +118,14 @@ export const AuthProvider = ({ children }) => {
initializeMsal();
}, []);
// Gérer l'authentification réussie
// Gérer l'authentification réussie
const handleSuccessfulAuth = async (authResponse) => {
try {
const account = authResponse.account;
const accessToken = authResponse.accessToken;
// Récupérer profil Microsoft Graph
// 🔹 Récupérer profil Microsoft Graph
const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
@@ -143,8 +144,9 @@ export const AuthProvider = ({ children }) => {
// 🔹 Synchroniser lutilisateur dans la DB
const syncResult = await syncUserToDatabase(entraUser, accessToken);
console.log("Résultat syncUserToDatabase:", syncResult);
// 🚀 NEW : si admin → lancer full-sync.php
// 🚀 Si admin → lancer full-sync.php
if (syncResult?.role === "Admin") {
try {
const syncResp = await fetch(getApiUrl('full-sync.php'), {
@@ -171,7 +173,13 @@ export const AuthProvider = ({ children }) => {
email: entraUser.mail || entraUser.userPrincipalName,
userPrincipalName: entraUser.userPrincipalName,
role: syncResult?.role || 'Employe',
service: syncResult?.service || 'Non défini',
// ✅ Correction ici
service: syncResult?.service
|| syncResult?.user?.service
|| entraUser.department
|| 'Non défini',
jobTitle: entraUser.jobTitle,
department: entraUser.department,
officeLocation: entraUser.officeLocation,
@@ -191,6 +199,7 @@ export const AuthProvider = ({ children }) => {
};
// Connexion classique (email/mot de passe)
const login = async (email, password) => {
try {

View File

@@ -414,7 +414,7 @@ END:VEVENT`;
)}
<button
onClick={() => setShowNewRequestModal(true)}
className="bg-blue-600 text-white px-3 lg:px-6 py-2 lg:py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
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"
>
<Plus className="w-5 h-5" />
<span className="hidden sm:inline">Nouvelle demande</span>
@@ -634,10 +634,10 @@ END:VEVENT`;
{/* button csv */}
<div className="p-4">
<div className="flex justify-end gap-2 mb-4">
<button onClick={() => exportTeamLeavesToGoogleCSV(teamLeaves)} className="bg-blue-600 text-white px-3 py-2 rounded hover:bg-blue-700">
<button onClick={() => exportTeamLeavesToGoogleCSV(teamLeaves)} className="bg-cyan-600 text-white px-3 py-2 rounded hover:bg-blue-700">
Export CSV
</button>
<button onClick={() => exportTeamLeavesToICS(teamLeaves)} className="bg-green-600 text-white px-3 py-2 rounded hover:bg-green-700">
<button onClick={() => exportTeamLeavesToICS(teamLeaves)} className="bg-purple-500 text-white px-3 py-2 rounded hover:bg-green-700">
Export ICS
</button>
</div>

View File

@@ -0,0 +1,524 @@
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';
const Collaborateur = () => {
const { user } = useAuth();
const [sidebarOpen, setSidebarOpen] = useState(false);
const isEmployee = user?.role === 'Collaborateur';
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('');
useEffect(() => {
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();
} catch (error) {
console.error('Erreur lors de la récupération des données équipe:', error);
} finally {
setIsLoading(false);
}
};
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);
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);
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);
setAllRequests([]);
}
};
const handleValidateRequest = async (requestId, action, comment = '') => {
try {
const response = await fetch('http://localhost/GTA/project/public/php/validateRequest.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
request_id: requestId,
action: action, // 'approve' ou 'reject'
comment: comment,
validator_id: user.id
}),
});
const text = await response.text();
console.log('Réponse validation:', text);
const data = JSON.parse(text);
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 !`);
} else {
alert(`Erreur: ${data.message}`);
}
} catch (error) {
console.error('Erreur validation:', error);
alert('Erreur lors de la validation');
}
};
const openValidationModal = (request, action) => {
setSelectedRequest(request);
setValidationAction(action);
setValidationComment('');
setShowValidationModal(true);
};
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';
}
};
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';
}
};
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>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 flex">
<Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
<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"
>
<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>
</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>
)}
</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} 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>
{/* 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">
<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"
>
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'
}`}
>
{validationAction === 'approve' ? 'Approuver' : 'Refuser'}
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default Collaborateur;

View File

@@ -179,7 +179,7 @@ const Dashboard = () => {
<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>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-cyan-600 mx-auto mb-4"></div>
<p className="text-gray-600">Chargement des données...</p>
</div>
</div>
@@ -226,7 +226,7 @@ const Dashboard = () => {
)}
<button
onClick={() => setShowNewRequestModal(true)}
className="bg-blue-600 text-white px-3 lg:px-6 py-2 lg:py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
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"
>
<Plus className="w-5 h-5" />
<span className="hidden sm:inline">Nouvelle demande</span>
@@ -274,10 +274,10 @@ const Dashboard = () => {
<button
onClick={openManualResetPage}
className="flex items-center gap-3 p-4 border border-blue-200 rounded-lg hover:bg-blue-50 transition-colors text-left"
className="flex items-center gap-3 p-4 border border-cyan-600 rounded-lg hover:bg-cyan-50 transition-colors text-left"
>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<Settings className="w-5 h-5 text-blue-600" />
<div className="w-10 h-10 bg-cyan-600 rounded-lg flex items-center justify-center">
<Settings className="w-5 h-5 text-cyan-600" />
</div>
<div>
<h3 className="font-medium text-gray-900">Interface d'administration</h3>
@@ -297,8 +297,8 @@ const Dashboard = () => {
<p className="text-xl lg:text-2xl font-bold text-gray-900">{leaveCounters.availableCP}</p>
<p className="text-xs text-gray-500">jours</p>
</div>
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<CalendarIcon className="w-4 h-4 lg:w-6 lg:h-6 text-blue-600" />
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-cyan-100 rounded-lg flex items-center justify-center">
<CalendarIcon className="w-4 h-4 lg:w-6 lg:h-6 text-cyan-600" />
</div>
</div>
</div>
@@ -333,7 +333,7 @@ const Dashboard = () => {
<p className="text-gray-600 mb-4">Aucune demande récente</p>
<button
onClick={() => setShowNewRequestModal(true)}
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
className="text-blue-600 hover:text-cyan-800 text-sm font-medium"
>
Faire votre première demande
</button>

View File

@@ -20,6 +20,7 @@ const EmployeeDetails = () => {
// 1⃣ Données employé
const resEmployee = await fetch(`http://localhost/GTA/project/public/php/getEmploye.php?id=${id}`);
const dataEmployee = await resEmployee.json();
console.log("Réponse API employé:", dataEmployee);
if (!dataEmployee.success) {
setEmployee(null);

View File

@@ -106,7 +106,7 @@ const Login = () => {
<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-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
<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>
@@ -119,7 +119,7 @@ const Login = () => {
onClick={handleO365Login}
disabled={isLoading}
type="button"
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
className="w-full bg-cyan-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
>
{isLoading && authMethod === 'o365' ? (
<span>Connexion Office 365...</span>
@@ -134,65 +134,13 @@ const Login = () => {
</button>
</div>
{/* Séparateur */}
<div className="relative mb-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">ou</span>
</div>
</div>
{/* Formulaire classique */}
<form onSubmit={handleSubmit} className="space-y-4 lg:space-y-6">
<div>
<label htmlFor="email" className="block text-sm lg:text-base font-medium text-gray-700 mb-2">
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4 lg:w-5 lg:h-5" />
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pl-9 lg:pl-10 pr-4 py-2 lg:py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm lg:text-base"
placeholder="votre.email@entreprise.com"
required
disabled={isLoading}
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm lg:text-base font-medium text-gray-700 mb-2">
Mot de passe
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4 lg:w-5 lg:h-5" />
<input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-9 lg:pl-10 pr-10 lg:pr-12 py-2 lg:py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm lg:text-base"
placeholder="••••••••"
required
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-900 disabled:opacity-50"
disabled={isLoading}
title={showPassword ? "Masquer le mot de passe" : "Afficher le mot de passe"}
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{/* Affichage des erreurs */}
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
@@ -213,21 +161,11 @@ const Login = () => {
</div>
)}
<button
type="submit"
disabled={isLoading}
className="w-full bg-gray-600 text-white py-2 lg:py-3 px-4 rounded-lg font-medium hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm lg:text-base"
>
{isLoading && authMethod === 'local' ? 'Connexion...' : 'Connexion locale'}
</button>
</form>
{/* Info sur l'authentification */}
<div className="mt-6 text-center">
<p className="text-xs text-gray-500">
Utilisez votre compte Office 365 pour une connexion sécurisée
</p>
</div>
</div>
</div>
</div>

View File

@@ -2,11 +2,12 @@
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';
const Manager = () => {
const { user } = useAuth();
const [sidebarOpen, setSidebarOpen] = useState(false);
const isEmployee = user?.role === 'Employe';
const isEmployee = user?.role === 'validateur';
const [teamMembers, setTeamMembers] = useState([]);
const [pendingRequests, setPendingRequests] = useState([]);
const [allRequests, setAllRequests] = useState([]);
@@ -15,6 +16,7 @@ const Manager = () => {
const [showValidationModal, setShowValidationModal] = useState(false);
const [validationComment, setValidationComment] = useState('');
const [validationAction, setValidationAction] = useState('');
const navigate = useNavigate();
useEffect(() => {
if (user?.id) {
@@ -339,7 +341,9 @@ const Manager = () => {
) : (
<div className="space-y-3">
{teamMembers.map((member) => (
<div key={member.id} className={`flex items-center justify-between p-3 bg-gray-50 rounded-lg ${isEmployee ? 'lg:p-4' : ''}`}>
<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">

View File

@@ -199,7 +199,7 @@ const Requests = () => {
</div>
<div className="hidden lg:flex items-center gap-3">
<button onClick={() => setShowNewRequestModal(true)} className="bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2">
<button onClick={() => setShowNewRequestModal(true)} className="bg-cyan-600 text-white px-4 py-2 rounded-lg flex items-center gap-2">
<Plus className="w-4 h-4" /> Nouvelle demande
</button>
</div>