From 7e1266a965f99084cb5227293e50e95e62fa8336 Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 11 Jan 2026 21:43:26 +0100 Subject: [PATCH] Groups ui team ui --- app/api/users/[userId]/groups/route.ts | 156 +++++++++++ app/missions/equipe/page.tsx | 372 +++++++++++++++++++++++-- 2 files changed, 512 insertions(+), 16 deletions(-) create mode 100644 app/api/users/[userId]/groups/route.ts diff --git a/app/api/users/[userId]/groups/route.ts b/app/api/users/[userId]/groups/route.ts new file mode 100644 index 0000000..d5561f5 --- /dev/null +++ b/app/api/users/[userId]/groups/route.ts @@ -0,0 +1,156 @@ +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/options"; +import { NextResponse } from "next/server"; + +async function getAdminToken() { + try { + const tokenResponse = await fetch( + `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: process.env.KEYCLOAK_CLIENT_ID!, + client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, + }), + } + ); + + const data = await tokenResponse.json(); + if (!tokenResponse.ok || !data.access_token) { + console.error('Token Error:', data); + return null; + } + + return data.access_token; + } catch (error) { + console.error('Token Error:', error); + return null; + } +} + +export async function GET(req: Request, props: { params: Promise<{ userId: string }> }) { + const params = await props.params; + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + try { + const token = await getAdminToken(); + if (!token) { + return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 }); + } + + // Get user groups + const groupsResponse = await fetch( + `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.userId}/groups`, + { + headers: { + 'Authorization': `Bearer ${token}`, + }, + } + ); + + if (!groupsResponse.ok) { + const errorData = await groupsResponse.json(); + console.error("Failed to get user groups:", errorData); + return NextResponse.json({ error: "Failed to get user groups" }, { status: groupsResponse.status }); + } + + const groups = await groupsResponse.json(); + return NextResponse.json(groups); + } catch (error) { + console.error("Error in get user groups:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function POST(req: Request, props: { params: Promise<{ userId: string }> }) { + const params = await props.params; + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + try { + const { groupId } = await req.json(); + + const token = await getAdminToken(); + if (!token) { + return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 }); + } + + // Add user to group + const addResponse = await fetch( + `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.userId}/groups/${groupId}`, + { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + }, + } + ); + + if (!addResponse.ok) { + const errorData = await addResponse.json(); + console.error("Failed to add user to group:", errorData); + return NextResponse.json({ error: "Failed to add user to group" }, { status: addResponse.status }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error in add user to group:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function DELETE(req: Request, props: { params: Promise<{ userId: string }> }) { + const params = await props.params; + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + try { + const { searchParams } = new URL(req.url); + const groupId = searchParams.get('groupId'); + + if (!groupId) { + return NextResponse.json({ error: "groupId is required" }, { status: 400 }); + } + + const token = await getAdminToken(); + if (!token) { + return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 }); + } + + // Remove user from group + const removeResponse = await fetch( + `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.userId}/groups/${groupId}`, + { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + }, + } + ); + + if (!removeResponse.ok) { + const errorData = await removeResponse.json(); + console.error("Failed to remove user from group:", errorData); + return NextResponse.json({ error: "Failed to remove user from group" }, { status: removeResponse.status }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error in remove user from group:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/app/missions/equipe/page.tsx b/app/missions/equipe/page.tsx index d3a2711..4e066e2 100644 --- a/app/missions/equipe/page.tsx +++ b/app/missions/equipe/page.tsx @@ -1,10 +1,18 @@ "use client"; import { useState, useEffect } from "react"; -import { Search, Plus, MoreHorizontal, Trash2, Edit2, Users, UserPlus, X, Check, Loader2 } from "lucide-react"; +import { Search, Plus, MoreHorizontal, Trash2, Edit2, Users, UserPlus, X, Check, Loader2, FolderKanban } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; // Types interface User { @@ -25,7 +33,7 @@ interface Group { } type ActiveTab = "users" | "groups"; -type EditMode = null | { type: "user" | "group"; id: string; action: "edit" | "roles" | "members" }; +type EditMode = null | { type: "user" | "group"; id: string; action: "edit" | "roles" | "members" | "groups" }; export default function EquipePage() { const { toast } = useToast(); @@ -46,6 +54,21 @@ export default function EquipePage() { // Group members state const [groupMembers, setGroupMembers] = useState([]); const [availableUsers, setAvailableUsers] = useState([]); + + // User groups state + const [userGroups, setUserGroups] = useState([]); + const [availableGroups, setAvailableGroups] = useState([]); + + // New user dialog state + const [newUserDialogOpen, setNewUserDialogOpen] = useState(false); + const [newUserData, setNewUserData] = useState({ + username: "", + firstName: "", + lastName: "", + email: "", + password: "", + roles: [] as string[], + }); // Fetch data on mount useEffect(() => { @@ -114,6 +137,23 @@ export default function EquipePage() { setEditData({ roles: user.roles || [] }); }; + const handleManageUserGroups = async (user: User) => { + setActionLoading(user.id); + try { + const groupsRes = await fetch(`/api/users/${user.id}/groups`); + if (groupsRes.ok) { + const userGroupsData = await groupsRes.json(); + setUserGroups(Array.isArray(userGroupsData) ? userGroupsData : []); + setAvailableGroups(groups.filter(g => !userGroupsData.some((ug: Group) => ug.id === g.id))); + } + setEditMode({ type: "user", id: user.id, action: "groups" }); + } catch (error) { + toast({ title: "Erreur", description: "Impossible de charger les groupes", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + const saveUserEdit = async (userId: string) => { setActionLoading(userId); try { @@ -319,6 +359,107 @@ export default function EquipePage() { setEditData({}); setGroupMembers([]); setAvailableUsers([]); + setUserGroups([]); + setAvailableGroups([]); + }; + + const createUser = async () => { + setActionLoading("new-user"); + try { + const response = await fetch("/api/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ...newUserData, + roles: newUserData.roles, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to create user"); + } + + const data = await response.json(); + setUsers(prev => [...prev, data.user]); + setNewUserDialogOpen(false); + setNewUserData({ + username: "", + firstName: "", + lastName: "", + email: "", + password: "", + roles: [], + }); + toast({ title: "Succès", description: "Utilisateur créé" }); + fetchData(); // Refresh data to get updated counts + } catch (error) { + toast({ + title: "Erreur", + description: error instanceof Error ? error.message : "Échec de la création", + variant: "destructive" + }); + } finally { + setActionLoading(null); + } + }; + + const addUserToGroup = async (groupId: string) => { + if (!editMode || editMode.action !== "groups") return; + + setActionLoading(groupId); + try { + const response = await fetch(`/api/users/${editMode.id}/groups`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ groupId }) + }); + + if (!response.ok) throw new Error("Failed to add user to group"); + + const group = availableGroups.find(g => g.id === groupId); + if (group) { + setUserGroups(prev => [...prev, group]); + setAvailableGroups(prev => prev.filter(g => g.id !== groupId)); + setGroups(prev => prev.map(g => + g.id === groupId ? { ...g, membersCount: g.membersCount + 1 } : g + )); + } + + toast({ title: "Succès", description: "Utilisateur ajouté au groupe" }); + } catch (error) { + toast({ title: "Erreur", description: "Échec de l'ajout", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + const removeUserFromGroup = async (groupId: string) => { + if (!editMode || editMode.action !== "groups") return; + + setActionLoading(groupId); + try { + const response = await fetch(`/api/users/${editMode.id}/groups?groupId=${groupId}`, { + method: "DELETE" + }); + + if (!response.ok) throw new Error("Failed to remove user from group"); + + const group = userGroups.find(g => g.id === groupId); + if (group) { + setUserGroups(prev => prev.filter(g => g.id !== groupId)); + setAvailableGroups(prev => [...prev, group]); + setGroups(prev => prev.map(g => + g.id === groupId ? { ...g, membersCount: Math.max(0, g.membersCount - 1) } : g + )); + } + + toast({ title: "Succès", description: "Utilisateur retiré du groupe" }); + } catch (error) { + toast({ title: "Erreur", description: "Échec du retrait", variant: "destructive" }); + } finally { + setActionLoading(null); + } }; return ( @@ -327,16 +468,131 @@ export default function EquipePage() {

Gestion des équipes

-
- - setSearchTerm(e.target.value)} - /> +
+ {activeTab === "users" && ( + + + + + + + Nouvel utilisateur + +
+
+ + setNewUserData({ ...newUserData, username: e.target.value })} + className="mt-1" + required + /> +
+
+ + setNewUserData({ ...newUserData, firstName: e.target.value })} + className="mt-1" + required + /> +
+
+ + setNewUserData({ ...newUserData, lastName: e.target.value })} + className="mt-1" + required + /> +
+
+ + setNewUserData({ ...newUserData, email: e.target.value })} + className="mt-1" + required + /> +
+
+ + setNewUserData({ ...newUserData, password: e.target.value })} + className="mt-1" + required + /> +
+
+ +
+ {roles.map(role => ( + + ))} +
+
+
+ + +
+
+
+
+ )} +
+ + setSearchTerm(e.target.value)} + /> +
-
{/* Tabs */} @@ -436,21 +692,33 @@ export default function EquipePage() { onClick={() => handleEditRoles(user)} disabled={actionLoading === user.id} className="h-8 w-8 p-0" + title="Gérer les rôles" > +
@@ -550,6 +818,7 @@ export default function EquipePage() { {editMode.action === "edit" && "Modifier"} {editMode.action === "roles" && "Gérer les rôles"} {editMode.action === "members" && "Gérer les membres"} + {editMode.action === "groups" && "Gérer les groupes"} + + )) + )} + + + + {/* Available Groups */} +
+ +
+ {availableGroups.length === 0 ? ( +

L'utilisateur est dans tous les groupes

+ ) : ( + availableGroups.map(group => ( +
+ + {group.name} + + +
+ )) + )} +
+
+ + )} + {/* Manage Members Form */} {editMode.type === "group" && editMode.action === "members" && (