Vue global collaborateur pour manager
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 l’utilisateur
|
||||||
$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",
|
||||||
|
|||||||
@@ -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();
|
||||||
?>
|
|
||||||
|
|||||||
@@ -8,34 +8,72 @@ 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>
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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 l’utilisateur dans la DB
|
// 🔹 Synchroniser l’utilisateur 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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,64 +134,12 @@ 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 && (
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user