Vue global collaborateur pour manager
This commit is contained in:
524
project/src/pages/Collaborateur.jsx
Normal file
524
project/src/pages/Collaborateur.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user