Missions/Equipe
This commit is contained in:
parent
54c7990ed8
commit
f9a61b566b
742
app/missions/equipe/page.tsx
Normal file
742
app/missions/equipe/page.tsx
Normal file
@ -0,0 +1,742 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Search, Plus, MoreHorizontal, Trash2, Edit2, Users, UserPlus, X, Check, Loader2 } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
// Types
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface Group {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
membersCount: number;
|
||||
}
|
||||
|
||||
type ActiveTab = "users" | "groups";
|
||||
type EditMode = null | { type: "user" | "group"; id: string; action: "edit" | "roles" | "members" };
|
||||
|
||||
export default function EquipePage() {
|
||||
const { toast } = useToast();
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>("users");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
|
||||
// Data
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [roles, setRoles] = useState<{ id: string; name: string }[]>([]);
|
||||
|
||||
// Inline edit states
|
||||
const [editMode, setEditMode] = useState<EditMode>(null);
|
||||
const [editData, setEditData] = useState<any>({});
|
||||
|
||||
// Group members state
|
||||
const [groupMembers, setGroupMembers] = useState<User[]>([]);
|
||||
const [availableUsers, setAvailableUsers] = useState<User[]>([]);
|
||||
|
||||
// Fetch data on mount
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [usersRes, groupsRes, rolesRes] = await Promise.all([
|
||||
fetch("/api/users"),
|
||||
fetch("/api/groups"),
|
||||
fetch("/api/roles")
|
||||
]);
|
||||
|
||||
if (usersRes.ok) {
|
||||
const usersData = await usersRes.json();
|
||||
setUsers(Array.isArray(usersData) ? usersData : []);
|
||||
}
|
||||
|
||||
if (groupsRes.ok) {
|
||||
const groupsData = await groupsRes.json();
|
||||
setGroups(Array.isArray(groupsData) ? groupsData : []);
|
||||
}
|
||||
|
||||
if (rolesRes.ok) {
|
||||
const rolesData = await rolesRes.json();
|
||||
setRoles(Array.isArray(rolesData) ? rolesData : []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de charger les données",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter functions
|
||||
const filteredUsers = users.filter(user =>
|
||||
user.username?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.firstName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.lastName?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const filteredGroups = groups.filter(group =>
|
||||
group.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// User actions
|
||||
const handleEditUser = (user: User) => {
|
||||
setEditMode({ type: "user", id: user.id, action: "edit" });
|
||||
setEditData({
|
||||
firstName: user.firstName || "",
|
||||
lastName: user.lastName || "",
|
||||
email: user.email || ""
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditRoles = (user: User) => {
|
||||
setEditMode({ type: "user", id: user.id, action: "roles" });
|
||||
setEditData({ roles: user.roles || [] });
|
||||
};
|
||||
|
||||
const saveUserEdit = async (userId: string) => {
|
||||
setActionLoading(userId);
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(editData)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update user");
|
||||
|
||||
setUsers(prev => prev.map(u =>
|
||||
u.id === userId ? { ...u, ...editData } : u
|
||||
));
|
||||
|
||||
toast({ title: "Succès", description: "Utilisateur modifié" });
|
||||
setEditMode(null);
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec de la modification", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const saveUserRoles = async (userId: string) => {
|
||||
setActionLoading(userId);
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}/roles`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ roles: editData.roles })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update roles");
|
||||
|
||||
setUsers(prev => prev.map(u =>
|
||||
u.id === userId ? { ...u, roles: editData.roles } : u
|
||||
));
|
||||
|
||||
toast({ title: "Succès", description: "Rôles mis à jour" });
|
||||
setEditMode(null);
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec de la mise à jour des rôles", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteUser = async (userId: string, email: string) => {
|
||||
if (!confirm("Êtes-vous sûr de vouloir supprimer cet utilisateur ?")) return;
|
||||
|
||||
setActionLoading(userId);
|
||||
try {
|
||||
const response = await fetch(`/api/users?id=${userId}&email=${encodeURIComponent(email)}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to delete user");
|
||||
|
||||
setUsers(prev => prev.filter(u => u.id !== userId));
|
||||
toast({ title: "Succès", description: "Utilisateur supprimé" });
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec de la suppression", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Group actions
|
||||
const handleEditGroup = (group: Group) => {
|
||||
setEditMode({ type: "group", id: group.id, action: "edit" });
|
||||
setEditData({ name: group.name });
|
||||
};
|
||||
|
||||
const handleManageMembers = async (group: Group) => {
|
||||
setActionLoading(group.id);
|
||||
try {
|
||||
const membersRes = await fetch(`/api/groups/${group.id}/members`);
|
||||
if (membersRes.ok) {
|
||||
const members = await membersRes.json();
|
||||
setGroupMembers(Array.isArray(members) ? members : []);
|
||||
setAvailableUsers(users.filter(u => !members.some((m: User) => m.id === u.id)));
|
||||
}
|
||||
setEditMode({ type: "group", id: group.id, action: "members" });
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Impossible de charger les membres", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const saveGroupEdit = async (groupId: string) => {
|
||||
setActionLoading(groupId);
|
||||
try {
|
||||
const response = await fetch(`/api/groups/${groupId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: editData.name })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update group");
|
||||
|
||||
setGroups(prev => prev.map(g =>
|
||||
g.id === groupId ? { ...g, name: editData.name } : g
|
||||
));
|
||||
|
||||
toast({ title: "Succès", description: "Groupe modifié" });
|
||||
setEditMode(null);
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec de la modification", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteGroup = async (groupId: string) => {
|
||||
if (!confirm("Êtes-vous sûr de vouloir supprimer ce groupe ?")) return;
|
||||
|
||||
setActionLoading(groupId);
|
||||
try {
|
||||
const response = await fetch(`/api/groups/${groupId}`, { method: "DELETE" });
|
||||
|
||||
if (!response.ok) throw new Error("Failed to delete group");
|
||||
|
||||
setGroups(prev => prev.filter(g => g.id !== groupId));
|
||||
toast({ title: "Succès", description: "Groupe supprimé" });
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec de la suppression", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const addMemberToGroup = async (userId: string) => {
|
||||
if (!editMode || editMode.action !== "members") return;
|
||||
|
||||
setActionLoading(userId);
|
||||
try {
|
||||
const response = await fetch(`/api/groups/${editMode.id}/members`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to add member");
|
||||
|
||||
const user = availableUsers.find(u => u.id === userId);
|
||||
if (user) {
|
||||
setGroupMembers(prev => [...prev, user]);
|
||||
setAvailableUsers(prev => prev.filter(u => u.id !== userId));
|
||||
setGroups(prev => prev.map(g =>
|
||||
g.id === editMode.id ? { ...g, membersCount: g.membersCount + 1 } : g
|
||||
));
|
||||
}
|
||||
|
||||
toast({ title: "Succès", description: "Membre ajouté" });
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec de l'ajout", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const removeMemberFromGroup = async (userId: string) => {
|
||||
if (!editMode || editMode.action !== "members") return;
|
||||
|
||||
setActionLoading(userId);
|
||||
try {
|
||||
const response = await fetch(`/api/groups/${editMode.id}/members/${userId}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to remove member");
|
||||
|
||||
const user = groupMembers.find(u => u.id === userId);
|
||||
if (user) {
|
||||
setGroupMembers(prev => prev.filter(u => u.id !== userId));
|
||||
setAvailableUsers(prev => [...prev, user]);
|
||||
setGroups(prev => prev.map(g =>
|
||||
g.id === editMode.id ? { ...g, membersCount: Math.max(0, g.membersCount - 1) } : g
|
||||
));
|
||||
}
|
||||
|
||||
toast({ title: "Succès", description: "Membre retiré" });
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec du retrait", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRole = (roleName: string) => {
|
||||
setEditData((prev: any) => ({
|
||||
...prev,
|
||||
roles: prev.roles.includes(roleName)
|
||||
? prev.roles.filter((r: string) => r !== roleName)
|
||||
: [...prev.roles, roleName]
|
||||
}));
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditMode(null);
|
||||
setEditData({});
|
||||
setGroupMembers([]);
|
||||
setAvailableUsers([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full bg-white">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-gray-100 py-3 px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-gray-800 text-base font-medium">Gestion des équipes</h1>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="Rechercher..."
|
||||
className="h-9 pl-9 pr-3 py-2 text-sm bg-white text-gray-800 border-gray-200 rounded-md w-60"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200 px-6">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => { setActiveTab("users"); cancelEdit(); }}
|
||||
className={`py-3 px-1 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "users"
|
||||
? "border-blue-600 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
<Users className="inline h-4 w-4 mr-2" />
|
||||
Utilisateurs ({users.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setActiveTab("groups"); cancelEdit(); }}
|
||||
className={`py-3 px-1 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "groups"
|
||||
? "border-blue-600 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
<Users className="inline h-4 w-4 mr-2" />
|
||||
Groupes ({groups.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto bg-gray-50 p-6">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-40">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-600" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-6">
|
||||
{/* Main List */}
|
||||
<div className={`bg-white rounded-lg border border-gray-200 overflow-hidden ${editMode ? "flex-1" : "w-full"}`}>
|
||||
{activeTab === "users" ? (
|
||||
/* Users Table */
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Utilisateur</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Rôles</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{filteredUsers.map(user => (
|
||||
<tr key={user.id} className={`hover:bg-gray-50 ${editMode?.id === user.id ? "bg-blue-50" : ""}`}>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 text-xs font-medium">
|
||||
{user.firstName?.[0] || ""}{user.lastName?.[0] || user.username?.[0] || "?"}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{user.firstName} {user.lastName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">@{user.username}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{user.email}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(user.roles || []).slice(0, 3).map(role => (
|
||||
<span key={role} className="px-2 py-0.5 text-xs bg-blue-50 text-blue-700 rounded-full">
|
||||
{role}
|
||||
</span>
|
||||
))}
|
||||
{(user.roles || []).length > 3 && (
|
||||
<span className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded-full">
|
||||
+{user.roles.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditUser(user)}
|
||||
disabled={actionLoading === user.id}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Edit2 className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditRoles(user)}
|
||||
disabled={actionLoading === user.id}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<UserPlus className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteUser(user.id, user.email)}
|
||||
disabled={actionLoading === user.id}
|
||||
className="h-8 w-8 p-0 hover:text-red-600"
|
||||
>
|
||||
{actionLoading === user.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
/* Groups Table */
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Groupe</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Chemin</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Membres</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{filteredGroups.map(group => (
|
||||
<tr key={group.id} className={`hover:bg-gray-50 ${editMode?.id === group.id ? "bg-blue-50" : ""}`}>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-purple-100 flex items-center justify-center">
|
||||
<Users className="h-4 w-4 text-purple-700" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900">{group.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{group.path}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded-full">
|
||||
{group.membersCount} membres
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditGroup(group)}
|
||||
disabled={actionLoading === group.id}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Edit2 className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleManageMembers(group)}
|
||||
disabled={actionLoading === group.id}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{actionLoading === group.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Users className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteGroup(group.id)}
|
||||
disabled={actionLoading === group.id}
|
||||
className="h-8 w-8 p-0 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{/* Empty states */}
|
||||
{activeTab === "users" && filteredUsers.length === 0 && (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<Users className="h-12 w-12 mx-auto mb-3 text-gray-300" />
|
||||
<p>Aucun utilisateur trouvé</p>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "groups" && filteredGroups.length === 0 && (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<Users className="h-12 w-12 mx-auto mb-3 text-gray-300" />
|
||||
<p>Aucun groupe trouvé</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Side Panel for Editing */}
|
||||
{editMode && (
|
||||
<div className="w-80 bg-white rounded-lg border border-gray-200 p-4 h-fit">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium text-gray-900">
|
||||
{editMode.action === "edit" && "Modifier"}
|
||||
{editMode.action === "roles" && "Gérer les rôles"}
|
||||
{editMode.action === "members" && "Gérer les membres"}
|
||||
</h3>
|
||||
<Button variant="ghost" size="sm" onClick={cancelEdit} className="h-8 w-8 p-0">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Edit User Form */}
|
||||
{editMode.type === "user" && editMode.action === "edit" && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">Prénom</label>
|
||||
<Input
|
||||
value={editData.firstName}
|
||||
onChange={(e) => setEditData({ ...editData, firstName: e.target.value })}
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">Nom</label>
|
||||
<Input
|
||||
value={editData.lastName}
|
||||
onChange={(e) => setEditData({ ...editData, lastName: e.target.value })}
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">Email</label>
|
||||
<Input
|
||||
value={editData.email}
|
||||
onChange={(e) => setEditData({ ...editData, email: e.target.value })}
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveUserEdit(editMode.id)}
|
||||
disabled={actionLoading === editMode.id}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{actionLoading === editMode.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Sauvegarder
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Roles Form */}
|
||||
{editMode.type === "user" && editMode.action === "roles" && (
|
||||
<div className="space-y-3">
|
||||
<div className="max-h-60 overflow-y-auto space-y-2">
|
||||
{roles.map(role => (
|
||||
<label
|
||||
key={role.id}
|
||||
className={`flex items-center gap-2 p-2 rounded-md cursor-pointer transition-colors ${
|
||||
editData.roles?.includes(role.name)
|
||||
? "bg-blue-50 border border-blue-200"
|
||||
: "bg-gray-50 border border-transparent hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editData.roles?.includes(role.name)}
|
||||
onChange={() => toggleRole(role.name)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{role.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveUserRoles(editMode.id)}
|
||||
disabled={actionLoading === editMode.id}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{actionLoading === editMode.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Mettre à jour
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Group Form */}
|
||||
{editMode.type === "group" && editMode.action === "edit" && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600">Nom du groupe</label>
|
||||
<Input
|
||||
value={editData.name}
|
||||
onChange={(e) => setEditData({ ...editData, name: e.target.value })}
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveGroupEdit(editMode.id)}
|
||||
disabled={actionLoading === editMode.id}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{actionLoading === editMode.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Sauvegarder
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manage Members Form */}
|
||||
{editMode.type === "group" && editMode.action === "members" && (
|
||||
<div className="space-y-4">
|
||||
{/* Current Members */}
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600 mb-2 block">
|
||||
Membres actuels ({groupMembers.length})
|
||||
</label>
|
||||
<div className="max-h-40 overflow-y-auto space-y-1">
|
||||
{groupMembers.length === 0 ? (
|
||||
<p className="text-xs text-gray-400 py-2">Aucun membre</p>
|
||||
) : (
|
||||
groupMembers.map(member => (
|
||||
<div key={member.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
|
||||
<span className="text-sm text-gray-700 truncate">
|
||||
{member.firstName} {member.lastName}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeMemberFromGroup(member.id)}
|
||||
disabled={actionLoading === member.id}
|
||||
className="h-6 w-6 p-0 hover:text-red-600"
|
||||
>
|
||||
{actionLoading === member.id ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<X className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Available Users */}
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600 mb-2 block">
|
||||
Ajouter des membres
|
||||
</label>
|
||||
<div className="max-h-40 overflow-y-auto space-y-1">
|
||||
{availableUsers.length === 0 ? (
|
||||
<p className="text-xs text-gray-400 py-2">Tous les utilisateurs sont membres</p>
|
||||
) : (
|
||||
availableUsers.map(user => (
|
||||
<div key={user.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
|
||||
<span className="text-sm text-gray-700 truncate">
|
||||
{user.firstName} {user.lastName}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => addMemberToGroup(user.id)}
|
||||
disabled={actionLoading === user.id}
|
||||
className="h-6 w-6 p-0 hover:text-green-600"
|
||||
>
|
||||
{actionLoading === user.id ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Plus, Users, FolderKanban } from "lucide-react";
|
||||
|
||||
export default function MissionsLayout({
|
||||
children,
|
||||
@ -11,6 +12,12 @@ export default function MissionsLayout({
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Check if we're on the equipe page or its subpages
|
||||
const isEquipePage = pathname === "/missions/equipe" || pathname.startsWith("/missions/equipe/");
|
||||
// Check if we're on the missions list or a mission detail page
|
||||
const isMissionsPage = pathname === "/missions" || (pathname.startsWith("/missions/") && !isEquipePage && pathname !== "/missions/new");
|
||||
const isNewMissionPage = pathname === "/missions/new";
|
||||
|
||||
return (
|
||||
<main className="w-full h-screen bg-white">
|
||||
<div className="w-full h-full px-4 pt-12 pb-4 flex">
|
||||
@ -24,16 +31,28 @@ export default function MissionsLayout({
|
||||
|
||||
{/* Navigation links */}
|
||||
<nav className="mt-4">
|
||||
<Link href="/missions" passHref>
|
||||
<div className={`px-6 py-[10px] ${pathname === "/missions" ? "bg-white" : ""} hover:bg-white`}>
|
||||
{/* Équipe link */}
|
||||
<Link href="/missions/equipe" passHref>
|
||||
<div className={`px-6 py-[10px] flex items-center gap-2 ${isEquipePage ? "bg-white" : ""} hover:bg-white transition-colors`}>
|
||||
<Users className="h-4 w-4 text-gray-600" />
|
||||
<span className="text-sm font-normal text-gray-700">Équipe</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Mes Missions with + button */}
|
||||
<div className={`px-6 py-[10px] flex items-center justify-between ${isMissionsPage ? "bg-white" : ""} hover:bg-white transition-colors group`}>
|
||||
<Link href="/missions" className="flex items-center gap-2 flex-1">
|
||||
<FolderKanban className="h-4 w-4 text-gray-600" />
|
||||
<span className="text-sm font-normal text-gray-700">Mes Missions</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href="/missions/new" passHref>
|
||||
<div className={`px-6 py-[10px] ${pathname === "/missions/new" ? "bg-white" : ""} hover:bg-white`}>
|
||||
<span className="text-sm font-normal text-gray-700">Nouvelle Mission</span>
|
||||
</div>
|
||||
</Link>
|
||||
</Link>
|
||||
<Link
|
||||
href="/missions/new"
|
||||
className={`p-1 rounded-md hover:bg-blue-100 transition-colors ${isNewMissionPage ? "bg-blue-100" : ""}`}
|
||||
title="Nouvelle Mission"
|
||||
>
|
||||
<Plus className="h-4 w-4 text-blue-600" />
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user