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

@@ -130,7 +130,7 @@ echo json_encode([
"authorized" => true, "authorized" => true,
"role" => $role, "role" => $role,
"groups" => [$role], "groups" => [$role],
"localUserId" => (int)$newUserId, // 🔹 ajout important "localUserId" => (int)$newUserId,
"user" => [ "user" => [
"id" => $newUserId, "id" => $newUserId,
"entraUserId" => $entraUserId, "entraUserId" => $entraUserId,

View File

@@ -28,11 +28,12 @@ if ($id <= 0) {
} }
try { try {
$stmt = $conn->prepare(" $stmt = $conn->prepare("
SELECT id, Nom, Prenom, Email, Matricule, Telephone, Adresse SELECT id, Nom, Prenom, Email
FROM CollaborateurAD FROM CollaborateurAD
WHERE id = ? AND Actif = 1 WHERE id = ?
"); ");
$stmt->bind_param("i", $id); $stmt->bind_param("i", $id);
$stmt->execute(); $stmt->execute();
$result = $stmt->get_result(); $result = $stmt->get_result();

View File

@@ -13,7 +13,6 @@ if ($conn->connect_error) {
die(json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error])); die(json_encode(["success" => false, "message" => "Erreur DB: " . $conn->connect_error]));
} }
// --- Authentification (client credentials) ---
$tenantId = "9840a2a0-6ae1-4688-b03d-d2ec291be0f9"; $tenantId = "9840a2a0-6ae1-4688-b03d-d2ec291be0f9";
$clientId = "4bb4cc24-bac3-427c-b02c-5d14fc67b561"; $clientId = "4bb4cc24-bac3-427c-b02c-5d14fc67b561";
$clientSecret = "ViC8Q~n4F5YweE18wjS0kfhp3kHh6LB2gZ76_b4R"; $clientSecret = "ViC8Q~n4F5YweE18wjS0kfhp3kHh6LB2gZ76_b4R";
@@ -42,8 +41,9 @@ if (!$accessToken) {
} }
// --- ID du groupe cible (Ensup-Groupe) --- // --- ID du groupe cible (Ensup-Groupe) ---
$groupId = "c1ea877c-6bca-4f47-bfad-f223640813a0"; // 🔹 Mets l'Object ID de ton groupe ici $groupId = "c1ea877c-6bca-4f47-bfad-f223640813a0";
// --- Récupérer infos du groupe ---
$urlGroup = "https://graph.microsoft.com/v1.0/groups/$groupId?\$select=id,displayName,description,mail,createdDateTime"; $urlGroup = "https://graph.microsoft.com/v1.0/groups/$groupId?\$select=id,displayName,description,mail,createdDateTime";
$ch = curl_init($urlGroup); $ch = curl_init($urlGroup);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]); curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $accessToken"]);
@@ -57,31 +57,6 @@ if (!isset($group["id"])) {
} }
$displayName = $group["displayName"] ?? ""; $displayName = $group["displayName"] ?? "";
$description = $group["description"] ?? "";
$mail = $group["mail"] ?? "";
$createdAt = null;
if (!empty($group["createdDateTime"])) {
$dt = new DateTime($group["createdDateTime"]);
$createdAt = $dt->format("Y-m-d H:i:s"); // format MySQL
}
// --- Insérer / mettre à jour le groupe dans EntraGroups ---
$stmt = $conn->prepare("INSERT INTO EntraGroups (Id, DisplayName, Description, Mail, CreatedAt, UpdatedAt, SyncDate, IsActive)
VALUES (?, ?, ?, ?, ?, NOW(), NOW(), 1)
ON DUPLICATE KEY UPDATE
DisplayName=?, Description=?, Mail=?, UpdatedAt=NOW(), SyncDate=NOW(), IsActive=1");
if ($stmt) {
$stmt->bind_param("ssssssss",
$groupId, $displayName, $description, $mail, $createdAt,
$displayName, $description, $mail
);
$stmt->execute();
}
// --- Récupérer les membres du groupe --- // --- Récupérer les membres du groupe ---
$urlMembers = "https://graph.microsoft.com/v1.0/groups/$groupId/members?\$select=id,givenName,surname,mail,department,jobTitle"; $urlMembers = "https://graph.microsoft.com/v1.0/groups/$groupId/members?\$select=id,givenName,surname,mail,department,jobTitle";
@@ -100,23 +75,25 @@ foreach ($members as $m) {
$nom = $m["surname"] ?? ""; $nom = $m["surname"] ?? "";
$email = $m["mail"] ?? ""; $email = $m["mail"] ?? "";
$service = $m["department"] ?? ""; $service = $m["department"] ?? "";
$role = "Collaborateur"; // par défaut
if (!$email) continue; if (!$email) continue;
// Insertion ou mise à jour de lutilisateur
$stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, role) $stmt = $conn->prepare("INSERT INTO CollaborateurAD (entraUserId, prenom, nom, email, service, role)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE prenom=?, nom=?, email=?, service=?, role=?"); ON DUPLICATE KEY UPDATE prenom=?, nom=?, email=?, service=?");
if ($stmt) { if ($stmt) {
$stmt->bind_param("sssssssssss", $role = "Collaborateur"; // attribué uniquement si nouvel utilisateur
$stmt->bind_param("ssssssssss",
$entraUserId, $prenom, $nom, $email, $service, $role, $entraUserId, $prenom, $nom, $email, $service, $role,
$prenom, $nom, $email, $service, $role $prenom, $nom, $email, $service
); );
$stmt->execute(); $stmt->execute();
$usersInserted++; $usersInserted++;
} }
} }
// --- Réponse finale ---
echo json_encode([ echo json_encode([
"success" => true, "success" => true,
"message" => "Synchronisation terminée", "message" => "Synchronisation terminée",

View File

@@ -44,149 +44,100 @@ $comment = $data['comment'] ?? '';
try { try {
$conn->begin_transaction(); $conn->begin_transaction();
// Vérifier si validateur est Users ou CollaborateurAD // Vérifier que le validateur existe dans CollaborateurAD
$isUserValidator = false; $stmt = $conn->prepare("SELECT Id, prenom, nom FROM CollaborateurAD WHERE Id = ?");
$stmt = $conn->prepare("SELECT ID FROM Users WHERE ID = ?");
$stmt->bind_param("i", $validatorId); $stmt->bind_param("i", $validatorId);
$stmt->execute(); $stmt->execute();
$res = $stmt->get_result(); $validator = $stmt->get_result()->fetch_assoc();
if ($res->fetch_assoc()) {
$isUserValidator = true;
} else {
$stmt = $conn->prepare("SELECT Id FROM CollaborateurAD WHERE Id = ?");
$stmt->bind_param("i", $validatorId);
$stmt->execute();
$res = $stmt->get_result();
if (!$res->fetch_assoc()) {
throw new Exception("Validateur introuvable dans Users ou CollaborateurAD");
}
}
$stmt->close(); $stmt->close();
// Récupération demande if (!$validator) {
throw new Exception("Validateur introuvable dans CollaborateurAD");
}
// Récupération de la demande
$queryCheck = " $queryCheck = "
SELECT dc.Id, dc.EmployeeId, dc.CollaborateurADId, dc.TypeCongeId, dc.DateDebut, dc.DateFin, dc.NombreJours, SELECT dc.Id, dc.CollaborateurADId, dc.TypeCongeId, dc.DateDebut, dc.DateFin, dc.NombreJours,
u.Nom as UserNom, u.Prenom as UserPrenom, ca.prenom as CADPrenom, ca.nom as CADNom,
ca.nom as CADNom, ca.prenom as CADPrenom,
tc.Nom as TypeNom tc.Nom as TypeNom
FROM DemandeConge dc FROM DemandeConge dc
JOIN TypeConge tc ON dc.TypeCongeId = tc.Id JOIN TypeConge tc ON dc.TypeCongeId = tc.Id
LEFT JOIN Users u ON dc.EmployeeId = u.ID
LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id LEFT JOIN CollaborateurAD ca ON dc.CollaborateurADId = ca.Id
WHERE dc.Id = ? AND dc.Statut = 'En attente' WHERE dc.Id = ? AND dc.Statut = 'En attente'
"; ";
$stmtCheck = $conn->prepare($queryCheck); $stmtCheck = $conn->prepare($queryCheck);
$stmtCheck->bind_param("i", $requestId); $stmtCheck->bind_param("i", $requestId);
$stmtCheck->execute(); $stmtCheck->execute();
$resultCheck = $stmtCheck->get_result(); $requestRow = $stmtCheck->get_result()->fetch_assoc();
if (!($requestRow = $resultCheck->fetch_assoc())) {
throw new Exception("Demande non trouvée ou déjà traitée");
}
$stmtCheck->close(); $stmtCheck->close();
$employeeId = $requestRow['EmployeeId']; if (!$requestRow) {
throw new Exception("Demande non trouvée ou déjà traitée");
}
$collaborateurId = $requestRow['CollaborateurADId']; $collaborateurId = $requestRow['CollaborateurADId'];
$typeCongeId = $requestRow['TypeCongeId']; $typeCongeId = $requestRow['TypeCongeId'];
$nombreJours = $requestRow['NombreJours']; $nombreJours = $requestRow['NombreJours'];
$employeeName = $employeeId $employeeName = $requestRow['CADPrenom']." ".$requestRow['CADNom'];
? $requestRow['UserPrenom']." ".$requestRow['UserNom']
: $requestRow['CADPrenom']." ".$requestRow['CADNom'];
$typeNom = $requestRow['TypeNom']; $typeNom = $requestRow['TypeNom'];
$newStatus = ($action === 'approve') ? 'Validée' : 'Refusée'; $newStatus = ($action === 'approve') ? 'Validée' : 'Refusée';
// 🔹 Mise à jour DemandeConge // 🔹 Mise à jour DemandeConge
if ($isUserValidator) { $queryUpdate = "
$queryUpdate = " UPDATE DemandeConge
UPDATE DemandeConge SET Statut = ?,
SET Statut = ?, ValidateurId = ?,
ValidateurId = ?, ValidateurADId = ?,
ValidateurADId = NULL, DateValidation = NOW(),
DateValidation = NOW(), CommentaireValidation = ?
CommentaireValidation = ? WHERE Id = ?
WHERE Id = ? ";
";
} else {
$queryUpdate = "
UPDATE DemandeConge
SET Statut = ?,
ValidateurId = NULL,
ValidateurADId = ?,
DateValidation = NOW(),
CommentaireValidation = ?
WHERE Id = ?
";
}
$stmtUpdate = $conn->prepare($queryUpdate); $stmtUpdate = $conn->prepare($queryUpdate);
$stmtUpdate->bind_param("sisi", $newStatus, $validatorId, $comment, $requestId); $stmtUpdate->bind_param("siisi", $newStatus, $validatorId, $validatorId, $comment, $requestId);
$stmtUpdate->execute(); $stmtUpdate->execute();
$stmtUpdate->close(); $stmtUpdate->close();
// 🔹 Déduction solde (seulement Users, pas AD, hors maladie) // 🔹 Déduction solde (pas maladie)
if ($action === 'approve' && $typeNom !== 'Congé maladie' && $employeeId) { if ($action === 'approve' && $typeNom !== 'Congé maladie' && $collaborateurId) {
$currentDate = new DateTime(); $year = date("Y");
$year = ($typeNom === 'Congé payé' && (int)$currentDate->format('m') < 6)
? $currentDate->format('Y') - 1
: $currentDate->format('Y');
$queryDeduct = " $queryDeduct = "
UPDATE CompteurConges UPDATE CompteurConges
SET Solde = GREATEST(0, Solde - ?) SET Solde = GREATEST(0, Solde - ?)
WHERE EmployeeId = ? AND TypeCongeId = ? AND Annee = ? WHERE CollaborateurADId = ? AND TypeCongeId = ? AND Annee = ?
"; ";
$stmtDeduct = $conn->prepare($queryDeduct); $stmtDeduct = $conn->prepare($queryDeduct);
$stmtDeduct->bind_param("diii", $nombreJours, $employeeId, $typeCongeId, $year); $stmtDeduct->bind_param("diii", $nombreJours, $collaborateurId, $typeCongeId, $year);
$stmtDeduct->execute(); $stmtDeduct->execute();
$stmtDeduct->close(); $stmtDeduct->close();
} }
// 🔹 Notification (User ou CollaborateurAD) // 🔹 Notification
$notificationTitle = ($action === 'approve') ? 'Demande approuvée' : 'Demande refusée'; $notificationTitle = ($action === 'approve') ? 'Demande approuvée' : 'Demande refusée';
$notificationMessage = "Votre demande de $typeNom a été " . (($action === 'approve') ? "approuvée" : "refusée"); $notificationMessage = "Votre demande de $typeNom a été " . (($action === 'approve') ? "approuvée" : "refusée");
if ($comment) $notificationMessage .= " (Commentaire: $comment)"; if ($comment) $notificationMessage .= " (Commentaire: $comment)";
$notifType = ($action === 'approve') ? 'Success' : 'Error'; $notifType = ($action === 'approve') ? 'Success' : 'Error';
if ($employeeId) { $queryNotif = "
$queryNotif = " INSERT INTO Notifications (CollaborateurADId, Titre, Message, Type, DemandeCongeId)
INSERT INTO Notifications (UserId, CollaborateurADId, Titre, Message, Type, DemandeCongeId) VALUES (?, ?, ?, ?, ?)
VALUES (?, NULL, ?, ?, ?, ?) ";
"; $stmtNotif = $conn->prepare($queryNotif);
$stmtNotif = $conn->prepare($queryNotif); $stmtNotif->bind_param("isssi", $collaborateurId, $notificationTitle, $notificationMessage, $notifType, $requestId);
$stmtNotif->bind_param("isssi", $employeeId, $notificationTitle, $notificationMessage, $notifType, $requestId); $stmtNotif->execute();
$stmtNotif->execute(); $stmtNotif->close();
$stmtNotif->close();
} elseif ($collaborateurId) {
$queryNotif = "
INSERT INTO Notifications (UserId, CollaborateurADId, Titre, Message, Type, DemandeCongeId)
VALUES (NULL, ?, ?, ?, ?, ?)
";
$stmtNotif = $conn->prepare($queryNotif);
$stmtNotif->bind_param("isssi", $collaborateurId, $notificationTitle, $notificationMessage, $notifType, $requestId);
$stmtNotif->execute();
$stmtNotif->close();
}
// 🔹 Historique (User ou CollaborateurAD) // 🔹 Historique
$actionText = ($action === 'approve') ? 'Validation congé' : 'Refus congé'; $actionText = ($action === 'approve') ? 'Validation congé' : 'Refus congé';
$actionDetails = "$actionText $employeeName ($typeNom)"; $actionDetails = "$actionText $employeeName ($typeNom)";
if ($comment) $actionDetails .= " - $comment"; if ($comment) $actionDetails .= " - $comment";
if ($isUserValidator) { $queryHistory = "
$queryHistory = " INSERT INTO HistoriqueActions (CollaborateurADId, Action, Details, DemandeCongeId)
INSERT INTO HistoriqueActions (UserId, CollaborateurADId, Action, Details, DemandeCongeId) VALUES (?, ?, ?, ?)
VALUES (?, NULL, ?, ?, ?) ";
"; $stmtHistory = $conn->prepare($queryHistory);
$stmtHistory = $conn->prepare($queryHistory); $stmtHistory->bind_param("issi", $validatorId, $actionText, $actionDetails, $requestId);
$stmtHistory->bind_param("issi", $validatorId, $actionText, $actionDetails, $requestId);
} else {
$queryHistory = "
INSERT INTO HistoriqueActions (UserId, CollaborateurADId, Action, Details, DemandeCongeId)
VALUES (NULL, ?, ?, ?, ?)
";
$stmtHistory = $conn->prepare($queryHistory);
$stmtHistory->bind_param("issi", $validatorId, $actionText, $actionDetails, $requestId);
}
$stmtHistory->execute(); $stmtHistory->execute();
$stmtHistory->close(); $stmtHistory->close();
@@ -204,4 +155,3 @@ try {
} }
$conn->close(); $conn->close();
?>

View File

@@ -7,35 +7,73 @@ import Requests from './pages/Requests';
import Calendar from './pages/Calendar'; import Calendar from './pages/Calendar';
import Manager from './pages/Manager'; import Manager from './pages/Manager';
import ProtectedRoute from './components/ProtectedRoute'; import ProtectedRoute from './components/ProtectedRoute';
import EmployeeDetails from './pages/EmployeeDetails'; import EmployeeDetails from './pages/EmployeeDetails';
import Collaborateur from './pages/Collaborateur';
function App() { function App() {
return ( return (
<AuthProvider> <AuthProvider>
<Router> <Router>
<Routes> <Routes>
{/* Route publique */}
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/dashboard" element={
<ProtectedRoute> {/* Routes protégées */}
<Dashboard /> <Route
</ProtectedRoute> path="/dashboard"
} /> element={
<Route path="/demandes" element={ <ProtectedRoute>
<ProtectedRoute> <Dashboard />
<Requests /> </ProtectedRoute>
</ProtectedRoute> }
} /> />
<Route path="/calendrier" element={
<ProtectedRoute> <Route
<Calendar /> path="/demandes"
</ProtectedRoute> element={
} /> <ProtectedRoute allowedRoles={['Collaborateur', 'RH']}>
<Route path="/manager" element={ <Requests />
<ProtectedRoute> </ProtectedRoute>
<Manager /> }
</ProtectedRoute> />
} />
<Route path="/employee/:id" element={<ProtectedRoute><EmployeeDetails /></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 />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes> </Routes>
</Router> </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'; return 'bg-red-100 text-red-800';
case 'Validateur': case 'Validateur':
return 'bg-green-100 text-green-800'; return 'bg-green-100 text-green-800';
case 'Collaborateur':
return 'bg-cyan-600 text-white';
default: 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={` <div
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 className={`
${isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'} 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) */} {/* Bouton fermer (mobile) */}
<div className="lg:hidden flex justify-end p-4"> <div className="lg:hidden flex justify-end p-4">
<button onClick={onToggle} className="p-2 rounded-lg hover:bg-gray-100"> <button onClick={onToggle} className="p-2 rounded-lg hover:bg-gray-100">
@@ -43,7 +47,7 @@ const Sidebar = ({ isOpen, onToggle }) => {
{/* Logo */} {/* Logo */}
<div className="p-6 border-b border-gray-100"> <div className="p-6 border-b border-gray-100">
<div className="flex items-center gap-3"> <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" /> <Building2 className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
@@ -69,7 +73,11 @@ const Sidebar = ({ isOpen, onToggle }) => {
{user?.service || "Service non défini"} {user?.service || "Service non défini"}
</p> </p>
{user?.role && ( {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} {user.role}
</span> </span>
)} )}
@@ -82,7 +90,9 @@ const Sidebar = ({ isOpen, onToggle }) => {
<Link <Link
to="/dashboard" to="/dashboard"
onClick={() => window.innerWidth < 1024 && onToggle()} 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" /> <Home className="w-5 h-5" />
@@ -92,7 +102,9 @@ const Sidebar = ({ isOpen, onToggle }) => {
<Link <Link
to="/demandes" to="/demandes"
onClick={() => window.innerWidth < 1024 && onToggle()} 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" /> <FileText className="w-5 h-5" />
@@ -102,26 +114,37 @@ const Sidebar = ({ isOpen, onToggle }) => {
<Link <Link
to="/calendrier" to="/calendrier"
onClick={() => window.innerWidth < 1024 && onToggle()} 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" /> <Calendar className="w-5 h-5" />
<span className="font-medium">Calendrier</span> <span className="font-medium">Calendrier</span>
</Link> </Link>
{(user?.role === 'Validateur' || user?.role === 'Admin' || user?.role === 'Collaborateur') && ( {/* Rubrique dynamique Collaborateur / Validateur */}
<Link {(user?.role === "Collaborateur" ||
to="/manager" user?.role === "Validateur" ||
onClick={() => window.innerWidth < 1024 && onToggle()} user?.role === "Manager" ||
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" user?.role === "RH" ||
}`} user?.role === "Admin") && (
> <Link
<Users className="w-5 h-5" /> to={user?.role === "Collaborateur" ? "/collaborateur" : "/manager"}
<span className="font-medium"> onClick={() => window.innerWidth < 1024 && onToggle()}
{user?.role === 'Collaborateur' ? 'Mon équipe' : 'Équipe'} className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(user?.role === "Collaborateur" ? "/collaborateur" : "/manager")
</span> ? "bg-blue-50 text-cyan-700 border-r-2 border-cyan-700"
</Link> : "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> </nav>
{/* Bouton déconnexion */} {/* Bouton déconnexion */}

View File

@@ -118,13 +118,14 @@ export const AuthProvider = ({ children }) => {
initializeMsal(); initializeMsal();
}, []); }, []);
// Gérer l'authentification réussie
// Gérer l'authentification réussie // Gérer l'authentification réussie
const handleSuccessfulAuth = async (authResponse) => { const handleSuccessfulAuth = async (authResponse) => {
try { try {
const account = authResponse.account; const account = authResponse.account;
const accessToken = authResponse.accessToken; 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', { const graphResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
headers: { 'Authorization': `Bearer ${accessToken}` } headers: { 'Authorization': `Bearer ${accessToken}` }
}); });
@@ -143,8 +144,9 @@ export const AuthProvider = ({ children }) => {
// 🔹 Synchroniser lutilisateur dans la DB // 🔹 Synchroniser lutilisateur dans la DB
const syncResult = await syncUserToDatabase(entraUser, accessToken); 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") { if (syncResult?.role === "Admin") {
try { try {
const syncResp = await fetch(getApiUrl('full-sync.php'), { const syncResp = await fetch(getApiUrl('full-sync.php'), {
@@ -171,7 +173,13 @@ export const AuthProvider = ({ children }) => {
email: entraUser.mail || entraUser.userPrincipalName, email: entraUser.mail || entraUser.userPrincipalName,
userPrincipalName: entraUser.userPrincipalName, userPrincipalName: entraUser.userPrincipalName,
role: syncResult?.role || 'Employe', 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, jobTitle: entraUser.jobTitle,
department: entraUser.department, department: entraUser.department,
officeLocation: entraUser.officeLocation, officeLocation: entraUser.officeLocation,
@@ -191,6 +199,7 @@ export const AuthProvider = ({ children }) => {
}; };
// Connexion classique (email/mot de passe) // Connexion classique (email/mot de passe)
const login = async (email, password) => { const login = async (email, password) => {
try { try {

View File

@@ -414,7 +414,7 @@ END:VEVENT`;
)} )}
<button <button
onClick={() => setShowNewRequestModal(true)} 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" /> <Plus className="w-5 h-5" />
<span className="hidden sm:inline">Nouvelle demande</span> <span className="hidden sm:inline">Nouvelle demande</span>
@@ -634,10 +634,10 @@ END:VEVENT`;
{/* button csv */} {/* button csv */}
<div className="p-4"> <div className="p-4">
<div className="flex justify-end gap-2 mb-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 Export CSV
</button> </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 Export ICS
</button> </button>
</div> </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)} /> <Sidebar isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
<div className="lg:ml-60 flex items-center justify-center min-h-screen"> <div className="lg:ml-60 flex items-center justify-center min-h-screen">
<div className="text-center"> <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> <p className="text-gray-600">Chargement des données...</p>
</div> </div>
</div> </div>
@@ -226,7 +226,7 @@ const Dashboard = () => {
)} )}
<button <button
onClick={() => setShowNewRequestModal(true)} 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" /> <Plus className="w-5 h-5" />
<span className="hidden sm:inline">Nouvelle demande</span> <span className="hidden sm:inline">Nouvelle demande</span>
@@ -274,10 +274,10 @@ const Dashboard = () => {
<button <button
onClick={openManualResetPage} 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"> <div className="w-10 h-10 bg-cyan-600 rounded-lg flex items-center justify-center">
<Settings className="w-5 h-5 text-blue-600" /> <Settings className="w-5 h-5 text-cyan-600" />
</div> </div>
<div> <div>
<h3 className="font-medium text-gray-900">Interface d'administration</h3> <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-xl lg:text-2xl font-bold text-gray-900">{leaveCounters.availableCP}</p>
<p className="text-xs text-gray-500">jours</p> <p className="text-xs text-gray-500">jours</p>
</div> </div>
<div className="w-8 h-8 lg:w-12 lg:h-12 bg-blue-100 rounded-lg flex items-center justify-center"> <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-blue-600" /> <CalendarIcon className="w-4 h-4 lg:w-6 lg:h-6 text-cyan-600" />
</div> </div>
</div> </div>
</div> </div>
@@ -333,7 +333,7 @@ const Dashboard = () => {
<p className="text-gray-600 mb-4">Aucune demande récente</p> <p className="text-gray-600 mb-4">Aucune demande récente</p>
<button <button
onClick={() => setShowNewRequestModal(true)} 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 Faire votre première demande
</button> </button>

View File

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

View File

@@ -106,7 +106,7 @@ const Login = () => {
<div className="bg-white rounded-2xl shadow-xl p-6 lg:p-8"> <div className="bg-white rounded-2xl shadow-xl p-6 lg:p-8">
{/* Logo */} {/* Logo */}
<div className="text-center mb-6 lg:mb-8"> <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" /> <Building2 className="w-6 h-6 lg:w-8 lg:h-8 text-white" />
</div> </div>
<h1 className="text-xl lg:text-2xl font-bold text-gray-900">GTA</h1> <h1 className="text-xl lg:text-2xl font-bold text-gray-900">GTA</h1>
@@ -119,7 +119,7 @@ const Login = () => {
onClick={handleO365Login} onClick={handleO365Login}
disabled={isLoading} disabled={isLoading}
type="button" 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' ? ( {isLoading && authMethod === 'o365' ? (
<span>Connexion Office 365...</span> <span>Connexion Office 365...</span>
@@ -134,65 +134,13 @@ const Login = () => {
</button> </button>
</div> </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 */} {/* 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 */} {/* Affichage des erreurs */}
{error && ( {error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg"> <div className="p-3 bg-red-50 border border-red-200 rounded-lg">
@@ -213,21 +161,11 @@ const Login = () => {
</div> </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 */} {/* 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> </div>
</div> </div>

View File

@@ -2,11 +2,12 @@
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import Sidebar from '../components/Sidebar'; import Sidebar from '../components/Sidebar';
import { Users, CheckCircle, XCircle, Clock, Calendar, FileText, Menu, Eye, MessageSquare } from 'lucide-react'; import { Users, CheckCircle, XCircle, Clock, Calendar, FileText, Menu, Eye, MessageSquare } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
const Manager = () => { const Manager = () => {
const { user } = useAuth(); const { user } = useAuth();
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const isEmployee = user?.role === 'Employe'; const isEmployee = user?.role === 'validateur';
const [teamMembers, setTeamMembers] = useState([]); const [teamMembers, setTeamMembers] = useState([]);
const [pendingRequests, setPendingRequests] = useState([]); const [pendingRequests, setPendingRequests] = useState([]);
const [allRequests, setAllRequests] = useState([]); const [allRequests, setAllRequests] = useState([]);
@@ -15,6 +16,7 @@ const Manager = () => {
const [showValidationModal, setShowValidationModal] = useState(false); const [showValidationModal, setShowValidationModal] = useState(false);
const [validationComment, setValidationComment] = useState(''); const [validationComment, setValidationComment] = useState('');
const [validationAction, setValidationAction] = useState(''); const [validationAction, setValidationAction] = useState('');
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
if (user?.id) { if (user?.id) {
@@ -339,7 +341,9 @@ const Manager = () => {
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{teamMembers.map((member) => ( {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="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center"> <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"> <span className="text-blue-600 font-medium text-sm">

View File

@@ -199,7 +199,7 @@ const Requests = () => {
</div> </div>
<div className="hidden lg:flex items-center gap-3"> <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 <Plus className="w-4 h-4" /> Nouvelle demande
</button> </button>
</div> </div>